빠른 흐름
const text = '{"name":"Kim","score":42}';
const data = JSON.parse(text);
const pretty = JSON.stringify(data, null, 2);기본 흐름
JSON.parse()는 실패 시 예외를 던진다 — try/catch 없이 신뢰할 수 없는 입력을 파싱하면 런타임이 중단된다
JSON.parse()는 유효하지 않은 JSON 문자열을 받으면 SyntaxError를 던집니다. 네트워크 응답, 사용자 입력, 파일에서 읽은 문자열처럼 제어하지 못하는 출처의 JSON을 파싱할 때는 반드시 try/catch로 감싸야 합니다. 파싱에 성공했다고 해서 값의 타입이 기대한 것과 일치한다는 보장도 없습니다. 파싱 후 값 타입을 검증하거나 JSON Schema 검증기를 함께 쓰는 것이 안전한 방어 전략입니다.
try {
const data = JSON.parse(text);
// 여기서 data의 타입을 검증해야 함
} catch (e) {
console.error("파싱 실패:", e.message);
}JSON.stringify()는 모든 JavaScript 값을 보존하지 않는다 — 손실이 조용히 일어난다
JSON.stringify()가 처리하는 방식은 값 타입마다 다릅니다. undefined, 함수, Symbol은 object 멤버에서 제거되고, array 안에 있으면 null로 대체됩니다. NaN과 Infinity는 null로 직렬화됩니다. BigInt는 TypeError를 던집니다. Date 객체는 .toISOString() 결과 문자열로 변환되어, 다시 파싱하면 Date가 아닌 string이 됩니다. 이 모든 변환은 예외 없이 조용히 일어나므로, 직렬화 전후 값 비교가 필요한 경우 각 타입의 처리 방식을 미리 파악해야 합니다.
replacer와 reviver로 직렬화 규칙을 대칭으로 정의한다 — Date나 BigInt 같은 타입을 보존할 때 필요하다
JSON.stringify()의 두 번째 인수 replacer는 직렬화 과정에서 각 키/값 쌍을 변환할 수 있습니다. 배열로 전달하면 포함할 키를 화이트리스트로 지정하고, 함수로 전달하면 값별 변환 로직을 적용합니다. JSON.parse()의 두 번째 인수 reviver는 파싱 후 값을 변환하며, depth-first로 leaf부터 root 방향으로 적용됩니다. Date를 ISO string으로 직렬화하고 파싱 시 다시 Date로 복원하는 패턴이 대표적인 사용 사례입니다.
const text = JSON.stringify(data, (key, value) => {
if (value instanceof Date) return { __type: "Date", iso: value.toISOString() };
return value;
});
const parsed = JSON.parse(text, (key, value) => {
if (value?.__type === "Date") return new Date(value.iso);
return value;
});BigInt는 기본 직렬화가 불가능하다 — 큰 정수는 string으로 전송하는 것이 가장 안전하다
JSON.stringify(BigInt(1)) 은 TypeError: Do not know how to serialize a BigInt를 던집니다. reviver의 context.source를 사용하면 파싱 단계에서 정밀도 손실 없이 원본 숫자 문자열을 읽을 수 있지만, 이는 비교적 최신 API입니다. 가장 호환성 있는 방법은 큰 정수를 처음부터 string으로 설계하는 것입니다. Twitter가 트윗 ID를 숫자와 string 두 필드로 모두 제공했던 것이 대표 사례입니다.
const text = '{"gross_gdp":12345678901234567890}';
const parsed = JSON.parse(text, (key, value, context) => {
if (key === "gross_gdp") return BigInt(context.source);
return value;
});parse와 stringify는 방향이 반대인 만큼 실패 지점도 다르다
JSON.parse()는 문자열이 문법적으로 맞는지와 타입이 기대와 맞는지가 핵심이고, JSON.stringify()는 어떤 값이 조용히 사라지거나 바뀌는지가 핵심입니다. 둘을 같은 "JSON 변환"으로 뭉뚱그리면 오류 지점을 잘못 찾기 쉽습니다.
또 하나 자주 생기는 실수는 "이미 object인 값"에 다시 JSON.parse()를 호출하거나, "이미 문자열인 JSON"에 JSON.stringify()를 한 번 더 호출하는 것입니다. parse는 문자열을 값으로 바꾸고, stringify는 값을 문자열로 바꾼다는 방향을 항상 분리해서 생각해야 합니다.
const text = '{"name":"Kim","score":42}';
const data = JSON.parse(text);
const pretty = JSON.stringify(data, null, 2);체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 신뢰할 수 없는 JSON 파싱 | try/catch + 파싱 후 타입 검증 |
Date 타입 왕복 직렬화 | replacer/reviver로 변환 규칙 정의 |
BigInt 포함 값 전달 | 처음부터 string 타입으로 설계 |
| 특정 필드만 직렬화 | replacer 배열로 화이트리스트 지정 |
| 이미 JSON 문자열인 값을 다시 다룰 때 | parse/stringify 방향을 먼저 확인 |
주의할 점
JSON.stringify() 결과가 "모든 값이 그대로 보존된 문자열"이라고 가정하면 위험합니다.
undefined, 함수, NaN, Infinity는 조용히 null로 바뀌거나 제거되고, BigInt는
예외를 던집니다. 직렬화 전후 타입 보존이 필요한 데이터는 replacer/reviver로 변환 규칙을
명시적으로 정의해야 합니다.
JSON.stringify({
ok: true,
missing: undefined,
limit: Infinity,
});이 결과는 missing을 제거하고 Infinity를 null로 바꿉니다. 직렬화가 성공했다고 해서 값이 그대로 보존됐다고 보면 안 됩니다.
const text = '{"name":"Kim"}';
JSON.stringify(text);이 결과는 JSON object가 아니라 "\"{\\\"name\\\":\\\"Kim\\\"}\"" 같은 "문자열의 문자열"입니다. parse와 stringify를 반대로 적용하면 데이터 구조가 한 단계 더 감싸져 디버깅이 어려워집니다.
참고 링크
2 sources