JavaScript · 5장 자료구조와 자료형
JSON: 서버와 주고받는 데이터와 직렬화의 함정
API 응답, 저장해 둔 장바구니, 설정 파일은 대부분 JSON이에요. 자바스크립트 객체와 생김새가 비슷해서 쉽게 보이지만, 객체를 JSON 글자로 바꾸는 순간 사라지거나 모양이 바뀌는 값이 있어요. 그 차이를 모르면 "저장했다 불러왔더니 날짜가 고장 났어요" 같은 버그를 만나요.
01. JSON.stringify와 JSON.parse
JSON은 데이터를 글자로 적는 형식이에요. 자바스크립트 객체 문법에서 빌려 왔지만 규칙이 더 엄격해요. 키와 문자열은 반드시 큰따옴표, 끝의 쉼표와 주석은 금지, 값은 문자열 · 숫자 · 불리언 · null · 배열 · 객체만 돼요. 객체를 JSON 글자로 바꾸는 게 JSON.stringify(직렬화), 글자를 다시 값으로 바꾸는 게 JSON.parse예요.
const user = { name: '하늘', age: 27, tags: ['js', 'css'], admin: false };
const text = JSON.stringify(user);
console.log(text, typeof text); // {"name":"하늘",…} string — 이제 글자예요
console.log(JSON.stringify(user, null, 2)); // 사람이 읽기 좋게 들여쓰기
const back = JSON.parse(text);
console.log(back.tags[1], back === user); // css false — 내용은 같지만 새 객체예요
서버로 보낼 때(fetch의 body), localStorage에 저장할 때처럼 글자만 오갈 수 있는 곳을 지날 때마다 이 두 함수를 거쳐요. fetch의 response.json()은 받은 글자에 JSON.parse를 대신 해 주는 함수예요.
02. 글자로 바뀌면서 사라지고 바뀌는 값
JSON에 없는 값은 stringify가 조용히 바꾸거나 빼요. 오류가 나지 않아서 더 위험해요.
const odd = {
memo: undefined, // 속성째 빠져요
save() {}, // 함수도 빠져요
score: NaN, // null이 돼요
at: new Date('2026-11-07T09:30:00Z'), // ISO 문자열이 돼요
tags: new Set(['js']), // {}가 돼요
list: [undefined, () => 1], // 배열 안에서는 null이 돼요
};
console.log(JSON.stringify(odd));
console.log(JSON.stringify({ id: 10n })); // BigInt는?
특히 Date는 문자열로 나가서 문자열로 돌아와요. parse는 그 글자가 날짜였는지 모르니까요. 그 밖에 서로를 가리키는 객체(순환 참조)와 BigInt는 TypeError가 나요. 큰 ID는 서버에서 문자열로 주고받는 이유가 여기에도 있어요(BigInt).
03. 받은 JSON을 안전하게 읽기
JSON.parse는 규칙에 맞지 않는 글자를 받으면 SyntaxError를 던져요. 서버가 오류 페이지(HTML)를 돌려주거나, 저장해 둔 값이 없어서 빈 문자열 · null을 읽을 때 흔히 생겨요. 밖에서 들어온 글자를 읽을 때는 try...catch로 감싸고 기본값을 정해요(오류 처리는 10장에서 자세히).
function readJson(text, fallback) {
try {
return JSON.parse(text);
} catch {
return fallback; // 망가진 글자면 기본값
}
}
console.log(readJson('{"theme":"dark"}', {})); // { theme: 'dark' }
console.log(readJson("{'theme': 'dark'}", {})); // {} — 작은따옴표는 JSON이 아니에요
console.log(readJson('<html>…', {})); // {} — 서버가 오류 페이지를 줬을 때
console.log(JSON.parse('{"a": 1,}')); // 끝 쉼표는?
날짜처럼 글자로 돌아온 값을 다시 원래 종류로 되살리려면 JSON.parse의 두 번째 인자 reviver를 써요. 키와 값마다 불리는 함수인데, 돌려주는 값이 그 자리의 새 값이 돼요. 반대로 stringify의 두 번째 인자에 키 배열을 주면 그 키만 골라 보내요.
const text = '{"title":"첫 글","createdAt":"2026-11-07T09:30:00.000Z"}';
const post = JSON.parse(text, (key, value) =>
key === 'createdAt' ? new Date(value) : value
);
console.log(post.createdAt instanceof Date, post.createdAt.getTime()); // true 1794043800000
console.log(JSON.stringify({ price: 5900, secret: 'x' }, ['price'])); // {"price":5900}
04. JSON으로 복사하기의 한계
예전에는 깊은 복사를 JSON.parse(JSON.stringify(obj))로 하곤 했어요. 지금 보면 위의 함정을 그대로 다 가져와요. Date는 문자열이 되고, undefined 속성은 사라지고, Map · Set은 빈 객체가 돼요. 깊은 복사는 structuredClone을 써요(깊은 복사).
const src = { at: new Date('2026-11-07T09:30:00Z'), memo: undefined, tags: new Set(['a']) };
const viaJson = JSON.parse(JSON.stringify(src));
console.log(viaJson, typeof viaJson.at); // { at: '2026-…', tags: {} } string
const viaClone = structuredClone(src);
console.log(viaClone.at instanceof Date, 'memo' in viaClone, viaClone.tags instanceof Set); // true true true
장바구니를 글자로 저장했다가 다시 불러오는 코드예요(브라우저라면 localStorage에 넣었다 꺼내는 자리예요). 불러온 뒤 updatedAt.getTime()에서 오류가 나요. JSON.parse 부분만 고쳐서 1794043800000이 나오게 해 보세요.
const cart = {
items: [{ name: '연필', qty: 2 }],
updatedAt: new Date('2026-11-07T09:30:00Z'),
};
const saved = JSON.stringify(cart); // localStorage.setItem('cart', …) 자리
const loaded = JSON.parse(saved); // localStorage.getItem('cart') 자리
console.log(loaded.updatedAt.getTime());
해답 보기
Date는 저장될 때 ISO 문자열이 되고, JSON.parse는 그걸 문자열로 돌려줘요. 문자열에는 getTime이 없어서 오류가 났어요. reviver로 updatedAt 키만 Date로 되살려요.
const loaded = JSON.parse(saved, (key, value) =>
key === 'updatedAt' ? new Date(value) : value
);
// 1794043800000
05. 실전 포인트
- 언제 쓰나요? 서버로 보낼 때와
localStorage·sessionStorage에 넣을 때JSON.stringify, 꺼낼 때try로 감싼JSON.parse를 써요. 디버깅 로그는JSON.stringify(값, null, 2)로 보기 좋게 찍어요. - 대신 쓰는 것: 깊은 복사는
structuredClone, 날짜는 ISO 문자열로 보내고 받는 쪽에서 되살려요. Map은Object.fromEntries(fromEntries), Set은[...set]으로 바꿔서 보내요. - 흔한 실수: 불러온 날짜를 Date로 생각하고 메서드를 부르는 것,
undefined로 지우려던 값이 "안 보냄"이 되는 것(지우려면null을 보내요), 오류 페이지를parse하다 화면이 멈추는 것, JSON 복사로 Date · Map을 잃는 것이에요.
정리
- JSON은 큰따옴표 · 끝 쉼표 금지 같은 엄격한 규칙을 가진 글자 형식이에요.
stringify에서undefined· 함수는 빠지고, NaN은null, Date는 문자열, Map · Set은{}가 돼요.parse는 잘못된 글자에SyntaxError를 던져요. 밖에서 온 글자는try로 감싸요.- reviver로 날짜를 되살리고, 깊은 복사는
structuredClone을 써요.
JSON.stringify({ a: undefined, b: null })의 결과는?
'{"b":null}'이에요. undefined인 속성은 통째로 빠지고 null은 그대로 남아요. 서버에 "이 값을 지워 달라"고 보내려면 null을 써요.
저장했다 불러온 객체의 createdAt.getFullYear()가 오류인 이유는?
Date는 JSON으로 바뀔 때 ISO 문자열이 되고, JSON.parse는 문자열 그대로 돌려줘요. 문자열에는 Date 메서드가 없어요. reviver나 new Date(값)으로 되살려요.
깊은 복사에 JSON.parse(JSON.stringify(obj)) 대신 structuredClone을 쓰는 이유는?
JSON을 거치면 Date가 문자열이 되고, undefined 속성이 사라지고, Map · Set이 빈 객체가 되기 때문이에요. structuredClone은 이런 값을 원래 종류 그대로 복사해요.