JSON 문자열 이스케이프 완벽 가이드: 문자, stringify, 함정
JSON 문자열을 이스케이프한다는 것은 임의의 텍스트를 JSON 문서 안에 문자열 리터럴로 안전하게 들어갈 수 있는 형태로 바꾸는 작업을 뜻합니다. 큰따옴표, 백슬래시, 그리고 줄바꿈이나 탭 같은 제어 문자 등 몇몇 문자는 구조적 의미를 가지거나 JSON 문자열 안에 그대로 들어갈 수 없기 때문에, 각각 \", \\, \n 같은 안전한 이스케이프 시퀀스로 치환됩니다. 이 과정을 잘못하면 페이로드는 더 이상 파싱되지 않습니다.
이런 상황은 자주 생깁니다. 하나의 JSON 객체를 다른 객체의 문자열 필드로 중첩할 때, 여러 줄짜리 코드 조각을 설정 값에 붙여 넣을 때, curl용 REST 요청 본문을 손으로 만들 때가 그렇습니다. 이 가이드는 정확히 어떤 문자를 이스케이프해야 하는지, 이스케이프와 JSON.stringify의 혼동을 어떻게 정리하는지 다루고, JSON 안에 JSON을 넣는 중첩과 유니코드 이스케이프를 짚어 가며, 페이로드를 조용히 망가뜨리는 함정들을 정리합니다. 지금 당장 무언가를 이스케이프하고 싶다면 JSON 이스케이프 도구가 브라우저에서 처리해 줍니다. 왜 그렇게 동작하는지가 궁금하다면 아래 내용이 답이 됩니다.
JSON 문자열 이스케이프란 무엇인가
JSON 문자열 이스케이프는 원시 문자열을 JSON 문서 안에 안전하게 넣을 수 있는 형태로 변환하는 과정입니다. JSON은 구조적 의미를 가지는 소수의 문자를 예약해 둡니다. 큰따옴표 "는 문자열을 구분하고, 백슬래시 \는 이스케이프 시퀀스를 시작합니다. 거기에 더해 U+0020 미만의 제어 문자, 즉 줄바꿈, 탭, 캐리지 리턴은 JSON 문자열 안에 그대로 등장하는 것 자체가 허용되지 않습니다. 이스케이프는 이런 문자들을 안전한 시퀀스로 치환해 결과 문자열이 어디서든 깔끔하게 파싱되도록 합니다.
실제로 언제 필요할까요? 자주 마주치는 상황은 다음과 같습니다.
- JSON 중첩: 웹훅 봉투, Kafka 메시지, 감사 로그가 요청 본문을 문자열 필드로 저장하므로, 안쪽 JSON은 할당되기 전에 먼저 이스케이프되어야 합니다.
- 손으로 작성한 설정: 여러 줄짜리 셸 스크립트, SQL 쿼리, 코드 조각을 하나의 JSON 값에 넣는다는 것은 모든 줄바꿈을
\n으로 바꾼다는 뜻입니다. - REST 요청 본문:
curl이나 HTTP 클라이언트용 JSON 본문을 손으로 만들 때는 따옴표와 줄바꿈이 셸과 전송 경로를 무사히 통과해야 합니다. - 로그 안전 인코딩: 사용자가 입력한 내용을 구조화된 로그 한 줄에 기록할 때, 주입된 따옴표나 줄바꿈이 형식을 망가뜨리지 않도록 해야 합니다.
작업 순서도 중요합니다. 지저분하거나 신뢰할 수 없는 JSON에서 시작한다면 먼저 유효성 검사를 거쳐 잘 구성된 것을 이스케이프하세요. JSON 포맷터에 붙여 넣어 보기 좋게 정리하고 확인한 다음, 깨끗해진 결과를 이스케이프하면 됩니다. 쓰레기를 이스케이프하면 이스케이프된 쓰레기가 나올 뿐입니다.
JSON에서 반드시 이스케이프해야 하는 문자
JSON 명세는 정확하고 짧은 목록을 정의합니다. 일곱 개의 문자는 전용 두 글자 이스케이프를 가지며, U+0020 미만의 나머지는 모두 \uXXXX 유니코드 이스케이프로 대체됩니다. JSON 이스케이프 문자의 전체 목록은 다음과 같습니다.
| 문자 | 이스케이프 결과 | 비고 |
|---|---|---|
" (U+0022) | \" | 문자열 구분자 |
\ (U+005C) | \\ | 이스케이프 시작 문자 (json escape backslash 경우) |
| 줄바꿈 (U+000A) | \n | |
| 캐리지 리턴 (U+000D) | \r | |
| 탭 (U+0009) | \t | |
| 백스페이스 (U+0008) | \b | |
| 폼 피드 (U+000C) | \f | |
| U+0020 미만의 기타 제어 문자 | \uXXXX | 예: U+0000 → \u0000 |
이스케이프가 필요 없는 것이 무엇인지도 그만큼 중요합니다. 슬래시 /는 지극히 평범한 문자입니다(이스케이프는 선택 사항이며, 아래에서 다루는 한 가지 좁은 경우에만 유용합니다). 작은따옴표는 JSON이 구분자로 쓰지 않으므로 절대 이스케이프할 필요가 없습니다. 그리고 U+0020 이상의 모든 출력 가능한 문자는 é, 日, 😀 같은 모든 멀티바이트 UTF-8 문자를 포함해 그대로 두어도 유효합니다.
차이는 예로 보면 분명해집니다. 왼쪽은 원시 입력, 오른쪽은 이스케이프된 JSON 문자열 리터럴입니다.
Input:
She said "hello" then left.
Escaped:
"She said \"hello\"\tthen left."
큰따옴표는 \"가 되었고 탭은 \t가 되었습니다. 이제 이 문자열은 어떤 JSON 파서, 로그 줄, 요청 본문에도 안전하게 넣을 수 있습니다.
JSON 이스케이프 vs JSON Stringify: 무엇이 다른가
많은 튜토리얼이 건너뛰는 탓에 헷갈려 하는 사람이 많습니다. 이스케이프와 JSON.stringify는 서로 다른 두 작업이 아니라, 같은 하나의 작업을 보는 두 가지 관점입니다.
JSON.stringify(value)는 임의의 자바스크립트(JavaScript) 값을 그에 해당하는 JSON 텍스트 표현으로 직렬화합니다. 그 값이 마침 문자열이라면, 직렬화한다는 것은 큰따옴표로 감싸고 안쪽의 특수 문자를 이스케이프한다는 뜻입니다. 그것이 바로 JSON 이스케이프입니다. 그래서 JSON.stringify("a\tb")는 따옴표를 포함해 일곱 글자짜리 문자열 "a\tb"를 반환합니다.
남는 선택은 그 바깥쪽 따옴표를 원하는지 여부입니다. 이는 JSON 이스케이프 도구의 큰따옴표로 감싸기 옵션에 그대로 대응합니다.
| 모드 | 입력 a"b에 대한 출력 | 사용 시점 |
|---|---|---|
| 감싸기 켜기 | "a\"b" | JSON.stringify와 동일한, 완전한 JSON 문자열 리터럴. 변수에 할당하거나 콜론 뒤에 붙여 넣으세요. |
| 감싸기 끄기 | a\"b | 감싸는 따옴표 없이 이스케이프된 본문만. JSON 문서에서 따옴표를 직접 입력할 때 사용하세요. |
그래서 “json stringify”를 검색해 이 글에 닿았다면, 정리는 간단합니다. 문자열을 stringify 하는 것은 감싸기 켜기 이스케이프와 같습니다. 따옴표 없는 형태는 거기서 바깥쪽 따옴표만 벗겨 낸 것입니다.
코드에서 JSON용으로 문자열을 이스케이프하는 방법
핵심 원칙은 하나입니다. replace() 호출을 줄줄이 손으로 엮지 마세요. 주요 언어는 모두 따옴표, 백슬래시, 제어 문자, 유니코드를 올바르게 처리하는 JSON 직렬화기를 기본 제공하니 그것을 쓰면 됩니다.
JavaScript
const text = 'She said "hi"\nthen left.';
const escaped = JSON.stringify(text);
console.log(escaped);
// "She said \"hi\"\nthen left."
문자열에 JSON.stringify를 적용하면 따옴표가 포함된 완전한 리터럴을 얻습니다. 본문만 원하나요? 첫 글자와 마지막 글자를 잘라 내세요: JSON.stringify(text).slice(1, -1).
Python
import json
text = 'She said "hi"\nthen left.'
print(json.dumps(text))
# "She said \"hi\"\nthen left."
print(json.dumps(text, ensure_ascii=False))
# "She said \"hi\"\nthen left." (non-ASCII kept as UTF-8)
json.dumps는 기본값이 ensure_ascii=True이며, 이는 모든 비-ASCII 문자를 \uXXXX로 이스케이프합니다. 도구의 ASCII 안전 모드와 동일한 동작입니다. 원시 UTF-8을 유지하려면 ensure_ascii=False를 넘기세요.
PHP
<?php
$text = "café \"quoted\"\nline";
echo json_encode($text);
// "caf\u00e9 \"quoted\"\nline" (default escapes non-ASCII to \uXXXX)
echo json_encode($text, JSON_UNESCAPED_UNICODE);
// "café \"quoted\"\nline"
json_encode는 기본적으로 비-ASCII 문자와 슬래시를 모두 이스케이프합니다. 악센트를 읽기 좋게 유지하려면 JSON_UNESCAPED_UNICODE를, /를 그대로 두려면 JSON_UNESCAPED_SLASHES를 추가하세요.
Go와 Java
Go에서는 json.Marshal(text)가 이스케이프되고 따옴표로 감싸진 바이트를 반환합니다.
b, _ := json.Marshal(`a "quoted" line`)
// b == `"a \"quoted\" line"`
Java에서는 Jackson의 objectMapper.writeValueAsString(text)나 org.json의 JSONObject.quote(text)가 동일하게 따옴표로 감싼 리터럴을 만듭니다. 어떤 언어든 라이브러리에 맡기세요. 직접 짜다 보면 빠뜨리기 쉬운 예외 상황까지 라이브러리는 이미 처리해 둡니다.
JSON 안에 JSON 넣기 (JSON 중첩)
이것이 사람들이 JSON을 손으로 이스케이프하는 가장 흔한 이유입니다. 웹훅 봉투, 메시지 큐 레코드, 감사 로그는 종종 요청 본문 전체를 문자열 필드로 저장합니다. 그렇게 하려면 안쪽 JSON을 먼저 이스케이프해야 합니다.
작은 객체가 두 겹의 인코딩을 거치는 과정은 이렇습니다.
1. Inner object: {"a":1}
2. Escaped as a string: "{\"a\":1}"
3. Placed in envelope: {"payload": "{\"a\":1}"}
안쪽 객체의 모든 "는 \"가 되었고, 전체가 바깥쪽 따옴표 한 쌍으로 감싸졌습니다. 그 결과는 payload에 할당할 수 있는 하나의 유효한 문자열 값입니다.
더 깊은 중첩의 함정은 백슬래시가 늘어난다는 점입니다. 이미 이스케이프된 문자열을 다시 이스케이프하면 그 백슬래시들도 이스케이프되므로, 한 겹마다 백슬래시는 대략 두 배가 됩니다. \"였던 안쪽 따옴표는 한 겹 바깥에서 \\\"가 되고, 또 한 겹 바깥에서는 \\\\\"가 됩니다. 세 겹 깊이의 JSON 중첩은 정말 읽기 어렵고, 그래서 도구가 도움이 됩니다. 반대 방향으로, 즉 문자열에서 안쪽 객체를 다시 꺼내려면 JSON 언이스케이프 도구에 통과시키세요.
유니코드와 \uXXXX 이스케이프
기본적으로 JSON은 원시 UTF-8을 그대로 받아들입니다. é는 é로, 日은 日로 남으니 문서가 더 읽기 좋습니다. 출력 가능한 유니코드 문자는 굳이 이스케이프할 필요가 없습니다.
그렇다면 ASCII 안전 \uXXXX 출력은 언제 써야 할까요? 다운스트림 시스템이 UTF-8을 믿고 맡길 수 없을 때뿐입니다. 오래된 SOAP나 XML 게이트웨이, 특정 로깅 파이프라인, 이메일 헤더, 혹은 순수 ASCII를 유지해야 하는 소스 파일 같은 경우입니다. ASCII 안전 모드에서는 U+007F 이상의 모든 문자가 \uXXXX 이스케이프가 됩니다. café는 caf\u00e9로 바뀝니다. 더 어수선하지만 바이트 단위로 ASCII이며, 규격을 준수하는 어떤 파서에서도 원본으로 디코딩됩니다.
한 가지 미묘한 점이 있습니다. \uXXXX는 16비트 UTF-16 코드 단위 하나를 인코딩하는데, 기본 다국어 평면 바깥의 문자, 즉 이모지나 희귀 문자는 21비트가 필요합니다. JSON은 이를 서로게이트 쌍으로 처리합니다. 즉 두 개의 \uXXXX 이스케이프를 나란히 붙입니다. 활짝 웃는 얼굴 😀(U+1F600)는 \ud83d\ude00이 됩니다. 대부분의 직렬화기가 이를 알아서 처리하지만, 손으로 짠 이스케이프기는 짝 없는 외톨이 서로게이트를 내보내 문제를 일으키기 쉽습니다.
서로게이트 쌍과 코드 포인트가 낯설다면, UTF-8 vs UTF-16 vs Unicode 인코딩 가이드가 하나의 문자가 바이트와 코드 단위로 정확히 어떻게 매핑되는지 설명합니다. 이모지 하나에 왜 이스케이프가 둘 필요한지도 거기서 확인할 수 있습니다.
언이스케이프: 이스케이프된 JSON을 다시 읽기
이스케이프에는 역연산이 있습니다. "a\tb"를 실제 두 줄짜리 혹은 탭이 들어간 텍스트로 되돌리려면 파싱하면 됩니다. 자바스크립트에서는 JSON.parse(str), 파이썬에서는 json.loads(str)입니다. 파서는 각 이스케이프 시퀀스를 따라가며 서로게이트 쌍을 포함한 원래 문자들을 재구성합니다.
언이스케이프가 실패하면 오류는 거의 항상 “유효하지 않은 이스케이프 시퀀스”이며, 흔한 원인이 몇 가지 있습니다.
- JSON이 이스케이프로 인식하지 못하는 문자 앞에 붙은 외톨이 백슬래시, 예를 들어
\q. \x41같은 지어낸 이스케이프. JSON에는\x16진 이스케이프가 없으며 오직\u만 씁니다.\u00처럼 16진 숫자가 네 자리보다 적은 잘린\u이스케이프.- 문자열 경계를 깨뜨리는 떠돌거나 짝이 안 맞는 큰따옴표.
모든 백슬래시가 유효한 이스케이프(\n \r \t \b \f \" \\ \/ \uXXXX) 중 하나를 시작하는지, 그리고 따옴표가 짝을 이루는지 확인하세요. 로그 줄 한가운데에서 복사해 온, 바깥쪽 따옴표가 떨어져 나간 이스케이프 문자열의 경우, JSON 언이스케이프 도구는 감싸는 따옴표가 있든 없든 본문을 받아 어느 쪽이든 디코딩합니다.
흔한 JSON 이스케이프 함정
망가진 페이로드는 대부분 다음 여섯 가지 실수 중 하나로 거슬러 올라갑니다.
1. 이중 이스케이프. 이미 이스케이프된 텍스트를 또 이스케이프하면 \n은 \\n이 되고 \"는 \\\"가 되어, 소비자는 줄바꿈 대신 문자 그대로의 백슬래시-n을 읽습니다. 보통 상위 서비스가 이미 값을 JSON 이스케이프했는데 여러분이 또 이스케이프할 때 발생합니다. 먼저 언이스케이프해 현재 상태를 확인한 뒤, 정확히 한 번만 이스케이프하세요.
2. 바깥쪽 따옴표를 빠뜨리기. 감싸기를 끄면 이스케이프된 본문만 얻을 뿐 완전한 문자열은 아닙니다. JSON 값이 와야 할 자리에 hello \"world\"를 그대로 붙여 넣으면 감싸는 따옴표가 없어 유효하지 않습니다. 감싸기를 켜 두든지 따옴표를 직접 입력하세요.
3. 비-ASCII 과잉 이스케이프. 소비자가 UTF-8을 잘 처리하는데도 ASCII 안전 모드를 켜면 출력만 부풀어 오릅니다. café가 아무 이유 없이 caf\u00e9가 됩니다. 읽기 더 어렵고, 전송량은 커지고, 이득은 전혀 없습니다. 특정 레거시 시스템이 순수 ASCII를 요구하지 않는 한 꺼 두세요.
4. 반사적으로 슬래시를 이스케이프하기. / 이스케이프가 의미를 가지는 곳은 정확히 한 군데, HTML <script> 태그 안에 인라인된 JSON입니다. 여기서는 JSON 맥락과 무관하게 부분 문자열 </script>가 태그를 일찍 닫아 버립니다. /를 \/로 이스케이프하면 이를 무력화합니다. 그 한 경우를 벗어나면 슬래시 이스케이프는 순전히 잡음일 뿐이니, REST 본문, 설정 파일, 메시지 페이로드에서는 꺼 두세요.
5. 손으로 엮은 replace 체인. 수동 replace('"', '\\"') 파이프라인은 거의 항상 무언가를 빠뜨립니다. 제어 문자 하나, 백스페이스 하나, 서로게이트 쌍 하나를요. 명세 전체를 아우르는 언어의 직렬화기를 사용하세요.
6. 이스케이프만 하고 언이스케이프는 안 하기(혹은 두 번 언이스케이프하기). 왕복은 균형이 맞아야 합니다. 들어올 때 한 번 이스케이프하고, 나갈 때 한 번 언이스케이프하세요. 두 번 언이스케이프하면 데이터의 일부였던 진짜 백슬래시를 망가뜨립니다.
확실히 못 박아 둘 구분이 하나 더 있습니다. JSON 이스케이프는 URL 인코딩이나 퍼센트 인코딩이 아닙니다. 둘은 서로 다른 전송 경로의 서로 다른 문제를 풉니다. 이 둘을 섞으면, 즉 값을 퍼센트 인코딩한 다음 그 결과를 JSON 이스케이프하거나 그 반대로 하면, 어느 파서도 깔끔하게 읽을 수 없는 엉망진창이 됩니다. URL 인코딩/디코딩 가이드는 퍼센트 인코딩이 언제 알맞은 도구인지, 그리고 그것이 JSON이 하는 일과 어떻게 다른지 다룹니다.
자주 묻는 질문
JSON에서 문자열을 이스케이프한다는 것은 무슨 뜻인가요?
JSON에 구조적 의미를 가지는 문자, 즉 큰따옴표, 백슬래시, 그리고 줄바꿈이나 탭 같은 제어 문자를 \", \\, \n 같은 안전한 이스케이프 시퀀스로 치환한다는 뜻입니다. 그 결과는 파싱을 깨뜨리지 않고 JSON 문서 안에 문자열 리터럴로 들어갈 수 있습니다.
JSON에서 어떤 문자를 이스케이프해야 하나요?
큰따옴표, 백슬래시, 줄바꿈, 캐리지 리턴, 탭, 백스페이스, 폼 피드는 각각 전용 이스케이프를 가지며, U+0020 미만의 다른 모든 제어 문자는 \uXXXX가 됩니다. 출력 가능한 문자와 멀티바이트 UTF-8은 이스케이프가 필요 없고, 슬래시는 선택 사항이며 HTML <script> 태그 안에서만 의미가 있습니다.
JSON 이스케이프는 JSON.stringify와 같은 건가요?
대체로 하나의 작업을 보는 두 관점입니다. 문자열에 적용한 JSON.stringify는 그것을 큰따옴표로 감싸고 안쪽의 특수 문자를 이스케이프합니다. 그것이 JSON 이스케이프입니다. 감싸기 켜기는 따옴표가 있는 형태(JSON.stringify와 동일)이고, 감싸기 끄기는 감싸는 따옴표 없이 이스케이프된 본문만 줍니다.
JavaScript나 Python에서 JSON용으로 문자열을 어떻게 이스케이프하나요?
JavaScript에서는 JSON.stringify(str)을, Python에서는 json.dumps(str)을 사용하세요. 손으로 짠 replace 체인 대신 항상 내장 함수에 기대세요. 내장 함수는 유니코드, 제어 문자, 그리고 여러분이 놓칠 모든 예외 상황을 올바르게 처리합니다.
제 JSON이 백슬래시가 더 늘어나며 망가지는 이유는 무엇인가요?
흔한 원인은 이중 이스케이프입니다. 이미 이스케이프된 텍스트를 또 이스케이프해 \n이 \\n이 되고, 소비자가 줄바꿈 대신 문자 그대로의 백슬래시-n을 읽는 것이죠. 먼저 값을 언이스케이프해 실제 상태를 확인한 뒤, 정확히 한 번만 이스케이프하세요.
JSON에서 슬래시나 유니코드를 이스케이프해야 하나요?
아닙니다. 둘 다 필수가 아닙니다. /는 평범한 문자이며, JSON을 HTML <script> 태그 안에 인라인할 때 </script> 시퀀스가 태그를 일찍 닫지 못하게 막는 경우에만 이스케이프가 필요합니다. 유니코드는 기본적으로 원시 UTF-8로 남고, \uXXXX는 다운스트림 시스템이 UTF-8을 처리하지 못할 때만 사용하세요.