JSON 파헤치기...

 

오늘의 굴욕을 잊지말자. JSON을 모른다고? 제정신이 아니군

오늘의 굴욕을 잊지말자. 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 값으로는

  1. Number
  2. String
  3. Boolean
  4. Object
  5. Array
  6. 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에 보내기 전에 준비 해야 할 사항이 있다.

encodingParameterEncoding 타입이다. 그러면 나는 어떻게 보내면 되는 것일까?

먼저 나는 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
}
  1. JSONSerialization을 이용해서 [[String: Any]] 형태를 JSON Data 타입으로 변형시켜준다.
  2. 그리고 JSON Data를 String으로 변환시켜준다.

두번째 장애물

앞서 말했듯이 encodingParamterEncoding타입으로 되어 있다. 그러면 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

참고 자료 1

PS

새벽에 글을 쓰려고 하니 앞이 흐릿하다…

나와 같은 고민을 했다는 사람이 있다니 뭔가 안도의 한숨을 쉬게 된다. ㅎㅎ