JSON 은 100 줄짜리 정규 문법 정의로 끝나는 단순한 형식이다. 그러나 파서를 직접 만들어 보면 — 왜 trailing comma 가 금지인지, 왜 JSON.parse('{"id":9999999999999999}') 가 10000000000000000 을 반환하는지, 왜 streaming 파서가 필요한지 — 결정을 거듭해야 한다. 이 가이드는 JSON 파서의 내부 구조와 흔히 놀라는 quirk 를 정리한다.
JSON 의 문법 — 전체 5 가지 값
value := object | array | string | number | true | false | null
object := { "key" : value , "key" : value ... }
array := [ value , value ... ]
string := " (escaped chars) "
number := -? digits ( .digits )? ( [eE] [+-]? digits )?그게 전부. 함수도 변수도 주석도 trailing comma 도 없음. 단순함이 강점 — 파서 구현 100 줄 안 가능.
파서의 2 단계 — Lexer → Parser
대부분의 JSON 파서는 두 단계로 분리:
1. Lexer (tokenizer) — 문자 시퀀스 → token
입력: {"id": 42, "name": "Yu"}
토큰:
LBRACE {
STRING "id"
COLON :
NUMBER 42
COMMA ,
STRING "name"
COLON :
STRING "Yu"
RBRACE }Lexer 가 공백·줄바꿈을 무시하고 의미 있는 단위만 추출. 한 글자 씩 읽으면서 상태 머신 (state machine) 진행:
"만남 → STRING state, 다음"까지- 숫자/
-만남 → NUMBER state t/f/n만남 → keyword (true/false/null) state- 예외 문자 → 에러
2. Parser — token 시퀀스 → 트리
재귀 하강 (recursive descent) 이 가장 흔함:
parseValue() {
switch (peek()) {
case LBRACE: return parseObject();
case LBRACKET: return parseArray();
case STRING: return consume().value;
case NUMBER: return parseNumber(consume());
case "true": consume(); return true;
...
}
}
parseObject() {
expect(LBRACE);
while (peek() !== RBRACE) {
const key = expect(STRING).value;
expect(COLON);
obj[key] = parseValue();
if (peek() === COMMA) consume();
else break; // ← 여기서 trailing comma 처리 분기
}
expect(RBRACE);
return obj;
}왜 trailing comma 가 금지일까
{"a": 1, "b": 2,} ← JSON 에러
[1, 2, 3,] ← JSON 에러Douglas Crockford 가 JSON 을 RFC 4627 로 표준화 (2006) 할 때 "minimal grammar" 를 우선했다. trailing comma 는:
- 파서 분기 추가 —
parseObject()의 comma 이후 RBRACE 허용 분기 필요 - PHP 의 PHP-JSON / 일부 API 응답 직렬화에서 의도치 않은 빈 요소 표현 가능 (Python
[1,2,]= 길이 2 vs 길이 3 혼동) - spec 단순화 vs DX 트레이드오프 — 단순화 채택
결과 — diff 가 깨진다. 라인 추가 시 이전 마지막 줄에 comma 추가 해야 함. JavaScript / Python / Go / Rust 다 trailing comma 허용 문법인데 JSON 만 X. 가장 자주 들리는 불평.
대안 — JSON5 / JSONC 가 trailing comma + 주석 허용. tsconfig.json 이 JSONC. 표준 JSON 은 여전히 X.
숫자의 정밀도 폭탄 — IEEE 754
JSON spec 은 숫자 표기에 제한 없음:
{"id": 9999999999999999} ← spec 상 valid그러나 JavaScript 의 JSON.parse() 결과는 Number 타입 — IEEE 754 double precision. 안전한 정수 범위 ±2^53 (= 9,007,199,254,740,992). 그 이상은 정밀도 손실:
JSON.parse('{"id":9999999999999999}').id
// 10000000000000000 ← 1 손실
Number.MAX_SAFE_INTEGER
// 9007199254740991 (= 2^53 - 1)Twitter API 가 옛날 이 사고에 당함. snowflake ID 가 64-bit 인데 JavaScript 클라이언트가 정밀도 잃음. 해결 — ID 를 string 으로 직렬화:
// Bad
{"id": 1234567890123456789}
// Good
{"id": "1234567890123456789"}다른 언어:
- Python —
json.loads()가 big int 허용 (int 무한 정밀도). 정밀도 손실 X. - Go —
json.Unmarshaldefault 가 float64.json.Number타입 사용해야 정밀도 보존. - Java — Jackson 의
BigInteger/BigDecimal옵션.
직접 확인 — JSON 포매터 / 검증기 에 큰 정수 박으면 트리 뷰에서 정밀도 손실 즉시 보임.
BigInt 와 JSON
JavaScript 의 BigInt 도입 (2020) 으로 큰 정수 지원. 그러나 JSON.stringify(123n) 은 TypeError — spec 이 BigInt 직렬화 미정의.
Workaround — toJSON() 메서드 또는 reviver:
BigInt.prototype.toJSON = function() { return this.toString(); };
JSON.stringify({id: 1234567890123456789n});
// '{"id":"1234567890123456789"}'String escape — 보이지 않는 함정
JSON string 안 허용 escape:
\" " (double quote)
\\ \ (backslash)
\/ / (slash, 선택)
\b backspace
\f form feed
\n newline
\r carriage return
\t tab
\uXXXX Unicode codepoint (4 hex)Surrogate pair 처리: U+10000 이상은 🎉 두 개로 분리 ("🎉"). 일부 파서가 unpaired surrogate 를 허용해 invalid UTF-8 생성 — 보안 issue.
중복 키의 운명
{"a": 1, "a": 2} ← spec 상 valid 인가?RFC 8259 — "key 이름은 unique 해야 한다" 권고. 그러나 강제 X. 대부분의 파서는:
- 마지막 값 채택 (JavaScript / Python / Go)
- 첫 값 채택 (일부 옛 파서)
- 배열로 보존 (CouchDB 등 특수)
보안 이슈 — 인증 토큰 검증 시 한 파서는 첫 값, 다른 파서는 마지막 값 채택 → 우회 공격 가능. 이중 직렬화 환경 (proxy → API) 에서 주의.
Streaming JSON — 메모리 한계
JSON.parse() 는 전체 문자열을 한 번에 파싱. 1 GB JSON 은 메모리 1 GB+. AWS Lambda / Cloud Function 메모리 한도 초과.
해결책 — streaming parser. 토큰 단위로 콜백:
- SAX-style — onObjectStart / onKey / onValue callback. 사용자가 트리 만들 책임.
- JSONPath streaming — 특정 경로만 추출. 큰 배열 안 element 하나씩 처리.
JSONStream(Node) /ijson(Python). - JSON Lines (JSONL / NDJSON) — 한 줄당 하나의 JSON 객체. 줄 단위 streaming 자연. 로그·analytics 표준.
{"user": "alice", "ts": 1700000000}
{"user": "bob", "ts": 1700000001}
{"user": "carol", "ts": 1700000002}
// 각 줄이 별개 JSON. 전체를 메모리에 안 올림.MongoDB EJSON — JSON 의 type 보강
BSON (MongoDB binary) 은 ObjectId / Date / Decimal128 같은 type 보유. JSON 은 string/number/boolean/null/object/array 5 가지뿐. MongoDB Extended JSON 은 wrapper 객체로 type 표현:
{
"_id": { "$oid": "507f1f77bcf86cd799439011" },
"created": { "$date": "2026-05-22T00:00:00Z" },
"price": { "$numberDecimal": "19.99" }
}MongoDB Extended JSON 가 16 가지 wrapper 인식. JSON 포매터 / 검증기 의 트리 뷰도 EJSON 자동 인식 옵션.
흔한 함정
1. JSON.stringify 의 undefined
JSON.stringify({a: undefined, b: 1}) // '{"b":1}' ← a 제거
JSON.stringify([undefined]) // '[null]' ← null 변환
JSON.stringify(undefined) // undefined ← 함수 자체가 undefined 반환2. NaN / Infinity
JSON.stringify({x: NaN}) // '{"x":null}'
JSON.stringify({x: Infinity}) // '{"x":null}'JSON spec 이 NaN / Infinity 미정의 → null 로 대체. 손실. 보존이 필요하면 string 으로.
3. 순환 참조
const a = {};
a.self = a;
JSON.stringify(a); // TypeError: Converting circular structure to JSON4. Date 의 자동 toJSON
JSON.stringify({d: new Date()})
// '{"d":"2026-05-22T05:30:00.000Z"}'
// Date 의 toJSON() 이 ISO 8601 string 반환
// parse 시 자동 복원 X
JSON.parse('{"d":"2026-05-22T05:30:00.000Z"}').d
// "2026-05-22T05:30:00.000Z" (string!)Date 복원은 reviver 필요:
JSON.parse(str, (k, v) =>
typeof v === "string" && /^\d{4}-\d{2}-\d{2}T/.test(v)
? new Date(v) : v);5. 큰 입력 typing freeze
JSON.parse 자체가 동기. 대용량 입력 시 main thread block. JSON 포매터 / 검증기 는 입력 4 KB 초과 시 useDebouncedValue 로 처리 (PR #77 패턴).
참고 자료
- RFC 8259 (JSON 표준) — datatracker
- json.org — Crockford 의 원조 페이지
- IEEE 754 (double precision) — Wikipedia
- MongoDB Extended JSON — 공식 문서
요약
- JSON 은 value 5 종 + object / array 만. 100 줄 안 파서 가능.
- Lexer (state machine) → Parser (recursive descent) 2 단계.
- trailing comma 금지 = spec 단순화의 트레이드오프. JSON5/JSONC 허용.
- Number = IEEE 754 double. ±2^53 밖 정수는 정밀도 손실. ID 는 string 으로.
- 중복 키 미정의 — 파서마다 다름. 보안 이슈 가능.
- Streaming = JSONL / NDJSON. 큰 데이터는 줄 단위.
- MongoDB EJSON 으로 Date / ObjectId / Decimal128 type 보강.
- 실험 — JSON 포매터 / 검증기 / JSON Path 추출기 / MongoDB Extended JSON / JSON → TypeScript / JSON Schema 생성기.