JSON

최근 수정 시각:

1. 개요2. 구조3. 특징4. 단점5. JSONP
5.1. JSONP 디코더
5.1.1. 문자열 조작5.1.2. script 태그 사용5.1.3. Function 객체 사용
5.2. JSONP 인코더


JavaScript Object Notation[1]

JSON 홈페이지

1. 개요[편집]

일반적으로 서버에서 클라이언트로 데이터를 보낼 때 사용하는 양식. 클라이언트가 사용하는 언어에 관계 없이 통일된 데이터를 주고받을 수 있도록, 일정한 패턴을 지닌 문자열을 생성해 내보내면 클라이언트는 그를 해석해 데이터를 자기만의 방식으로 온전히 저장, 표시할 수 있게 된다.

과거 웹 초기 시절부터 사용되어 온 XML은 헤더와 태그 등의 여러 요소로 가독성이 떨어지고, 쓸데없이 용량을 잡아먹는다는 단점이 항상 지적되어 왔다. 이에 대응해 간결하고 통일된 양식으로 각광을 받고 있는 것이 JSON이다.

2. 구조[편집]

{
    "운영체제": [
        "macOS",
        "iOS",
        "Windows",
        "DOS"
    ],
    "회사": [
        "Apple",
        "Microsoft"
    ]
}


[]안의 것은 순서가 있는 배열[2], {}안의 것은 속성명(흔히 이름표라고 한다)이 있는 객체를 의미하며, 객체 안에 객체를 넣을 수도 있어서 XML처럼 복잡한 구조 또한 표현이 가능하다! 더 구체적으로 지원하는 값은 {}로 표기되는 객체, []로 표기되는 배열, 문자열, 숫자, 불리언, null의 6가지가 전부다. 또한 null의 존재에서 알 수 있듯, JSON은 JavaScript 이외의 언어에서 사용될 상황을 다분히 고려하고 있다.[3]

3. 특징[편집]

그래도 표면적으로 W3C표준XML인데, 그것과는 무관하게 JSON은 2009년 말 Ecma에 의해 ECMAScript5에서 스크립트 엔진의 기본 기능으로 내장되어 버렸기 때문에 새롭게 출시되는 브라우저들 기준에서는 DOM을 통해 XML 파싱하는 것보다 JavaScript 엔진에서 JSON을 메모리로 받는 쪽이 성능으로 보나 트래픽으로 보나 훨씬 더 나은 선택이 되어버렸다. 그런 관계로 웹에서 XML은 본격 계륵화가 진행중...

기본적으로 JavaScript 객체표기법의 부분집합이기 때문에 웹 브라우저 레벨에서 참 쉽게 해석할 수 있으며, 모양과 규칙 자체가 단순한 관계로 다른 언어에서 구현하기도 쉽다. 그래서 오늘날 사용되는 거의 모든 프로그래밍 언어에서 사용가능하다. 이러한 지원에 힘입어 AJAX 구현에도 상당히 빈번하게 쓰인다.

엄청나게 단순하지만 유연한 표기법이기 때문에 언어와 독립된 표준적인 데이터 표기법으로 확산되고 있다. 본래의 범위를 넘어서 바이너리 데이터 표기용으로 마개조한 BSON[4]도 있으며, NoSQL DB중 DocumentDB들에서는 그냥 표준 표기법으로 취급되고 있다. 최근에는 NoSQL뿐만 아니라 관계형DB인 PostgreSQL에서도 지원하기 시작. MySQL에서도 5.7에서 공식 지원한다.

그러나 확산되고 있다고 해도 어디까지나 웹 브라우저가 주가 되는 환경, 특히 사용자에게 보여주는 HTML 과 이를 실시간으로 가공하는 JavaScript가 연동하는 환경 위주로 퍼지고 있으며, 그렇지 않은 곳에서는 여전히 XML이 사용된다. XML 파서도 다양한 플랫폼에 구현돼있고 XML이란 놈이 딱히 무슨 결함이 있는 것도 아니며 결정적으로 XML에는 스키마(Schema)가 있어서 데이터의 무결성을 검증할 수 있다. JSON은 데이터의 무결성을 검증하는 역할을 전적으로 그걸 사용하는 측에 떠맡긴다.

그리고 JSON도 적용되는 곳이 늘어남에 따라 XML과 같이 valid한 검증뭐?이 필요해졌으며, Namespace 문제가 발생하는 경우가 늘고 있다. 이를 극복하기 위해 JSON-Schema, JSON-LD(JSON Linking Data) 같은 것이 등장하고 있다. 대항마라더니 결국 XML을 그대로 따라가고 있다.

그러나 대응책으로 제시된 JSON 스키마는 현재 거의 쓰이지 않고 있다. 웹이라는 환경 특성상 데이터가 좀 잘못 들어간다고 해서 특별히 큰일이 나는 게 아니기 때문이다. 게다가 데이터가 잘못 들어가면 프로그램이 강제 종료한다거나 하는 크리티컬한 응용에서는 그냥 XML을 사용하고 있다. JSON은 콤마가 누락되거나 중괄호가 잘못 닫히는 등의 오염에 아주 취약하다. XML은 SAX파서를 사용해서 이런 데이터 오류를 바로잡고 재동기화가 쉽다.

Unity3D 엔진에서 플러그인으로 시리얼라이저와 디시리얼라이저를 제공하기 시작하면서 모바일 플랫폼에서도 대거 사용되기 시작한다. CSV 등 기존 포멧에 비해 태그에 의한 데이타 확장과 변형이 용이하고 과거에 비해 데이타 사용량이 자유롭기 때문에 이런 현상이 가속화 되고 있다. (단적인 예로 많은 게임 데이터들이 제이슨으로 되어있다.)

Sublime Text, Visual Studio Code에서는 환경 설정 파일에 JSON을 사용하고 있다.

http://www.json.org에서 표기법과 언어별 지원 라이브러리를 볼 수 있다.

참고 : http://j.mearie.org/post/122845365013/serialization

4. 단점[편집]

XML이나 다른 데이터 기술 형식에 비해 아래와 같은 단점이 있다.

  • 문법 오류에 민감하다. 콤마가 누락되거나 중괄호가 잘못 닫히는 등 구두점에서 오타가 나면 전체 JSON파일이 망가진다.

  • 주석을 지원하지 않는다. 그래서 설정 파일을 JSON으로 작성하는 것은 어렵다.

  • 데이터 타입을 강제할 수 없다. JSON 스키마로 보완은 가능하지만 데이터 스스로 자신의 타입을 기술할 방법이 없다.


위와 같은 단점을 보완하고자 YAML 등의 대체 포맷이 등장했으나 기계간 데이터 전송에 주로 쓰이는 JSON의 특성상 위의 단점은 큰 문제가 아니라서 JSON은 2017년 11월 현재도 XML과 대등하게 널리 쓰이고 있다.물론 기계간의 데이터 전송에는 문자열 형식의 JSON보다는 바이너리 형식의 BSON이나 MessagePack이 더 용량도 작고 유용하다.

5. JSONP[편집]

OpenAPI 서비스 중 CORS가 설정되지 않은 구형 서비스에서 JSON 데이터를 돌려줄 때 사용하는 데이터 형식이다. P는 패딩(Padding)의 약어로, JSON데이터 객체를 함수 호출의 형식으로 감싸서 돌려준다. 이때 패딩하는 함수의 이름은 요청자가 정하게 되어 있으며 일반적으로 callback 파라메터를 사용해서 패딩 함수 이름을 서버에 전달해준다. 예로 jQuery의 jsonp는 jQuery89034758903475같은 난수 이름을 자동으로 생성한다.

예를 들어 https://jsonplaceholder.typicode.com/posts/1?callback=__cb12345 를 호출하면 아래와 같은 응답이 돌아온다.

/**/ typeof __cb12345 === 'function' && __cb12345({
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
});


이것은 내용 자체는 자바스크립트 코드이지만 사실 Content Type은 단순 문자열로, 이 문자열에서 실제 JSON데이터를 뽑아내기 위해서는 이것을 해석해야 한다. 방법은 여러 가지가 있다.

5.1. JSONP 디코더[편집]

사실 jQuery에 내장된 $.jsonp 함수를 쓰는 게 제일 편하지만 jQuery를 쓰기 곤란한 상황을 만날 수도 있다. jQuery이후에 등장한 서드파티 ajax라이브러리는 JSONP를 지원하지 않는 추세로 가고 있어서 JSONP형식 데이터를 해석하는 크롤러를 만들 때 크게 고통받는데 이때는 디코더를 직접 구현해야 한다.

5.1.1. 문자열 조작[편집]

제일 무식한 방법. 문자열의 첫 여는 중괄호부터 문자열의 가장 마지막 닫는 중괄호 사이의 모든 문자열을 추출한 뒤에 JSON.parse 함수로 처리하는 방법이다.

JSON.parse(res.substring(res.indexOf('{'), res.lastIndexOf('}')+1));

단 이 방법은 XSS의 방어 매커니즘을 무력화하는 방법이다. 자바스크립트가 아닌 곳, 예를 들어 Python이나 PHP같은 곳에서 JSONP데이터를 다룰 때에는 자바스크립트 엔진을 거치지 않아도 되는 이 방법이 가장 깔끔할 수도 있다. 어차피 자바스크립트가 아닌 곳에서 사용하는 JSON파서는 악성 코드를 만나면 무효한(Invalid) JSON객체 에러를 내며 중지될 뿐이기 때문이다.

사실 JSON.parse 함수도 악성 코드를 걸러내는 기능은 충실히 가지고 있다. 그렇기 때문에 현대의 웹 API 서버는 호환성 떨어지는 JSONP를 쓰지 않는다. 하지만 이건 서버가 결정하는 일이지 클라이언트(사용자)가 결정할 수 있는 일이 아니기 때문에 웹 크롤러를 다루다 보면 JSONP를 해석해야 할 날이 언젠가는 온다. 특히 JSON데이터를 직접 전달하려면 웹 서버에 CORS관련 설정을 해 줘야 하는데 이 설정을 제대로 하기가 까다로운 편이라 전문 인력을 확보하지 못한 서비스업체에서는 계속 JSONP방식을 고수하기도 한다.

참고로 JSON.parse가 아닌 eval메소드를 쓰는 방법도 있긴 하다. 하지만 eval을 JSONP 해석에 쓰는 건 보안 관점에서 극히 위험하기 때문에 서술하지 않는다.

5.1.2. script 태그 사용[편집]

가장 널리 알려진 방법으로, DOM에 scipt 태그를 주입해서 JSONP 콜백 객체를 전역 범위에 정의한 뒤 그것을 사용하는 방법이다.

var s = document.createElement('script');
s.src = 'https://jsonplaceholder.typicode.com/posts/1?callback=__cb12345';

document.body.appendChild(s);


appendChild 하기 전에 __cb12345 라는 이름의 함수를 미리 정의해 놓고 있어야 한다. 이 함수 안에서 수신받은 JSON객체의 처리 작업을 진행한다.
이 방법의 단점은 웹 브라우저에서만 동작한다는 것이다. 따라서 크롤러 봇에서는 이 방법으로 JSONP데이터를 긁어올 수 없다. Node.js용 DOM라이브러리를 쓰거나 PhantomJS같은 걸 쓰면 불가능하지는 않지만 겨우 JSONP데이터를 해석하는데 쓰기엔 너무 거대하고 무거운 라이브러리들이다.

전역 객체를 생성하기 때문에 전역 네임스페이스가 오염된다는 문제가 있다. 거의 가능성은 없지만 우연히 기존에 정의된 함수 이름을 JSONP 콜백 함수가 덮어써버려 페이지가 다운될 수도 있다.

5.1.3. Function 객체 사용[편집]

위의 script 태그 사용 방식은 Node.js같이 DOM객체를 갖지 않는 자바스크립트 환경에서는 쓸 수 없다는 문제가 있다. 그래서 서버 사이드에서는 Function 객체를 사용한다.

아래의 unwrapJSONP 함수는 HTTP 요청을 보내는 부분을 고의로 없앴다. 원래는 fetch함수의 콜백 함수였음. 파라메터 jsonp는 HTTP요청을 통해 받은 응답 JSONP 문자열이고 callbackname은 요청시 보낸 callback=? 의 콜백 함수 이름(문자열)이다. Promise 객체가 존재하는 곳에서만 사용할 수 있으므로 너무 오래된 브라우저에서는 Promise polyfill라이브러리를 먼저 로드해야 한다. 인터넷 익스플로러만 아니면 걱정 안 해도 된다

function unwrapJSONP(jsonp, callbackname) {
  var f = new Function(callbackname, jsonp);
  return new Promise(function(done) { f(function(d) { return done(d) }); });
}


ECMAScript2015 에서 추가된 화살표 함수를 사용한 방법
function unwrapJSONP(jsonp, callbackname) {
  let f = new Function(callbackname, jsonp);

  return new Promise(done => {
    f((d) => done(d));
  });
}


자바스크립트의 Function 생성자는 마지막 파라메터의 내용을 함수 본체로 하고 나머지 파라메터를 생성한 함수의 파라메터 목록으로 하는 새로운 익명 함수를 정의한다. 따라서 var f = new Function(...) 의 실행 결과 아래와 같은 함수가 생성된다.

function f(__cb12345) {
  /**/ typeof __cb12345 === 'function' && __cb12345({
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  });
}

이 함수는 지역 범위(로컬 스코프)에 정의되어 전역 네임스페이스를 오염시키지 않는다.

생성된 함수 f의 파라메터로 콜백 함수 d 를 넘겨주는데 이 콜백 함수 d에서 수신받은 JSON데이터를 처리한다.

사용할 때는 var jsonpObj = await unwrapJSONP(jsonp, '__cb12345'); 처럼 사용한다. async/await 를 사용할 수 없는 구형 브라우저 환경에서는 unwrapJSONP(jsonp, '__cb12345').then(function(jsonpObj) { ... }); 처럼 사용한다.

5.2. JSONP 인코더[편집]

패딩을 붙이는 것이기 때문에 매우 쉽게 만들 수 있다.

return "" + request.params.callback + "(" + jsonObj.toString() + ");";

이거면 충분하다. JSON객체를 문자열로 변환하고 그 앞뒤로 함수 호출 형식의 껍데기를 씌워주면 된다. 더 안전하게 하려면 callback 파라메터로 전달된 문자열이 유효한 함수 이름인지(밑줄을 제외한 특수문자가 섞여 있는지 등) 검사하는 로직을 덧붙여주면 된다.



[1] 제이슨이라고 읽는다.[2] 0,1,2,... 와 같은 숫자로 된 이름표가 자동으로 주어진다. 위 예제의 의하면 "0":"macOS", "1":"iOS".... 의 순서로 이름표가 매겨진다.[3] JavaScript에서 정말 아무것도 아니라는 의미는 undefined고 null은 null이라는 이름의 객체다(...)[4] MongoDB에서 쓰인다.