자바스크립트 중급 #7 JSON 다루기와 직렬화

자바스크립트 중급 시리즈의 마지막 글입니다. 데이터를 외부와 주고받을 때 거의 항상 거치는 형식 — JSON을 정리합니다.

JSON이 무엇인가? #

**JSON (JavaScript Object Notation)**은 자바스크립트 객체 문법에서 영감을 얻은 데이터 교환 형식입니다. 자바스크립트뿐 아니라 거의 모든 언어가 표준으로 지원합니다.

JSON 예시
{
  "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 — 문자열 → 객체 #

parse 기본
const text = '{"id":"u1","name":"커티스"}';
const obj = JSON.parse(text);

obj.id;     // 'u1'
obj.name;   // '커티스'

문자열을 자바스크립트 객체로 만듭니다. 형식이 잘못되면 throw 합니다.

잘못된 JSON
JSON.parse('{name: "커티스"}');   // ✗ SyntaxError (키에 따옴표 없음)
JSON.parse("{'a': 1}");           // ✗ SyntaxError (작은따옴표)
JSON.parse('{"a": 1,}');          // ✗ SyntaxError (트레일링 콤마)

외부 데이터를 parse 할 때는 보통 try/catch로 감싸요.

안전한 parse
function safeParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

JSON.parse의 reviver — 변환 함수 #

두 번째 인자로 각 키-값을 변환하는 함수를 줄 수 있습니다.

reviver — Date 복원
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 — 객체 → 문자열 #

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가 만나면 조용히 사라지거나 변형 됩니다.

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 #

BigInt
JSON.stringify({ count: 100n });   // ✗ TypeError

BigInt는 stringify가 처리할 줄 모르고 throw 합니다. 직접 변환해야 합니다.

BigInt 직렬화
const obj = { count: 100n };

JSON.stringify(obj, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'

toJSON 메서드 — 클래스가 자기를 직렬화 #

객체에 toJSON 메서드가 있으면 stringify가 그 메서드의 반환값을 대신 직렬화 합니다.

toJSON 으로 커스텀
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가 정확히 이 패턴을 사용합니다.

Date 의 toJSON
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로 직접 처리.

WeakSet으로 방문 추적
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이 표준화됐습니다.

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에 안전하게 보관할 때.

object → base64
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편에서 다룬 내용:

  1. 클래스#/static/get/set (#1)
  2. 비동기 — Promise, async/await, Promise.all (#2)
  3. 이터레이터/제너레이터Symbol.iterator, function*, yield (#3)
  4. 디스트럭처링/spread/rest 깊이 — 매개변수 패턴, immutable update (#4)
  5. ?.?? — 안전 접근과 nullish 기본값 (#5)
  6. fetch API — 표준 네트워크 도구, AbortController (#6)
  7. JSON 다루기 — parse/stringify, structuredClone (이번 글)

여기까지 익히면 모던 자바스크립트의 일상적인 표현은 거의 다 닿을 수 있습니다. 다음 고급 시리즈에서는 한 단계 더 깊이 — 클로저, this, 프로토타입, 이벤트 루프, 메모리 모델까지 자바스크립트 엔진의 동작 원리를 다룹니다.

X