결론부터: JSON.parse() 오류는 대개 「모델이 JSON을 못 쓴다」가 아니다. JSON 값 하나만 받는 파서에 채팅 답 전체를 넣었다는 뜻이다. JSON.parse는 JSON 문법(ECMA-262 / RFC 8259)의 값 하나만 받는다. 마크다운 펜스, 앞뒤 설명, 후행 쉼표, 이스케이프 안 된 줄바꿈, 잘림, JS/Python 방언은 바로 SyntaxError를 던진다. 2026년 수정 순서는 이렇다: Structured Output을 켜거나 Tool Calling의 arguments를 읽을 수 있으면 채팅 본문을 파싱하지 마라. 파싱해야 하면 추출한 뒤 parse하고, 그다음 JSON Schema로 검증하라 — 「parse될 때까지」 정규식으로 고치는 것부터 시작하지 마라.
2026년 9월 17일 기준. 본 사이트는 이미 AI Structured Output이란?, AI가 JSON Schema에 맞는 JSON을 생성하는 방법, OpenAI vs Gemini Structured Output, Gemini API로 구조화된 JSON 생성하기, Tool Calling은 왜 JSON Schema에 의존하는가를 다뤘다. 이 글은 모델 출력이 왜 JSON.parse를 통과하지 못하는지, 어떤 순서로 고칠지만 답한다.
JSON.parse가 실제로 받는 것
브라우저와 Node에서 JSON.parse가 구현하는 것은 JSON 텍스트이지, 「얼추 비슷한 JavaScript 객체 리터럴」이 아니다. 공백(스페이스, 탭, 줄바꿈, 캐리지 리턴)은 값 양옆에 올 수 있다. 그 외 입력은 정확히 하나의 값이어야 한다: 객체, 배열, 문자열, 숫자, true / false / null. 값 뒤에 공백이 아닌 문자가 오면 실패한다 — Chrome은 자주 Unexpected non-whitespace character after JSON이라고 쓴다.
아래 형태는 JS에서는 돌아가고 JSON에서는 죽는다. 모델은 학습 데이터에서 이를 자주 베낀다:
| 형태 | JS 객체 / JSON5 | JSON.parse |
|---|---|---|
| 후행 쉼표 | {"ok": true,} 가능 | 예외 |
| 작은따옴표 | {'ok': true} 가능 | 예외 |
| 주석 | // note 가능 | 예외 |
| 따옴표 없는 키 | {ok: true} 가능 | 예외 |
undefined / NaN / Infinity | 언어에 있음 | 예외 |
| 문자열 안의 날 줄바꿈 | 템플릿 문자열은 허용 | 예외; \n이어야 함 |
디버깅은 한 질문으로: 넘긴 것이 「JSON 값 하나」인가, 「모델이 읽기 쉽게 감싼 문단」인가? 전자는 파서의 일이다. 후자는 먼저 추출해야 한다.
실패 원인 분류 표
정확한 문구와 씨름하기 전에 SyntaxError를 분류하라. Chrome, Safari, Node는 같은 버그를 다르게 쓴다. 유형은 몇 개뿐이다:
| 유형 | 모델이 자주 내는 것 | 전형적인 결과 | 먼저 할 일 |
|---|---|---|---|
| 포장층 | ```json 펜스, 「JSON은 다음과 같다」 | 첫 문자가 { / [가 아님 | 펜스를 벗기고, 균형 잡힌 값을 잘라라 |
| 방언 | 후행 쉼표, 작은따옴표, 주석, 따옴표 없는 키 | Unexpected token | Structured Output으로 바꿔라; JS로 파싱하지 마라 |
| 깨진 문자열 | 이스케이프 안 된 ", 날 줄바꿈, 전각 쉼표 | 문자열이 일찍 끝나거나, 키 뒤에 :가 없음 | 열을 보고; 필드 길이를 제한하라 |
| 잘림 | 닫히지 않은 객체나 배열 | Unexpected end of JSON input | 출력 상한을 올리고; 스트림이 끝날 때까지 기다려라 |
| 다중 값 | JSON 두 개, 또는 첫 값 뒤의 설명 | 첫 값 뒤에 문자가 남음 | 첫 번째 완전한 값만 잘라라 |
| 인코딩 | BOM, 제로폭 문자, 이중 stringify | 이상한 토큰, 또는 parse 결과가 문자열 | BOM을 제거하고; 다시 parse하기 전에 typeof를 확인하라 |
Agent라면 하나를 더 기억하라: Tool Calling arguments는 이미 객체이거나, 벤더가 이미 제약한 JSON 문자열인 경우가 많다. assistant 메시지 전체를 JSON.parse에 넣지 마라. 다른 채널이다 — Tool Calling은 왜 JSON Schema에 의존하는가를 보라.
펜스와 앞뒤 설명
채팅 모델은 코드를 펜스에 넣도록 학습됐다. 「JSON만」이라고 써도 답은 자주 이렇다:
```json
{"ok": true, "id": "A-1024"}
```
Here is the result. I can explain the fields if you want.
첫 문자는 백틱이지 {가 아니다. JSON.parse는 0열에서 실패한다. 앞에 「네, JSON은 여기다:」를 붙이거나 뒤에 면책을 다는 것도 같은 버그다. 더 나쁜 경우: 값이 두 개 — 샘플 다음에 진짜 결과. 덩어리 전체를 파싱하면 첫 } 뒤에서 죽는다.
추출 규칙은 하나다: 첫 번째 균형 잡힌 {} 또는 []를 찾되(문자열 안의 괄호는 건너뛰고), 그 조각만 JSON.parse에 넘겨라. 펜스가 있으면 먼저 벗겨라. 첫 {부터 마지막 }까지 한 번에 자르지 마라 — 문자열 안의 괄호, 또는 설명에 붙은 두 번째 객체가 잘못 잘린다.
방언: 후행 쉼표, 작은따옴표, 주석, 따옴표 없는 키
모델은 JavaScript, Python, JSON5, YAML을 대량으로 봤다. 「구조화 데이터」를 달라고 하면 방언을 섞는다. 아래는 전부 JSON.parse에 불법이다:
{
ok: true, // bare key + comment
'name': 'Ada', // single quotes
"tags": ["a",], // trailing comma
"flag": True // Python boolean
}
undefined, NaN, Infinity, None도 더하라. 각 언어에서는 의미가 있다. JSON에는 null과 유한 숫자만 있다. 이런 형태를 「받기」 위해 JSON.parse를 eval이나 new Function으로 바꾸면 파서가 임의 코드 실행 구멍이 된다. 프로덕션에서 하지 마라.
JSON5와 JSONC는 주석과 후행 쉼표를 삼킨다. 사람이 설정을 고칠 때는 괜찮다. 모델 출력의 기본 파서로는 나쁘다. 문법을 느슨하게 하면 「쉼표 하나 더」와 「깨진 문자열」을 구분할 수 없다. 느슨한 층이 필요하면 추출 + parse 실패 뒤에만 두고, 수리 후에도 Schema를 돌려라.
문자열과 구두점: 이스케이프, 줄바꿈, 전각과 스마트 따옴표
적법한 JSON 문자열은 큰따옴표를 쓴다. 안의 "와 백슬래시는 이스케이프해야 한다. 제어 문자는 \n, \t, 또는 \uXXXX여야 한다. 모델이 사용자 댓글을 베끼면 날 따옴표와 줄바꿈이 필드 안에 떨어진다. 문자열이 일찍 끝나고, 다음 쉼표나 CJK 문자가 unexpected token이 된다.
CJK 출력에는 자주 나오는 오염이 있다: 전각 쉼표 ,, 전각 콜론 :, 굽은 따옴표 “” / ‘’. 구두점처럼 보이지만 코드포인트는 0x2C / 0x3A / 0x22가 아니다. 이 「거의 JSON」은 name 값 뒤에서 죽는다:
{
"name": "Ada",
"ok": true
}
「ASCII 구두점을 쓰라」는 문장을 하나 더 써서 고치지 마라. 긴 문자열 필드에 maxLength를 두고, 모델이 구두점을 다시 치지 말고 원문을 인용하게 하며, 최종 채널은 Structured Output을 써라. 디버깅할 때는 JSON 검증기에 붙여넣고 하이라이트가 어느 열에서 멈추는지 보라 — 전각 쉼표는 바로 보인다.
잘림과 스트리밍: Unexpected end of JSON input
Unexpected end of JSON input는 거의 항상 문법이 끝나기 전에 텍스트가 끊겼다는 뜻이다: } 없음, ] 없음, 또는 닫히지 않은 문자열. 2026년의 흔한 원인은 출력 토큰 상한, 안전 필터의 중간 절단, 또는 불완전한 스트림 chunk에 JSON.parse를 호출한 것이다.
스트리밍 API는 증분만 준다. 앞 chunk는 {"ok": tr일 수 있다. 그걸 parse하면 실패한다. 대신 이렇게 하라:
- 스트림이 끝날 때까지 기다리고(
finish_reason/stop), 전체 버퍼를 parse하라; - 또는 토큰 단위로 진행하는 진짜 스트리밍 JSON 파서를 써라 — 반쪽 값에
JSON.parse를 호출하지 마라; - 종료 이유가
length/max_tokens이면 파싱 버그가 아니다. 생성이 안 끝난 것이다 — 상한을 올리고, Schema를 줄이거나, 모델에 페이지를 나눠라.
잘린 뒤 중괄호를 자동으로 닫는 것은 초안용 수법이다. 형태는 parse돼도 필드가 빠지거나 문자열이 반으로 잘릴 수 있다. 수리 뒤에는 Schema 검증을 하고, 실패하면 재시도하라. 조용히 저장하지 마라.
보이지 않는 문자와 이중 인코딩
UTF-8 BOM(U+FEFF)은 JSON 공백이 아니다. 일부 복사 경로와 게이트웨이가 앞에 붙인다. 그러면 JSON.parse는 0열에서 unexpected token을 낸다. 제로폭 공백과 소프트 하이픈도 같다. 추출 전에 replace(/^\uFEFF/, "")로 지우고 trim하라.
이중 인코딩은 더 조용하다. JSON.stringify 한 번은 문자열 "{\"ok\":true}"를 만든다. 그 따옴표 친 형태를 parse하면 객체 대신 문자열 {"ok":true}가 나온다. 두 번째 parse에서 객체가 된다. 한 번만 parse하고 .ok를 읽으면 undefined다 — 「파싱은 됐고」 필드는 없다. 다시 parse하기 전에 typeof를 확인하라. 「항상 두 번 parse」를 하드코딩하지 마라. 진짜 객체면 예외가 난다.
수정 순서: 먼저 채널을 바꾸고, 추출하고, 수리는 마지막
이 순서가 프롬프트 문장을 쌓는 것보다 낫다:
- 채널을 바꿔라. 최종 답은 Structured Output으로 보낸다(OpenAI
response_format.json_schema, GeminiresponseMimeType+ Schema, Claudeoutput_config.format). 도구 파라미터는 Tool Callingarguments로 보내고, 산문에서 긁지 마라. AI Structured Output이란?을 보라. - 추출하라.
```json펜스를 벗기고, 첫 번째 균형 값을 자르고, BOM을 버려라. - 엄격하게 parse하라.
JSON.parse만 써라. 실패하면 원문과 오류 위치를 남겨라.eval하지 마라. - Schema를 검증하라. parse 성공은 문법만 맞다는 뜻이다. 빠진 필드, 잘못된 타입, 여분 키는 JSON Schema / ajv가 필요하다. AI가 JSON Schema에 맞는 JSON을 생성하는 방법을 보라.
- 수리는 마지막이다.
jsonrepair같은 도구는 중괄호를 닫고 후행 쉼표를 지울 수 있다. 추출 + parse가 실패한 뒤에만, 그리고 수리가 의미를 바꿀 수 있음을 받아들일 때만 써라. 그다음에도 3과 4를 돌려라. 수리기를 전역 기본 파서로 두지 마라.
프롬프트는 여전히 도움이 된다: 「펜스 없음, 설명 없음」. 포장층이 나올 확률을 낮춘다. Schema를 대체하지 않고, JSON.parse를 느슨하게 만들지도 않는다. 2026년에 채팅 산문을 API로 취급하면 펜스와 잘림에 계속 돈을 낸다.
작은 추출 + parse 파이프라인
가르침용 최소 파이프라인: 펜스를 벗기고, BOM을 버리고, 균형 값을 자른 뒤 JSON.parse. 흔한 포장은 처리한다. 후행 쉼표나 전각 구두점은 고치지 않는다 — 그건 Structured Output이나 명시적 수리 층에 맡겨라.
function stripFence(text) {
const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
return m ? m[1] : String(text);
}
function sliceBalancedJson(text) {
const src = text.replace(/^\uFEFF/, "").trim();
const start = src.search(/[\{\[]/);
if (start < 0) throw new SyntaxError("No JSON value found");
const open = src[start];
const close = open === "{" ? "}" : "]";
let depth = 0, inStr = false, esc = false;
for (let i = start; i < src.length; i++) {
const ch = src[i];
if (inStr) {
if (esc) { esc = false; continue; }
if (ch === "\\") { esc = true; continue; }
if (ch === '"') inStr = false;
continue;
}
if (ch === '"') { inStr = true; continue; }
if (ch === open) depth++;
else if (ch === close) {
depth--;
if (depth === 0) return src.slice(start, i + 1);
}
}
throw new SyntaxError("Unterminated JSON value");
}
function parseModelJson(raw) {
return JSON.parse(sliceBalancedJson(stripFence(raw)));
}
슬라이서는 문자열 안인지 추적해야 한다. 그렇지 않으면 필드 값의 {가 너무 일찍 닫힌다. 중첩 객체와 배열은 depth로 처리한다. 잘랐는데도 parse가 실패하면, 실패 텍스트를 검증기에 붙여넣고 위의 분류 표를 보라. 이 층에 정규식을 더 쌓지 마라.
로컬에서 오류 위치 보기
모델 출력을 바로 프로덕션 파서에 넣지 마라. 브라우저에서 세 가지를 확인하라: 적법한 JSON인가; 아니면 어느 열인가; 이미 Schema가 있으면 계약을 만족하는가.
- JSON 검증기 —
SyntaxError가 어디에 떨어지는지 보고, Schema가 있으면 같이 붙여라. - JSON 포맷터 — 포맷되면 대개 parse된다; 실패하면 원문의 전각 쉼표나 펜스를 찾아라.
- JSON Diff — parse 성공 뒤, 모델 객체와 당신이 허용하는 최소 객체를 비교하라.
데이터는 브라우저를 떠나지 않는다. 실패한 모델 답, Schema, Tool Calling arguments 덩어리를 나란히 보기 좋다. 필드 이름과 required가 안정되면 그때 Host에 연결하라.
FAQ
「JSON처럼 보이는데」도 JSON.parse가 거절하는 이유는?
사람은 펜스, 후행 쉼표, 굽은 따옴표, 앞뒤 설명을 눈감아 준다. JSON.parse는 RFC 8259 값 딱 하나만 받는다. 닮은 것과 적법한 JSON은 다르다.
```json 펜스를 지우는 정규식이면 충분한가?
충분하지 않다. 펜스는 포장층 하나일 뿐이다. 뒤에 설명 문장, 두 번째 JSON, 후행 쉼표, 잘림이 남는다. 펜스를 벗긴 뒤에도 균형 슬라이스와 엄격한 parse가 필요하다.
JSON Mode와 Structured Output은 무엇이 다른가?
JSON Mode는 대개 「JSON처럼 보이게」만 제약하고, 필드와 타입은 보지 않는다. Structured Output은 디코드 단계에서 JSON Schema로 불법 토큰을 막는다. 프로그램이 소비할 거면 Structured Output을 우선하라. JSON Mode만 켜고 채팅 본문을 JSON.parse하지 마라.
jsonrepair나 JSON5를 기본 파서로 써야 하나?
안 된다. 실패해야 할 입력을 삼키고, 의미를 바꿀 수 있다. 추출 + JSON.parse 실패 뒤의 수리 층으로만 쓰고, 그다음에도 Schema 검증을 하라.
스트리밍 응답에서 JSON.parse는 언제 호출하나?
스트림이 끝나고 버퍼가 완전한 값인 뒤에. 반쪽 chunk를 파싱하면 매번 Unexpected end of JSON input가 난다. 토큰이 오는 대로 소비하려면 스트리밍 파서를 쓰고, JSON.parse를 쓰지 마라.
parse는 됐는데 필드가 틀리다. 이 글의 범위인가?
다음 층이다. JSON.parse는 문법만 보증한다. 빠진 필드, 잘못된 타입, 여분 키는 Schema 문제다 — 본 사이트의 Structured Output과 Tool Calling 검증 글을 보라.
요약
JSON.parse 실패는 채널 문제다. 「JSON을 출력하라」는 문장을 하나 더 써서 고치지 못한다. 채팅 모델은 펜스를 두르고, 방언을 섞고, 토큰 상한에서 멈춘다. 파서는 깨끗한 JSON 값 하나만 받는다. 2026년에는 모델을 Structured Output 또는 Tool Calling arguments로 연결하고, 그다음 추출 + 엄격한 parse + Schema, 수리는 마지막이다.
프롬프트는 포장층을 줄일 수 있다. 문법을 느슨하게 만들지는 못한다. 실패 텍스트를 로컬 검증기에 붙여넣고 어느 열에서 멈추는지 본 뒤 결정하라: 펜스를 벗길지, 채널을 바꿀지, 출력 상한을 올릴지. 모델은 바뀐다. JSON.parse가 받는 것과 당신의 필드 계약은 바뀌면 안 된다.