SyntaxError: Bad control character in string literal in JSON at position N 은 JSON 문자열 안에 이스케이프되지 않은 원시 제어 문자 —— 탭, 줄바꿈, 캐리지 리턴, 또는 U+0000–U+001F 범위의 어떤 문자 —— 가 있다는 뜻입니다. JSON 명세는 이를 금지합니다. 이 글은 왜 그렇고, 어디서 오며, 어떻게 없애는지 설명합니다.
어떤 문자열 오류를 보고 있나요?
- Bad control character —— 원시 탭/줄바꿈/null 바이트가 문자열 안에 이스케이프 없이 있음.
- Bad escaped character ——
\뒤에 JSON 이 허용하지 않는 게 옴 (예:\x, Windows 경로). - Unterminated string ——
"로 문자열을 열고 닫지 않음.
제어 문자란?
Unicode 의 처음 32 개 코드 포인트(U+0000~U+001F)는 제어 문자 —— 텔레타이프 시대에서 물려받은 보이지 않는 포매팅 코드입니다. 익숙한 것 들:
| 코드 포인트 | 이름 | JSON 이스케이프 |
|---|---|---|
U+0000 | Null | |
U+0008 | Backspace | \b |
U+0009 | 수평 탭 | \t |
U+000A | Line feed(줄바꿈) | \n |
U+000C | Form feed | \f |
U+000D | Carriage return | \r |
U+001B | Escape(ANSI 시퀀스의 시작) | |
JSON 명세(RFC 8259 §7)는 명시적입니다: 이 범위의 모든 문자가 JSON 문자열 안에 나타나면 반드시 이스케이프되어야 합니다. 따옴표 안의 원시 줄바꿈은 값이 아니라 구문 오류입니다.
왜 원시 줄바꿈이 JSON 을 깨뜨리나
실제 줄바꿈 문자가 들어 있는 문자열 값을 생각해 보세요:
// 바이트가 어떻게 보이는지 (줄바꿈은 리터럴, \n 이 아님):
{"message":"line one
line two"}
JSON.parse('{"message":"line one\nline two"}')
// SyntaxError: Bad control character in string literal in JSON at position 20파서는 여는 큰따옴표를 보고, 문자를 읽다가, 원시 0x0A 바이트를 만납니다. JSON 문자열은 같은 논리 줄 위에서 두 큰따옴표 사이에 끊김 없는 문자 시퀀스여야 하므로, 맨몸의 줄바꿈이 문자열을 예기치 않게 종료시킵니다.
올바른 표현은 두 문자 이스케이프 시퀀스 \n 을 쓰는 것:
// 올바름 —— \n 은 이스케이프이지 리터럴 줄바꿈이 아님
{"message":"line one\nline two"}
JSON.parse('{"message":"line one\nline two"}') // ✓ 동작제어 문자가 JSON 에 끼어드는 경로
1. 템플릿 리터럴이나 결합으로 JSON 문자열 만들기
const note = `first line
second line`; // 템플릿 리터럴의 실제 줄바꿈
const json = `{"note":"${note}"}`; // 원시 줄바꿈이 문자열에 임베드됨
JSON.parse(json); // SyntaxError: Bad control character…
// ✓ 해결: 값에는 항상 JSON.stringify, 수동 보간 절대 금지
const json = JSON.stringify({ note }); // 줄바꿈을 자동 이스케이프2. 파일을 읽어 내용을 그대로 임베드
import fs from 'fs';
const content = fs.readFileSync('notes.txt', 'utf8');
// content 에 \n, \r\n, \t 등이 있을 수 있음
// ❌ 수동 문자열 빌딩 —— 원시 제어 문자가 임베드됨
const json = '{"content":"' + content + '"}';
// ✓ JSON.stringify 가 모든 이스케이프를 처리
const json = JSON.stringify({ content });3. 출력을 이스케이프하지 않는 시스템의 API 응답
일부 백엔드 시스템(레거시 PHP 스크립트, 커스텀 직렬화기, JSON 을 수동으로 만드는 DB 트리거)은 이스케이프되지 않은 줄바꿈이나 탭이 들어간 필드 값을 내놓습니다. 받는 건 구문상 잘못된 JSON 이고 response.json() 가 던집니다.
진단은 원시 body 길이와 앞 200 자를 로그. 문자열 값이어야 할 곳에 보이는 줄바꿈이 있는지 확인하세요.
4. 터미널에서 복사-붙여넣기(ANSI 이스케이프 시퀀스)
터미널 출력에는 ANSI 색상 코드가 들어 있는데, Escape 문자(0x1B)로 시작해 [ 가 뒤따릅니다. 터미널 출력을 JSON 문자열에 붙여 넣으면 색상 리셋(\x1B[0m)마다 제어 문자가 됩니다.
// JSON 문자열에 붙여 넣은 터미널 출력:
{"log":"\u001B[32mOK\u001B[0m request processed"}
// ^^^ 원시 ESC 바이트 —— 6 문자짜리 \u001B 이스케이프 시퀀스여야 함5. 바이너리 데이터에서 온 null 바이트
바이너리 파일 일부, DB BLOB 컬럼, C 스타일 문자열을 JSON 필드에 읽어 들이면 null 바이트(U+0000)가 임베드될 수 있습니다 —— DB 에서 JSON 으로 가는 파이프라인에서 이 오류의 가장 흔한 원천.
고치는 법
예방: 항상 JSON.stringify 사용
결정적인 해결책입니다. JSON.stringify() 는 모든 제어 문자를 정확히 이스케이프합니다 —— 수동으로 이스케이프할 필요가 전혀 없습니다.
// ✓ userInput 에 뭐가 있든 안전
const json = JSON.stringify({ message: userInput });수리: 사후에 제거 또는 이스케이프
외부 출처에서 깨진 JSON 을 받고 프로듀서를 고칠 수 없다면, 파싱 전에 원시 문자열을 살균할 수 있습니다. 가장 안전한 방법은 맨몸의 제어 문자를 이스케이프하는 것:
function escapeControlChars(raw) {
return raw.replace(
/[\u0000-\u001F]/g,
(ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')
);
}
const fixed = escapeControlChars(rawFromApi);
const data = JSON.parse(fixed);제어 문자가 의미 있는 데이터가 아니라 노이즈(ANSI 코드, null 바이트)라면 그냥 제거하는 게 더 간단:
// \t, \n, \r(텍스트에 흔함)만 남기고 모든 제어 문자 제거
const cleaned = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
const data = JSON.parse(cleaned);현실의 또 다른 두 가지 원천
- 위치 0 의 UTF-8 BOM. "UTF-8 with BOM" 으로 저장된 파일은
0xEF 0xBB 0xBF로 시작합니다. RFC 8259 는 JSON 에서 BOM 을 금지하고JSON.parse가 position 0 오류로 거부하는데, 이걸 제어 문자 오류로 잘못 읽기 쉽습니다. 파싱 전에text.replace(/^/, '')로 제거하거나, 에디터에서 파일을 "UTF-8"(BOM 없음)으로 저장하세요. - VS Code: "Remove Control Characters". 한 파일을 수동으로 정리한다면 VS Code 의
Selection → Remove All Occurrences of Find Match과 정규식 검색[\x00-\x08\x0b\x0c\x0e-\x1f](진짜\t\n\r은 보존)을 결합하면 보이지 않는 제어 바이트를 한 번에 지웁니다. 자주 한다면 일부 확장에서 제공하는 "Remove Control Characters" 명령도 있습니다.
문제 위치 탐지
오류 메시지에 바이트 위치가 들어 있습니다. 그것으로 무엇이 거기 있는지 확인:
try {
JSON.parse(raw);
} catch (e) {
const pos = Number(e.message.match(/position (\d+)/)?.[1]);
if (!isNaN(pos)) {
const context = raw.slice(Math.max(0, pos - 20), pos + 20);
console.log('Context around error:', JSON.stringify(context));
// JSON.stringify 가 제어 문자를 다시 이스케이프해 보여줌
// 예: "...line one\nline two..." —— 그 \n 은 원래 원시 줄바꿈
}
}자주 묻는 질문
JSON 의 bad control character 가 뭔가요?
JSON 문자열 안에 있는 U+0000~U+001F 범위의 이스케이프되지 않은 원시 문자 —— 탭, 줄바꿈, 캐리지 리턴, null 바이트, 또는 ANSI escape. RFC 8259 §7 은 이런 문자들 모두 이스케이프되어야 한다고 요구합니다 (예: 줄바꿈은 \n).
bad control character 오류를 어떻게 고치나요?
결정적인 해결책은 JSON.stringify() 로 JSON 을 만드는 것 —— 자동으로 제어 문 자를 이스케이프합니다. 소스에서 못 고치는 깨진 JSON 을 받는다면 파싱 전에 정규식으로 제어 문자를 이스케이프하거나 제거하세요(위 스니펫 참조).
왜 원시 줄바꿈이 JSON 을 깨뜨리나요?
JSON 문자열은 두 큰따옴표 사이의 끊김 없는 시퀀스여야 합니다. 리터럴 줄바꿈 바이트(0x0A)는 문자열을 일찍 끝내므로 파서가 제어 문자 오류를 보고합니다. 유효한 표현은 두 문자 이스케이프 \n.
JSON 에서 ANSI 색상 코드를 어떻게 제거하나요?
ANSI 시퀀스는 Escape 바이트(U+001B)로 시작합니다. 파싱 전에 raw.replace(/\[[0-9;]*m/g, '') 로 제거하거나, 애초에 원시 터미널 출력을 JSON 문자열 값에 붙여 넣지 마세요.
브라우저에서 깨진 JSON 수리
깨진 JSON 을 fixjson.org 의 JSON Fix 에 붙여 넣으세요. 관대한 파서가 제어 문자 위반을 식별하고 정확한 위치를 보고합니다. 간단한 경우 문자열을 자동으로 수리할 수 있습니다.
- JSON Fix —— 잘못된 JSON 검증, 수리, 포매팅
- "[object Object] is not valid JSON" 고치기 —— JSON 구문 오류 완전 가이드
- JSON.parse 'Unexpected Token' 오류 고치는 법 —— SyntaxError 의 모든 변형과 원인
- Unexpected end of JSON input —— 구조가 닫히기 전에 잘렸을 때
- Unexpected token o 설명 —— JSON.parse 에 문자열 대신 객체를 넘겼을 때