Quick Flow
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이 됩니다. 이 모든 변환은 예외 없이 조용히 일어나므로, 직렬화 전후 값 비교가 필요한 경우 각 타입의 처리 방식을 미리 파악해야 합니다.
Date는 ISO string 계약을 정하고 reviver에서 선택적으로 복원한다
Date는 기본 toJSON()이 먼저 ISO 8601 string을 만들기 때문에 JSON.stringify()의 replacer가 받는 값은 보통 Date가 아니라 이미 string입니다. 따라서 replacer에서 value instanceof Date로 태그를 붙이는 예제는 기대대로 동작하지 않습니다. API에서 날짜를 ISO string으로 보낸다는 계약을 먼저 정하고, 필요한 필드만 reviver에서 Date로 복원합니다.
const source = { createdAt: new Date("2026-08-10T09:00:00Z") };
const text = JSON.stringify(source);
const parsed = JSON.parse(text, (key, value) => {
if (key === "createdAt" && typeof value === "string") {
const date = new Date(value);
if (!Number.isNaN(date.valueOf())) return date;
}
return value;
});new Date(value)가 유효한 날짜인지 확인하고, 모든 ISO처럼 보이는 string을 무조건 Date로 바꾸지 않습니다. 날짜 필드 이름과 시간대 규칙은 API 계약으로 정합니다. replacer는 BigInt를 string으로 바꾸거나 특정 키를 제외하는 등, toJSON()과 충돌하지 않는 변환에 씁니다.
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" && context?.source) return BigInt(context.source);
return value;
});이 예제의 세 번째 context 인수는 최신 구현에서만 제공됩니다. 지원하지 않는 환경에서 큰 정수를 받는 계약이라면 복원 시도를 하지 말고 처음부터 JSON string으로 받아야 합니다.
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