사전 JSON
포맷

JSON

gabury1

JSON 은 데이터를 글자로 적어 두는 규칙입니다. 프로그램끼리 데이터를 주고받을 때 이 글자를 그대로 파일에 담거나 그대로 전송합니다. 사람이 열어 봐도 읽힙니다. 대신 담을 수 있는 값의 종류가 아주 적습니다.

쉽고 빠른 이해

JSON 은 프로그램끼리 주고받을 데이터를 글자로 적어 두는 규칙입니다. {"result":true, "count":42} 처럼 값 하나를 그대로 텍스트에 담습니다.

프로그래밍 언어마다 객체와 숫자를 표현하는 방식이 갈립니다. 언어끼리 데이터를 주고받을 때마다 그 차이를 일일이 맞춰야 한다면 번거롭습니다. JSON 은 모든 언어가 이미 아는 최소한의 공통 표기 하나로 그 문제를 피합니다.

돌아가는 방식은 이렇습니다.

  1. 값을 객체 · 배열 · 숫자 · 문자열 · true · false · null 일곱 갈래로만 한정합니다.
  2. 사람이 읽을 수 있는 텍스트로 적어서 파일에 담거나 그대로 전송합니다.
  3. 서로 다른 시스템끼리 주고받는 텍스트는 정해진 인코딩 하나로 통일해서 적습니다.

대가도 있습니다. 정수와 실수를 가르는 자리가 없어 큰 정수는 파싱 과정에서 정밀도를 조용히 잃을 수 있고, 주석을 적는 자리도 없습니다. 이미지나 동영상 같은 이진 데이터를 그대로 담는 자리도 없어서, 그런 데이터를 다뤄야 하는 자리에는 맞지 않습니다.

상세

JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)은 구조가 있는 데이터를 직렬화(메모리에 있는 데이터를 저장하거나 전송할 수 있는 형태로 바꾸는 것)하는 텍스트 포맷입니다. RFC(Request for Comments) 8259 는 JSON 을 텍스트 기반의 언어 독립적인 데이터 교환 포맷이라고 적습니다. ECMAScript 프로그래밍 언어 표준 3판에 정의된 JavaScript 의 객체 리터럴(코드에 값을 직접 적은 표현, 예: {a: 1})에서 파생됐습니다. JSON 이 정하는 것은 구조화된 데이터를 이식 가능하게 표현하기 위한 작은 형식 규칙 한 벌입니다.

담을 수 있는 값은 네 가지 원시형과 두 가지 구조형입니다. 원시형은 문자열, 숫자, 불리언, null 이고 구조형은 객체와 배열입니다. 문자열은 유니코드 문자를 0개 이상 늘어놓은 것입니다. 객체는 이름과 값의 쌍(멤버)을 0개 이상 담은 순서 없는 모음입니다. 이름은 문자열입니다. 배열은 값을 0개 이상 담은 순서 있는 나열입니다. 배열 안의 값이 모두 같은 타입이어야 한다는 요구는 없습니다. 같은 것을 ECMA-404(The JSON Data Interchange Format)는 값 일곱 갈래로 셉니다. 불리언 하나가 true 와 false 두 값으로 갈리는 만큼 하나가 늘어난 셈입니다. JSON 값은 객체, 배열, 숫자, 문자열, true, false, null 중 하나입니다.

이 포맷이 무엇으로 쪼개지는지는 다음과 같습니다. 객체의 값과 배열의 원소는 각각 다시 값 하나이므로, 객체와 배열은 서로를 담아 중첩할 수 있습니다.

flowchart TD
    T["JSON 텍스트"] --> V["값"]
    V --> O["객체"]
    V --> A["배열"]
    V --> N["숫자"]
    V --> S["문자열"]
    V --> TR["true"]
    V --> FA["false"]
    V --> NU["null"]
    O -->|0개 이상| M["이름과 값의 쌍"]
    M -.값 하나로 재귀.-> V
    A -->|원소 0개 이상| V

JSON 텍스트는 토큰의 나열입니다. 토큰에는 여섯 개의 구조 문자와 문자열, 숫자, 그리고 세 개의 리터럴 이름이 들어갑니다. 여섯 개의 구조 문자가 각각 차지하는 코드포인트(유니코드가 문자 하나하나에 매기는 고유 번호. U+ 뒤에 16진수로 적습니다)는 다음과 같습니다.

토큰 코드포인트 자리
[ U+005B 배열 열기
] U+005D 배열 닫기
{ U+007B 객체 열기
} U+007D 객체 닫기
: U+003A 이름과 값 가르기
, U+002C 값과 다음 이름 가르기

리터럴 이름은 true, false, null 셋뿐이고 반드시 소문자여야 합니다. 다른 리터럴 이름은 허용되지 않습니다. 토큰 앞뒤에는 의미 없는 공백을 둘 수 있습니다. 공백은 탭, 줄바꿈, 캐리지 리턴, 스페이스를 하나 이상 이은 것입니다. 문법에서는 이 공백을 ws(whitespace, 공백)로 적습니다. 토큰 안에는 공백이 들어갈 수 없습니다. 문자열 안의 스페이스만 예외입니다.

JSON 텍스트 하나는 직렬화된 값 하나입니다. 문법으로 적으면 JSON-text = ws value ws 입니다. 예전 JSON 명세 가운데 일부는 JSON 텍스트를 객체나 배열로 제한했습니다. 그래서 JSON 텍스트가 필요한 자리에 항상 객체나 배열만 내놓는 구현은 상호운용됩니다. 그런 구현이 내놓는 텍스트는 예전 명세를 따르는 구현이든 지금 명세를 따르는 구현이든 똑같이 적합한 JSON 텍스트로 받아들이기 때문입니다.

닫힌 생태계(외부와 데이터를 주고받지 않는 시스템들끼리의 묶음) 안에서만 오가는 JSON 텍스트가 아니라면 UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트)로 인코딩해야 합니다. 곧 외부 시스템과도 데이터를 주고받는 시스템이 만드는 JSON 텍스트는 반드시 UTF-8이어야 합니다. RFC 8259 가 MUST 로 못 박은 자리입니다. MUST · SHOULD · MAY 같은 대문자 낱말은 RFC 2119 가 정한 요구 강도 표시로, MUST 는 반드시 지켜야 한다는 뜻이고 SHOULD 는 권장, MAY 는 선택해도 된다는 뜻입니다. 이전 명세들은 전송할 때 UTF-8 을 요구하지 않았습니다. 다만 JSON 기반 소프트웨어 구현의 대다수가 UTF-8 을 골랐고, 상호운용을 이루는 유일한 인코딩이 될 정도였습니다. 구현은 네트워크로 전송되는 JSON 텍스트 앞에 바이트 순서 표시(Byte Order Mark, U+FEFF)를 붙여서는 안 됩니다. 파싱하는 쪽은 상호운용을 위해 그것을 오류로 보지 않고 무시해도 됩니다(MAY).

JSON 텍스트의 미디어 타입은 application/json 이고 파일 확장자는 .json 입니다. 이 미디어 타입을 처음 등록한 문서는 RFC 4627 입니다.

여기까지가 구문입니다. ECMA-404 는 JSON 구문이 완전한 데이터 교환의 명세가 아니라고 적습니다. 의미 있는 데이터 교환을 하려면 생산자와 소비자가 그 JSON 구문의 특정한 쓰임에 붙는 의미를 두고 합의해야 합니다. JSON 이 제공하는 것은 그런 의미를 붙일 수 있는 구문의 틀입니다. JSON 자체는 어떤 동작도 규정하지 않습니다.

표현 한계

이 포맷이 표현하지 못하는 값들은 조용히 바뀌는 방식으로 드러나기도 하고, 예외를 던지는 방식으로 드러나기도 합니다.

정수와 실수를 가르는 자리

JSON 에는 숫자가 한 종류뿐입니다. 숫자는 10진 숫자를 써서 밑 10 으로 적습니다. 정수부 앞에는 마이너스 부호를 선택적으로 붙일 수 있습니다. 정수부 뒤에는 소수부나 지수부가 따라올 수 있습니다. 앞자리 0 은 허용되지 않습니다. 문법으로 표현할 수 없는 수치 값은 허용되지 않습니다. Infinity 와 NaN(Not a Number, 숫자가 아님)이 그런 값입니다. ECMA-404 도 같은 자리를 막지만 RFC 8259 와는 다른 표현을 씁니다. RFC 8259 는 문법으로 표현할 수 없는 값이라고 적고, ECMA-404 는 자릿수의 나열로 나타낼 수 없는 값이라고 적습니다.

RFC 8259 는 구현이 받아들이는 숫자의 범위와 정밀도에 한계를 두는 것을 허용합니다. IEEE(Institute of Electrical and Electronics Engineers, 전기전자공학자협회) 754 binary64 배정밀도 숫자를 구현한 소프트웨어가 널리 쓰이므로, 그 이상의 정밀도나 범위를 기대하지 않는 구현들 사이에서는 상호운용이 잘 이뤄질 수 있습니다. 그런 구현들은 JSON 숫자를 기대하는 정밀도 안에서 근사합니다. 1E400 이나 3.141592653589793238462643383279 같은 JSON 숫자는 잠재적인 상호운용 문제를 가리킬 수 있습니다. 그것을 만든 소프트웨어가 받는 쪽에 널리 쓰이는 것보다 큰 수치 크기와 정밀도 처리 능력을 기대한다는 뜻이기 때문입니다. 그런 소프트웨어를 쓸 때, 정수이면서 [-(2**53)+1, (2**53)-1] 범위 안에 있는 숫자는 구현들이 그 수치 값에 정확히 합의한다는 뜻에서 상호운용됩니다.

RFC 7493 은 이 자리를 더 조입니다. 이 문서가 정하는 I-JSON(Internet JSON, 인터넷 JSON) 메시지를 만드는 구현은 받는 구현이 IEEE 754 배정밀도가 제공하는 것보다 큰 크기나 정밀도의 수치 값을 처리할 수 있다고 가정할 수 없습니다. I-JSON 메시지는 그보다 큰 크기나 정밀도를 나타내는 숫자를 담지 않아야 합니다(SHOULD NOT). 보내는 쪽은 절댓값이 9007199254740991 보다 큰 정수를 받는 쪽이 정확한 값으로 다뤄 주리라 기대할 수 없습니다. 더 큰 크기나 정밀도의 숫자를 정확히 교환해야 하는 응용에는 그 숫자를 JSON 문자열 값으로 인코딩하는 것이 권장됩니다. 이 우회는 포맷이 그 숫자를 담게 된 것이 아닙니다. 받는 프로그램이 그 값의 의도된 의미를 알아야 성립합니다.

이 손실은 파싱 시점에 이미 일어납니다. JSON 텍스트 안의 숫자는 JSON.parse() 의 두 번째 인자로 넘기는 reviver(파싱을 마친 각 값을 다시 손볼 수 있게 호출되는 함수)가 돌기 전에 이미 JavaScript 숫자로 변환되고, 그 과정에서 정밀도를 잃을 수 있습니다. 정밀도 손실 없이 큰 수를 옮기는 한 가지 방법은 문자열로 직렬화한 뒤 BigInt 나 다른 임의 정밀도 형식으로 되살리는 것입니다.

원문 텍스트에서 reviver 에 이르는 길은 두 갈래로 갈립니다. 하나는 reviver 의 두 번째 인자인 value 로 넘어오는 길인데 이미 JavaScript 숫자로 바뀌어 정밀도를 잃은 뒤입니다. 다른 하나는 reviver 의 세 번째 인자인 context 가 갖는 context.source 로 넘어오는 길인데, 파싱을 거치지 않은 원본 텍스트를 그대로 들고 있습니다.

flowchart TD
    O["JSON 텍스트 · 예: 12345678901234567890"] --> P["JSON.parse() 가 읽는다"]
    P --> L["JavaScript 숫자로 변환 · 정밀도를 잃는다"]
    L --> V["reviver 의 value 인자 · 이미 손실된 값"]
    P --> S["context.source · 원본 텍스트를 그대로 들고 있다"]

숫자에 관해 ECMA-404 는 JSON 이 숫자의 의미론에 대해 불가지론(특정한 내부 표현을 정해 두지도, 묻지도 않는다는 뜻)이라고 적습니다. 어느 프로그래밍 언어에나 여러 숫자 타입이 있을 수 있습니다. 용량도 보수 표현도 제각각입니다. 고정소수점과 부동소수점, 2진과 10진이 갈립니다. 그것이 언어 사이의 교환을 어렵게 만듭니다. 그래서 JSON 은 사람이 쓰는 숫자 표현, 곧 숫자의 나열만 제공합니다.

같은 이름이 두 번 나오는 자리

객체 안의 이름은 유일해야 합니다(SHOULD). 이름이 모두 유일한 객체는 그 객체를 받는 모든 소프트웨어 구현이 이름과 값의 대응에 합의한다는 뜻에서 상호운용됩니다. 객체 안의 이름이 유일하지 않을 때 그런 객체를 받는 소프트웨어의 동작은 예측할 수 없습니다. 많은 구현은 마지막 이름과 값의 쌍만 보고합니다. 다른 구현들은 오류를 보고하거나 객체 파싱에 실패합니다. 어떤 구현들은 중복을 포함해 모든 쌍을 보고합니다. ECMA-404 는 JSON 구문이 이름으로 쓰이는 문자열에 아무 제약도 두지 않고 이름 문자열이 유일할 것을 요구하지도 않는다고 적습니다.

RFC 7493 은 I-JSON 메시지의 객체가 이름이 중복되는 멤버를 가져서는 안 된다고(MUST NOT) 못 박습니다. 여기서 중복이란 이스케이프(문자열 안에서 그대로 쓸 수 없는 문자를 역솔리더스(\)로 시작하는 다른 표기로 바꿔 적는 것)된 문자를 처리한 뒤의 이름이 동일한 유니코드 문자 나열인 것을 말합니다.

멤버 순서

JSON 파싱 라이브러리들은 객체 멤버의 순서를 호출하는 소프트웨어에 보이는지 여부를 두고 서로 다르게 동작하는 것이 관찰돼 왔습니다. 동작이 멤버 순서에 의존하지 않는 구현은 그런 차이에 영향을 받지 않는다는 뜻에서 상호운용됩니다. ECMA-404 는 이름과 값 쌍의 순서에 어떤 의미도 부여하지 않는다고 적습니다. RFC 7493 은 I-JSON 메시지에서 객체 멤버의 순서가 메시지의 뜻을 바꾸지 않는다고 적습니다. 받는 구현은 멤버 순서만 다른 두 메시지를 동등하게 다뤄도 됩니다(MAY). 그래서 멤버 순서에 뜻을 싣는 코드는 구현마다 다르게 동작합니다.

문자열 안의 짝 없는 서러게이트

문자열은 따옴표로 열고 닫습니다. 따옴표 안에는 모든 유니코드 문자를 둘 수 있지만 반드시 이스케이프해야 하는 문자가 있습니다. 따옴표, 역솔리더스(\), 그리고 U+0000 부터 U+001F 까지의 제어 문자입니다. 기본 다국어 평면(Basic Multilingual Plane, U+0000 ~ U+FFFF)의 문자는 역솔리더스와 소문자 u 뒤에 16진수 네 자리를 붙인 여섯 글자로 적을 수 있습니다. 그 평면 밖의 문자는 UTF-16(Unicode Transformation Format 16-bit, 유니코드 변환 형식 16비트)이 코드포인트 하나를 16비트 코드 단위 둘로 쪼개 나타내는 서러게이트 페어(코드포인트 하나를 나타내려고 짝지어 쓰는 16비트 코드 단위 둘)를 인코딩한 열두 글자로 적습니다. 높은음자리표 U+1D11E 하나만 담은 문자열은 "\uD834\uDD1E" 로 적을 수 있습니다.

문제는 RFC 8259 의 ABNF(Augmented Backus-Naur Form, 확장 배커스-나우어 표기법)가 유니코드 문자를 인코딩할 수 없는 비트 나열까지 이름과 문자열 값에 허용한다는 점입니다. 짝이 없는 UTF-16 서러게이트 하나인 "\uDEAD" 가 그런 경우입니다. 라이브러리가 서러게이트 페어가 쪼개지는지 확인하지 않고 UTF-16 문자열을 잘라낸 자리에서 이런 사례가 관찰됐습니다. 그런 값이 든 JSON 텍스트를 받는 소프트웨어의 동작은 예측할 수 없습니다. 구현에 따라 문자열 값의 길이를 다르게 돌려주거나 치명적인 런타임 예외를 겪을 수도 있습니다. 서러게이트 페어를 합쳐서 코드포인트 하나(U+1D11E)로 볼지, 쪼개진 두 코드 단위 그대로 볼지는 ECMA-404 가 구현마다 정하는 의미론적 결정이라고 적는 자리입니다.

주석이 낄 자리

JSON 텍스트에 주석을 적는 문법은 없습니다. ECMA-404 가 세는 토큰은 여섯 개의 구조 토큰과 문자열, 숫자, 세 개의 리터럴 이름뿐입니다. 주석은 그 목록에 없습니다.

토큰 앞뒤에 둘 수 있는 것은 의미 없는 공백입니다. 그 공백은 탭(U+0009), 줄바꿈(U+000A), 캐리지 리턴(U+000D), 스페이스(U+0020) 네 코드포인트로 닫혀 있습니다. 그 밖의 글자는 토큰 사이에도 놓일 자리가 없습니다.

이진 데이터와 순환 구조

값의 목록에 바이트열을 담는 자리가 없습니다. ECMA-404 는 JSON 이 이진 데이터를 요구하는 응용에는 적합하지 않다고 적습니다. 같은 문단이 JSON 은 적어도 직접적으로는 순환 그래프를 지원하지 않는다고 적습니다. 이것은 예외로 드러납니다. JSON.stringify() 로 순환 참조가 있는 객체를 인코딩하려 하면 TypeError 가 던져집니다. JSON 포맷이 객체 참조를 지원하지 않기 때문입니다.

JavaScript 값 중 옮겨지지 않는 것

JSON.stringify() 로 JavaScript 값을 JSON 텍스트로 바꿀 때, undefined, Function, Symbol 값은 유효한 JSON 값이 아닙니다. 이 변환 도중 그런 값을 만나면 객체 안에서는 생략되고 배열 안에서는 null 로 바뀝니다. Infinity 와 NaN 은 앞의 표현 한계에서 본 것처럼 JSON 문법에 없는 값이라, 이 변환은 그 값을 null 로 바꿉니다. 앞의 undefined 등과 달리 생략되지는 않습니다. BigInt 값을 직렬화하려 하면 예외를 던집니다. 다만 그 BigInt 가 toJSON() 메서드를 가지고 있으면 그 메서드가 직렬화 결과를 내줄 수 있습니다.

예시

RFC 8259 가 싣는 객체 한 덩이

JSON
{
  "Image": {
      "Width":  800,
      "Height": 600,
      "Title":  "View from 15th Floor",
      "Thumbnail": {
          "Url":    "http://www.example.com/image/481989943",
          "Height": 125,
          "Width":  100
      },
      "Animated" : false,
      "IDs": [116, 943, 234, 38793]
    }
}

이 텍스트의 Image 멤버는 객체입니다. 그 객체의 Thumbnail 멤버도 객체입니다. IDs 멤버는 숫자의 배열입니다. 800 과 false 와 "View from 15th Floor" 가 각각 숫자, 리터럴 이름, 문자열입니다.

값 하나짜리 JSON 텍스트

JSON
"Hello world!"
JSON
42
JSON
true

RFC 8259 는 값만 담은 이 세 개의 작은 JSON 텍스트를 나란히 싣습니다. 객체나 배열로 감싸지 않아도 JSON 텍스트입니다.

JSON.parse() 호출 한 줄

JavaScript
const json = '{"result":true, "count":42}';

const obj = JSON.parse(json);

console.log(obj.count);
// Expected output: 42

console.log(obj.result);
// Expected output: true

MDN(Mozilla Developer Network)의 JSON.parse() 문서가 싣는 호출입니다. 글자로 적힌 "count":42 가 파싱을 거쳐 숫자 42 가 됩니다.

큰 정수가 조용히 바뀌는 한 줄

JavaScript
const bigJSON = '{"gross_gdp": 12345678901234567890}';

const bigObj = JSON.parse(bigJSON, (key, value, context) => {
  if (key === "gross_gdp") {
    // Ignore the value because it has already lost precision
    return BigInt(context.source);
  }
  return value;
});

12345678901234567890 은 앞의 표현 한계가 말한 범위 밖입니다. reviver 함수에 value 가 넘어온 시점에는 이미 정밀도를 잃은 뒤라, 원래 자릿수를 되찾으려면 context.source 가 들고 있는 원본 JSON 텍스트를 다시 읽어야 합니다.

배경

프로그래밍 언어들은 객체를 지원하는지, 지원한다면 그 객체가 어떤 성질과 제약을 갖는지를 두고 서로 크게 다릅니다. 객체 시스템의 모델은 제각각으로 갈릴 수 있습니다. 지금도 계속 진화합니다. 숫자도 마찬가지입니다. 언어마다 쓰는 숫자 타입이 여러 갈래로 나뉩니다. 그 차이가 서로 다른 언어 사이의 데이터 교환을 어렵게 만듭니다.

그래서 필요했던 것은 모든 언어가 이미 아는 최소한의 표기였습니다. JSON 은 객체 시스템 대신 이름과 값의 쌍을 모아 적는 단순한 표기만 제공합니다. 대부분의 언어에는 그런 모음을 표현하는 기능이 레코드, 구조체, 딕셔너리, 맵, 해시, 객체 같은 이름으로 있습니다. 순서 있는 목록도 배열, 벡터, 리스트 같은 이름으로 있습니다. 숫자도 마찬가지로, 언어마다 다른 내부 표현 대신 사람이 눈으로 읽는 자릿수 나열 하나로만 적습니다. 모든 언어는 내부 표현에 합의하지 않더라도 숫자 나열을 해석할 줄 압니다. 그것이면 교환에는 충분하다는 것이 이 포맷의 판단입니다. RFC 8259 는 JSON 의 설계 목표가 최소한이고 이식 가능하며 텍스트이고 JavaScript 의 부분집합인 것이었다고 적습니다.

이름은 그 부분집합에서 나왔습니다. JavaScript 의 객체 리터럴에서 착안했기에 JavaScript Object Notation 입니다. 다만 ECMAScript 의 내부 데이터 표현을 다른 언어에 강요하려 들지는 않습니다. 다른 모든 언어와 공유하는 것은 ECMAScript 구문의 작은 부분집합뿐입니다. JSON 은 2001년 JSON.org 웹사이트에서 처음 세상에 소개됐습니다. RFC 4627 은 Douglas Crockford 가 썼습니다. 지금의 RFC 8259 는 그 문서에 비교적 적은 수의 변경을 가해 만든 것입니다. 본문 대부분이 여전히 그의 글입니다. ECMA-404 는 앞선 정의들을 대체하는 문법 정본입니다. RFC 8259 와 ECMA-404 가 정하는 JSON 구문은 동일하도록 의도됐습니다. ECMA-404 는 JSON 이 너무 단순해서 그 문법이 앞으로 바뀌는 일은 없으리라 예상한다고 적습니다.

사용처

RFC 8259 는 실제로 JSON 이 쓰인 자리로 스무 개 남짓한 프로그래밍 언어를 꼽습니다 — ActionScript, C, C#, Clojure, ColdFusion, Common Lisp, E, Erlang, Go, Java, JavaScript, Lua, Objective Caml, Perl, PHP, Python, Rebol, Ruby, Scala, Scheme. 이 언어들로 짠 응용은 서로 다른 객체 모델과 숫자 타입을 씁니다. 그런데도 이 언어들 사이의 데이터 교환에 JSON 텍스트가 실제로 쓰였다고 RFC 8259 는 적습니다.

관련 항목

이 포맷이 기대는 표준

Unicode · UTF-8 · IEEE 754 · ABNF · ECMAScript · ECMA-262

JSON 을 정하는 문서

RFC 8259 · ECMA-404 · RFC 7493 · I-JSON · RFC 4627 · RFC 7159 · RFC 2119

실어 나르는 이름

미디어 타입 · application/json · IANA(Internet Assigned Numbers Authority, 인터넷 주소 관리 기관)

값을 다루는 호출

JSON.parse · JSON.stringify · BigInt

직렬화와 인코딩

직렬화 · 서러게이트 페어 · 바이트 순서 표시 · 상호운용성

다른 이름: JavaScript Object Notation · 제이슨