RFC 8259로 읽는 JSON 문법의 경계
핵심 요약: RFC 8259의 문법 규칙을 평이한 언어로 옮깁니다. 어떤 문자를 직접 쓸 수 있고, 무엇을 이스케이프해야 하며, 명세가 의도적으로 여지를 남긴 곳은 어디인지 정리합니다.

핵심부터 말하면, JSON 문법 오류가 나타나면 문서 전체를 다시 읽지 말고 첫 번째 불법 문자를 찾아 작은따옴표, 후행 쉼표, 주석, 제어 문자 중 무엇인지 분류하세요. 인코딩 측면에서는 BOM 없는 UTF-8인지 확인하고, 2^53 - 1을 넘는 정수는 문자열로 전달해야 합니다.
명세 본문은 열몇 페이지에 불과하지만, 모든 "MUST", "SHOULD", "MAY" 뒤에는 한 팀을 밤새 깨어 있게 만든 사건이 숨어 있습니다.
1. 세 문헌의 관계
JSON을 배울 때 RFC 4627, RFC 7159, RFC 8259, 그리고 ECMA-404까지 숫자가 섞여 혼란스러워지곤 합니다. 관계는 사실 아주 명확합니다:
| 문헌 | 시점 | 상태 | 주요 차이 |
|---|---|---|---|
| RFC 4627 | 2006 | 대체됨 | 최상위 값이 객체 또는 배열이어야 함 |
| RFC 7159 | 2014 | 대체됨 | 최상위 값이 임의의 JSON 값으로 완화됨 |
| RFC 8259 | 2017.12 | 현행 표준 | UTF-8 명시, 상호 운용성 지침 추가 |
| ECMA-404 | 2013, 제2판 2017.12 | 문법 명세 | 문법만 정의하며 인코딩과 전송은 다루지 않음 |
어느 것을 읽어야 할까? RFC 8259 하나면 충분합니다. ECMA-404는 문법 부분에서 동등하며, 차이는 RFC 8259가 "UTF-8로 전송"이라는 요구와 "구현자가 주의해야 할 상호 운용성 문제"까지 명시한다는 점입니다.
2. 여섯 가지 값 타입
명세의 문법 규칙은 하나의 도식으로 압축할 수 있습니다:
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
핵심은 여섯 가지입니다:
- 객체는 중괄호를 쓰고 멤버를 쉼표로 구분하며, 마지막 멤버 뒤의 쉼표는 허용되지 않습니다.
- 배열은 대괄호를 쓰고 요소를 쉼표로 구분하며, 후행 쉼표 역시 허용되지 않습니다.
- 숫자는 앞에 붙는
+를 허용하지 않고, 앞자리 0(01은 무효)도, 소수점만 있는 형태(1.은 무효)도,NaN이나Infinity도 허용하지 않습니다. - 문자열은 큰따옴표를 써야 하며 작은따옴표는 무효입니다.
- 불리언과 null은 소문자 리터럴입니다.
True와NULL은 모두 무효입니다. - 구조 요소 사이에는 공백 문자
0x20(공백),0x09(탭),0x0A(줄 바꿈),0x0D(캐리지 리턴)만 올 수 있습니다.
아래 스니펫은 유효한 JSON이지만, 처음 보는 사람은 대개 틀렸다고 생각합니다. 최상위 값이 문자열 하나이기 때문입니다:
"just a string"
RFC 7159 이후로 최상위 값은 정말 어떤 값이든 될 수 있습니다. 그렇다고 API에서도 그렇게 쓰라는 뜻은 아니며, 객체나 배열만 쓰는 것을 권장합니다. 이유는 6절에서 설명합니다.
3. 문자열 안에 직접 쓸 수 있는 문자
"내 컴퓨터에서는 잘 돌아가는데"라는 말에 가장 자주 속는 부분입니다. 명세가 문자열에 부과하는 규칙은 다음과 같습니다:
- 직접 쓸 수 있는 문자:
%x20-21,%x23-5B,%x5D-10FFFF - 다시 말해 큰따옴표
"(0x22)와 역슬래시\(0x5C)는 반드시 이스케이프해야 합니다. - 0x20 미만의 모든 제어 문자는 반드시 이스케이프해야 합니다. 줄 바꿈, 탭, 캐리지 리턴이 여기에 포함됩니다.
- 사용할 수 있는 이스케이프 시퀀스는
\",\\,\/,\b,\f,\n,\r,\t,\uXXXX뿐입니다.
{
"ok": "중국어 텍스트, 이모지 🎉, 전각 기호는 그대로 쓸 수 있습니다",
"escaped": "줄 바꿈은 여기에\n탭은 여기\t에서 끝납니다",
"control": "\u0007은 벨 문자라서 반드시 이스케이프해야 합니다",
"quote": "그가 말했습니다: \"작은따옴표는 쓰지 마세요\""
}
\/는 레거시 이스케이프입니다(</script>가 일찍 닫히는 것을 막는 용도). 현대 시나리오에서는 거의 필요 없지만, 파서는 반드시 지원해야 합니다.
4. 인코딩: UTF-8, BOM 없음
RFC 8259 8.1절은 세 가지를 말합니다:
- 교환용 JSON 텍스트는 반드시 UTF-8로 인코딩해야 합니다.
- 구현체는 텍스트 시작 부분에 BOM(
EF BB BF)을 추가해서는 안 됩니다. - 파서는 더 견고하게 동작하기 위해 BOM을 무시할 수 있습니다.
실무에서 흔한 두 가지 실패 사례:
- Windows 메모장이 "UTF-8(BOM 포함)"으로 저장한 파일을
JSON.parse가Unexpected token으로 거부합니다. - 중국어 JSON 파일을 GBK로 저장했는데 UTF-8로만 디코딩하는 파이프라인에서 깨진 문자로 변합니다. 그래도 문법 검사는 우연히 통과하므로 오류가 비즈니스 계층까지 미뤄집니다.
처리는 간단합니다. 파일을 읽을 때 BOM을 제거하고 명시적으로 UTF-8로 디코딩하며, 쓸 때는 BOM을 추가하지 않습니다.
/**
* 문자열 앞부분의 UTF-8 BOM을 제거합니다.
* @param text 원본 텍스트
* @returns BOM이 제거된 텍스트
*/
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
미디어 타입에 대해서도 한 가지: application/json은 이제 charset 파라미터를 받지 않습니다. 쓴다고 오류가 나지는 않지만, 이미 지난 관행입니다.
5. 명세가 의도적으로 여지를 남긴 세 곳
명세가 못을 박지 않은 곳이 바로 구현 차이가 생기는 곳입니다.
1. 숫자의 범위와 정밀도
RFC 8259는 "이 명세는 숫자의 범위나 정밀도를 제한하지 않는다"고만 말하면서, 대부분의 구현체가 IEEE 754 배정밀도를 쓴다는 점을 언급합니다.
그래서 같은 9223372036854775807이 JavaScript에서는 9223372036854776000이 되지만, Java의 long에서는 정확하게 유지됩니다.
결론: 2^53 - 1을 넘는 정수, 그리고 소수점 이하 정밀도가 많이 필요한 금액은 항상 문자열로 전달하고, 스키마에 pattern을 명시해야 합니다.
2. 중복 키
명세의 표현은 "객체 안의 이름은 중복이 없어야 한다(SHOULD)"이며, 중복일 때 어떻게 해야 하는지는 말하지 않습니다.
JSON.parse는 마지막 값을 남깁니다.- 일부 파서는 첫 번째 값을 남깁니다.
- 일부 라이브러리는 그냥 오류를 던집니다.
결론: 중복 키를 작성하지 말고, 검증기에서 특정 동작에 의존하지 마세요. 빌드 단계에서 "중복 키 검출"을 넣는 비용이 사후에 조사하는 비용보다 훨씬 쌉니다.
3. 깊이와 길이 제한
명세는 중첩 깊이에 제한을 두지 않지만, 모든 구현체에는 제한이 있습니다. V8은 중첩이 수만 단계를 넘으면 RangeError: Maximum call stack size exceeded를 던집니다.
서버는 자체 한도를 두어야 합니다. 이는 보안 요구이지 명세 요구가 아닙니다.
6. 반례로 이해도 점검하기
아래 각 줄은 무효한 JSON입니다. 파서에 넣어 오류 메시지가 유용한지 확인해 보세요:
{ 'name': 'json' } // single quotes
{ name: "json" } // key name not quoted
{"a": 1,} // trailing comma
[1, 2, 3,] // trailing comma in an array
{"a": 01} // leading zero
{"a": +1} // leading plus sign
{"a": .5} // missing integer part
{"a": NaN} // non-standard literal
{"a": undefined} // not a JSON value
{"a": 1} // comment // comment
재미있는 경계 사례가 있습니다. {"a": 1} 뒤에 주석이 붙은 형태는 JSON5에서는 유효하고 JSONC에서도 유효하지만 표준 JSON에서는 무효입니다.
그래서 "설정 파일은 잘 도는데 API가 거부한다"는 상황은 거의 항상 방언 문제입니다.
7. 운영 체크리스트
- [ ] 외부로 전송하는 JSON은 모두
charset없이application/json을 사용합니다. - [ ] 파일 읽기/쓰기 파이프라인은 BOM을 제거하고 명시적 UTF-8을 사용합니다.
- [ ]
2^53을 넘는 정수는 문자열로 전달하고 스키마에 표시합니다. - [ ] 중복 키는 빌드 단계에서 검출합니다.
- [ ] 서버는 요청 본문 크기와 중첩 깊이에 한도를 둡니다.
- [ ] 공개 API의 최상위 값은 객체나 배열이며, 스칼라 단독 값은 쓰지 않습니다.
FAQ
텍스트가 유효한 JSON인지 빠르게 판단하려면?
파서나 온라인 파싱 도구에 텍스트를 넣고 첫 번째 불법 문자의 위치를 확인한 다음, RFC 8259의 여섯 가지 값 타입과 공백 규칙에 비추어 보세요. 최상위 값은 어떤 JSON 값이든 될 수 있지만, 구조 요소 사이에는 공백, 탭, 줄 바꿈, 캐리지 리턴만 허용됩니다. 작은따옴표, 따옴표 없는 키, 후행 쉼표, 주석이 있으면 표준 JSON이 아닙니다.
JSON은 왜 반드시 UTF-8을 쓰고 BOM이 없어야 하나?
RFC 8259 8.1절은 교환용 JSON 텍스트를 UTF-8로 인코딩하도록 요구하고, 구현체가 시작 부분에 BOM을 추가해서는 안 된다고 하며, 파서는 견고성을 위해 BOM을 무시할 수 있다고 설명합니다. BOM이 있는 파일은 눈으로 보기에는 이상이 없는데도 JSON.parse가 Unexpected token을 던집니다. 따라서 읽을 때 BOM을 제거하고 명시적으로 UTF-8로 디코딩해야 합니다.
이런 문법 제한을 피하려고 JSON5나 JSONC를 쓰면 되나?
됩니다. 다만 설정 파일처럼 로컬에서 읽는 경우에 한하며, 시스템 간에 교환하는 데이터는 여전히 표준 JSON이어야 합니다. 반례에서 보았듯 {"a": 1} 뒤에 주석이 붙은 형태는 JSON5와 JSONC에서는 유효하지만 표준 JSON에서는 무효입니다. "설정 파일은 도는데 API가 거부한다"는 상황이 거의 항상 방언 문제인 이유입니다. 변환할 때는 주석, 작은따옴표, 후행 쉼표를 먼저 걷어 내고 결과를 표준 파서에 넘기세요.