자바스크립트 중급 #7 JSON 다루기와 직렬화
자바스크립트 중급 시리즈의 마지막 글입니다. 데이터를 외부와 주고받을 때 거의 항상 거치는 형식 — JSON을 정리합니다.
JSON이 무엇인가? #
**JSON (JavaScript Object Notation)**은 자바스크립트 객체 문법에서 영감을 얻은 데이터 교환 형식입니다. 자바스크립트뿐 아니라 거의 모든 언어가 표준으로 지원합니다.
{
"id": "u1",
"name": "커티스",
"age": 30,
"tags": ["dev", "blog"],
"active": true,
"spouse": null
}자바스크립트 객체와 비슷하지만 차이가 몇 가지 있습니다.
- 키는 반드시 큰따옴표로 감싼 문자열
- 값은 string / number / boolean / null / array / object만
- 함수, undefined, Date, Symbol, BigInt는 허용되지 않음
- 트레일링 콤마 안 됨 —
{"a": 1,}는 invalid
자바스크립트의 모든 값이 JSON으로 표현되는 건 아니라는 게 핵심입니다.
JSON.parse — 문자열 → 객체
#
const text = '{"id":"u1","name":"커티스"}';
const obj = JSON.parse(text);
obj.id; // 'u1'
obj.name; // '커티스'
문자열을 자바스크립트 객체로 만듭니다. 형식이 잘못되면 throw 합니다.
JSON.parse('{name: "커티스"}'); // ✗ SyntaxError (키에 따옴표 없음)
JSON.parse("{'a': 1}"); // ✗ SyntaxError (작은따옴표)
JSON.parse('{"a": 1,}'); // ✗ SyntaxError (트레일링 콤마)
외부 데이터를 parse 할 때는 보통 try/catch로 감싸요.
function safeParse(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}JSON.parse의 reviver — 변환 함수
#
두 번째 인자로 각 키-값을 변환하는 함수를 줄 수 있습니다.
const text = '{"createdAt": "2026-05-04T10:00:00Z", "name": "커티스"}';
const obj = JSON.parse(text, (key, value) => {
if (key === 'createdAt' && typeof value === 'string') {
return new Date(value);
}
return value;
});
obj.createdAt instanceof Date; // true
JSON 에는 Date 타입이 없어서 보통 ISO 문자열로 보냅니다. parse 시점에 reviver로 다시 Date 객체로 복원하는 게 흔한 패턴입니다. 큰 앱에서는 zod 같은 스키마 라이브러리가 같은 일을 더 강력하게 합니다.
JSON.stringify — 객체 → 문자열
#
const obj = { id: 'u1', name: '커티스' };
JSON.stringify(obj);
// '{"id":"u1","name":"커티스"}'
보기 좋은 출력 — 들여쓰기 #
세 번째 인자로 들여쓰기 너비를 줄 수 있습니다. 디버깅이나 파일 저장 시 유용합니다.
JSON.stringify(obj, null, 2);
// {
// "id": "u1",
// "name": "커티스"
// }
여기서 null은 두 번째 인자(replacer, 잠시 뒤). 사용 안 하면 null.
replacer — 필터링/변환 #
두 번째 인자로 포함할 키의 배열 또는 변환 함수를 줄 수 있습니다.
const user = { id: 'u1', name: '커티스', password: 'secret', age: 30 };
JSON.stringify(user, ['id', 'name', 'age']);
// '{"id":"u1","name":"커티스","age":30}'
// password 제외됨
JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // 제외
return value;
});undefined를 반환하면 그 키가 결과에 안 포함됩니다. 비밀번호 같은 민감 정보 마스킹에 유용한 패턴입니다.
사라지는 값들 — stringify의 함정
#
자바스크립트의 일부 값은 JSON으로 표현이 안 됩니다. stringify가 만나면 조용히 사라지거나 변형 됩니다.
JSON.stringify({
a: undefined, // 키 자체가 사라짐
b: () => {}, // 함수도 사라짐
c: Symbol('s'), // 심볼도 사라짐
d: NaN, // null 로 변환
e: Infinity, // null 로 변환
f: -Infinity, // null 로 변환
});
// '{"d":null,"e":null,"f":null}'
a, b, c의 키 자체가 결과에서 빠집니다. 이는 종종 버그의 원인이 됩니다 — “분명 값을 넣었는데 받는 쪽에 안 옵니다” 의 흔한 원인이 이것입니다.
BigInt는 throw #
JSON.stringify({ count: 100n }); // ✗ TypeError
BigInt는 stringify가 처리할 줄 모르고 throw 합니다. 직접 변환해야 합니다.
const obj = { count: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'
toJSON 메서드 — 클래스가 자기를 직렬화
#
객체에 toJSON 메서드가 있으면 stringify가 그 메서드의 반환값을 대신 직렬화 합니다.
class User {
constructor(name, password) {
this.name = name;
this._password = password;
}
toJSON() {
return { name: this.name }; // password 제외
}
}
const u = new User('커티스', 'secret');
JSON.stringify(u);
// '{"name":"커티스"}'
빌트인 Date가 정확히 이 패턴을 사용합니다.
const d = new Date('2026-05-04T10:00:00Z');
d.toJSON(); // '2026-05-04T10:00:00.000Z'
JSON.stringify({ d }); // '{"d":"2026-05-04T10:00:00.000Z"}'
Date가 ISO 문자열로 자동 변환되는 이유가 이것입니다.
순환 참조 — stringify가 throw #
const a = { name: '커티스' };
a.self = a;
JSON.stringify(a); // ✗ TypeError: Converting circular structure to JSON
객체가 자기 자신을 가리키면 stringify가 무한 루프에 빠지지 않으려고 throw 합니다. 큰 그래프(트리에 부모 참조 포함)를 직렬화할 때 자주 마주칩니다.
해결 방법: 순환 참조 끊기 또는 replacer로 직접 처리.
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}structuredClone — JSON 트릭의 모던 대체
#
옛날에는 깊은 복사를 하려고 다음 트릭을 자주 썼습니다.
const copy = JSON.parse(JSON.stringify(original));이게 함수, undefined, Date, Map, Set을 못 다룹니다. Date가 ISO 문자열로 바뀌고, 함수/undefined는 사라집니다.
ES2022부터 #4 에서 본 structuredClone이 표준화됐습니다.
const original = {
name: '커티스',
createdAt: new Date(),
tags: new Set(['a', 'b']),
};
const copy = structuredClone(original);
copy.createdAt instanceof Date; // true
copy.tags instanceof Set; // true
대부분의 자료구조를 정확히 복사해 줍니다. 함수와 클래스 인스턴스 prototype은 못 옮긴다는 한계는 있지만, 일반 객체/배열/Date/Map/Set은 모두 OK. 새 코드에서는 structuredClone이 깊은 복사의 정답 입니다.
자주 쓰는 미니 레시피 #
1) 안전한 깊은 복사 + 폴백 #
function deepCopy(value) {
if (typeof structuredClone === 'function') {
return structuredClone(value);
}
return JSON.parse(JSON.stringify(value));
}2) URL safe encode/decode #
btoa / atob는 base64 인코딩 도우미. 작은 객체를 URL/storage에 안전하게 보관할 때.
const data = { id: 'u1', name: '커티스' };
const encoded = btoa(JSON.stringify(data));
// "eyJpZCI6InUxIiwibmFtZSI6IuyZv+u3sCJ9"
const decoded = JSON.parse(atob(encoded));
// { id: 'u1', name: '커티스' }
3) JSON 검증 후 파싱 #
큰 앱에서는 zod 같은 스키마 라이브러리가 표준이지만, 가벼운 경우에는 직접 검사가 더 짧습니다.
function parseUser(text) {
const obj = JSON.parse(text);
if (typeof obj?.id !== 'string') throw new Error('id 누락');
if (typeof obj?.name !== 'string') throw new Error('name 누락');
return obj;
}타입스크립트 + React 실전 #6 에서 zod로 같은 일을 더 강력하게 푸는 패턴을 다뤘습니다.
마무리 #
이번 글에서 정리한 내용:
- JSON은 string/number/boolean/null/array/object만, 큰따옴표 키
JSON.parse의 reviver로 Date 같은 타입 복원JSON.stringify세 번째 인자로 보기 좋은 들여쓰기- replacer로 키 필터링 / 변환
undefined/함수/Symbol은 stringify에서 사라짐- BigInt는 throw — 직접 처리
toJSON메서드가 있으면 stringify가 그 결과를 사용 (Date 동작 원리)- 순환 참조는 throw — WeakSet으로 추적
structuredClone이 깊은 복사의 모던 정답
중급 시리즈를 마치며 #
7편에서 다룬 내용:
- 클래스 —
#/static/get/set(#1) - 비동기 — Promise, async/await, Promise.all (#2)
- 이터레이터/제너레이터 —
Symbol.iterator,function*,yield(#3) - 디스트럭처링/spread/rest 깊이 — 매개변수 패턴, immutable update (#4)
?.와??— 안전 접근과 nullish 기본값 (#5)- fetch API — 표준 네트워크 도구, AbortController (#6)
- JSON 다루기 — parse/stringify, structuredClone (이번 글)
여기까지 익히면 모던 자바스크립트의 일상적인 표현은 거의 다 닿을 수 있습니다. 다음 고급 시리즈에서는 한 단계 더 깊이 — 클로저, this, 프로토타입, 이벤트 루프, 메모리 모델까지 자바스크립트 엔진의 동작 원리를 다룹니다.