오늘의 굴욕을 잊지말자. JSON을 모른다고? 제정신이 아니군
JSON 파헤치기…
개요
이번에 클라이언트 API를 작업하는 과정에서 혼란이 생겼다.
- Method : POST
Argument
Parameter | type | 설명 |
---|---|---|
badgeMenu | String | “APPROVAL” / “MAIL” / “SURVEY” / “CHAT” / “NOTIFICATION” ※대문자 |
useNoti | Boolean | 메뉴별로 앱 아이콘에 표시할지 여부 true/false |
[{
"badgeMenu" : "APPROVAL",
"useNoti" : false
}, {
"badgeMenu" : "MAIL",
"useNoti" : false
}, {
"badgeMenu" : "SURVEY",
"useNoti" : true
}, {
"badgeMenu" : "CHAT",
"useNoti" : true
}, {
"badgeMenu" : "NOTIFICATION",
"useNoti" : true
}]
위의 예시에 맞게 post를 할 때, body에 위 배열을 담아서 보내야 했다. 처음엔 이전에 했던 방식으로
struct BadgeData: Codable {
badgeMenu: String
useNoti: Bool
}
객체를 이용해서 [BadgeData]를 Parameter ( [String: Bool] ) 로 변환시켜 request body에 넣어 api를 만들려고 했다. 하지만 원하는대로 작동하지 않았다.
결국 문제를 해결하지 못하고 사수한테 질문을 하게 되었다. “ 위 모양대로 Parameters로 변환이 안되는데 뭐가 문제일까요…? “
그리고 사수는 말씀하셨다.
사수: JSON으로 만들지말고 그냥 Data타입으로 그냥 담아서 보내는게 어떨까요?
나: 예…?
사수: JSON이 뭐에요
나: 예..? 그게… 데이터를 보내는 약속 같은…
사수: JSON의 약자가 뭐에요
나: 음 .. 그게.. (속마음) 갑자기 그렇게 물어보시면 몰라요… ㅠ
그리고 사수는 다시 얘기를 시작했다.
사수: JSON은 기본적으로 Key - Value로 되어 있어요 근데 위에 예시는 그냥 배열로 담겨져 있잖아요. 저건 Parameters로 변환할 수 없어요.
그랬다… 처음부터 저건 JSON형태가 아닌 것이었다.
나는 그것도 모르고 그냥 삽질을 하면서 이틀을 보냈다. 씁….
이 기회를 삼아 간단하게 다시 JSON을 정리하고자 한다!! 울지말자 화이팅!
정의
JSON: JavaScript Object Notation
직역하자면, 자바스크립트 객체 표기법
말 그대로 자바스크립트에서 객체를 표기하는 방법이다. 그 표현 방법이 개발자가 보기기에도 가독성이 높고 컴퓨터가 이해할 수 있는 바이너리 처리를 하는데도 수월해서 다른 언어군에서도 데이터를 전달하는 표준으로 사용하고 있다.
특징
JSON은 Key - Value의 형태를 가지고 있다.
value 값으로는
- Number
- String
- Boolean
- Object
- Array
- NULL
을 가질 수 있다.
예시1)
// Object
struct Person: Codable {
name: String = “호권”
age: Int = “28”
subName: String = “호떡”
enName: String = “gwonii”
}
// JSON
{
“name” : “호권”,
“age” : 28,
“subName” : “호떡”,
“enName” : “gwonii”
}
위의 Person 객체를 JSON의 형태로 만들면 밑에 처럼 { }을 이용해서 표현할 수 있다.
일반적으로
객체는 { } 중괄호로 둘러쌓아 표현한다.
배열은 [ ] 대괄호로 둘러쌓아 표현한다.
그리고 데이터는 , (쉼표) 를 통해 구분한다.
돌아보기
여기서 처음 개요의 문제를 다시 돌아보면,
{
"badge" : [{
"badgeMenu" : "APPROVAL",
"useNoti" : false
}, {
"badgeMenu" : "MAIL",
"useNoti" : false
}, {
"badgeMenu" : "SURVEY",
"useNoti" : true
}, {
"badgeMenu" : "CHAT",
"useNoti" : true
}, {
"badgeMenu" : "NOTIFICATION",
"useNoti" : true
}]
}
의 형태로 만들어졌다면, 쉽게 JSON 형태로 request body에 담아 API post를 잘 할 수 있었을 것이다.
하지만 배열에 key값이 없기에 JSON 형태의 key - value로 표현할 수가 없다.
해결방법
그래서 [ [String: Any] ] 를 Parameters로 변환할 수 있는 방법을 찾아보았다. 문제를 해결하면 내용을 추가하려고 한다.
2020/09/01 Tue 추가된 내용
첫번째 장애물
[[String: Any]]
는 Parameters타입으로 변환할 수 없었다.
그렇다는 것은 Parameters
를 이용해서 httpBody에 값을 넣을 수 없다는 얘기인 것이다.
그래서 Parameters
를 이용하지 않고 String의 형태로 body에 담아서 request를 보내도록 도전해보았다.
1단계
나는 Parameters
를 사용할 순 없지만 [[String: Any]]
의 형태를 body에 담아보내야 하는 사실은 변함이 없다. 그래서 먼저 [[String: Any]]
타입을 만들도록 하였다.
let badgesSetting: [BadgeData] = [
.init(badgeMenu: "예시메뉴", useNoti: true),
... ,
... ,
...
]
var badges: [[String: Any]] = [ ]
badgeSetting.forEach { (bdageData) in
badges.append([
"badgeMenu": badgeData.badgeMenu,
"useNoti": badgeData.useNoti
])
}
위와 같은 방식으로 임의로 [[String: Any]]
을 만들어 준다.
2단계
Parameters
를 이용하는 것이 아니라 encoding
에 담아 보낸다!
Parameters
는 위에서 지겹도록 보듯이 [String: Any]
타입으로 되어 있다.
그래서 Paramters
는 [:] 빈 Dictonary를 보내고 encoding
에 body를 담아 보낸다.
encoding
에 보내기 전에 준비 해야 할 사항이 있다.
encoding
은 ParameterEncoding
타입이다. 그러면 나는 어떻게 보내면 되는 것일까?
먼저 나는 String의 형태로 body에 담아보낼 것이다. 그러면 보낼 String을 먼저 JSON의 모양으로 만들어 준다.
guard let data = try? JSONSerialization.data(withJSONObject: badgesData, options: []) else {
return
}
guard let badgesString = String(data: data, encoding: .utf8) else {
return
}
JSONSerialization
을 이용해서[[String: Any]]
형태를JSON Data
타입으로 변형시켜준다.- 그리고
JSON Data
를 String으로 변환시켜준다.
두번째 장애물
앞서 말했듯이 encoding
은 ParamterEncoding
타입으로 되어 있다. 그러면 String이 ParamterEncoding
을 순응해야 한다.
그래서 나는 String을 ParamterEncoding
을 순응하도록 만들 것이다.
extension String: ParameterEncoding {
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var request = try urlRequest.asURLRequest()
request.httpBody = data(using: .utf8, allowLossyConversion: false)
return request
}
}
그래서 위의 코드를 추가해보자. String도 Parameter로 encode 할 수 있도록 만들어주는 코드이다.
3단계
그러면 이제
self.request(
path: "/api/user/mobile/badge/notisetting",
method: .post,
parameters: [:],
encoding: badgesString)
request는 AF를 래핑하여 사용하고 있기에 기본적인 코드는 생략하도록 한다.
이렇게 encoding
에 String을 담아 보낼 수 있게 된다.
세번째 장애물
여기까지 모두 순조로웠으나,,,, request는 성공하지 못했다.
그 이유를 알기위해 서버의 에러 문구를 확인해보니
application/x-www-form-urlencoded; charset=utf-8
not allowed 라고 명시되어 있었다..
이건 또 무슨 말인가…
개념은 복잡하겠지만 원인은 간단했다.
Alamofire
에서는 기본적으로 body를 encode 하려고 할 때, application/x-www-form-urlencoded
encode 방식이 default로 정해져 있었다.
Alamofire 원문
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
/// `application/x-www-form-urlencoded; charset=utf-8`.
그러면 여기서 또 의문이 생겼다. 이전에 JSON을 body 담아 보낸적이 있는데 그건 또 무엇이란 말인가?
이것 또한 이유는 간단했다.
내가 Parameters
를 사용하지 않았기 때문이다. 내 예상이지만, Parameters
형태로 body에 보내려고 할 때에는 Alamofire
에서 자동적으로 Content-Type: application/json
을 header에 넣어주는 것 같다.
하지만 나는 encoding
에 String으로 넣었기 때문에 Alamofire
는 당연히 JSON 형태가 아니라고 판단하여
application/x-www-form-urlencoded; charset=utf-8
을 header에 담아 보낸 것이다.
4단계
그러면 나는 이제 문제의 원인도 알았으니 간단하게 해결해보자.
self.request(
path: "/api/user/mobile/badge/notisetting",
header: HTTPHeader(name: "Content-Type", value: "application/json"),
method: .post,
parameters: [:],
encoding: badgesString)
header를 추가하여 request를 보내보자…
그 결과 드디어 200 code를 받을 수 있게 되었다… (감격)…
2020/09/01 Tue
PS
새벽에 글을 쓰려고 하니 앞이 흐릿하다…
나와 같은 고민을 했다는 사람이 있다니 뭔가 안도의 한숨을 쉬게 된다. ㅎㅎ