본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev16

JSON.parse(JSON.stringify(obj))로 깊은 복사를 하다가 Date가 문자열이 된 이유

  • #Engineering Note
  • #JavaScript

문제 발생

중첩 객체를 깊은 복사하려고 관습적으로 쓰던 이 한 줄이 문제를 만들었습니다.

const copy = JSON.parse(JSON.stringify(state));
copy.createdAt.getFullYear();   // TypeError: copy.createdAt.getFullYear is not a function

Date가 문자열이 됐고, Map{}가 됐으며, undefined인 필드는 아예 사라졌습니다. 순환 참조가 있는 객체에서는 TypeError: Converting circular structure to JSON으로 죽었습니다.

원인 분석

JSON.stringifyJSON이라는 형식으로 직렬화합니다. JSON에는 날짜, 맵, 집합, undefined, BigInt, 순환 참조라는 개념이 없습니다. 그러니 깊은 복사 도구가 아니라 "JSON으로 표현 가능한 부분만 남기는 필터"에 가깝습니다.

JSON 왕복 결과
DateISO 문자열
Map, Set{}
undefined 필드사라짐
NaN, Infinitynull
BigIntTypeError
순환 참조TypeError

structuredClone()은 구조적 복제 알고리즘으로 깊은 복사를 수행합니다. 2022년 3월부터 Baseline이고, 위 표의 항목을 모두 제대로 처리합니다 — DateDate로, MapMap으로, 순환 참조는 순환 참조로 복사됩니다.

해결 방안

  1. 깊은 복사에는 structuredClone을 씁니다.
const original = { name: "MDN", at: new Date(), tags: new Set(["a"]) };
original.itself = original;                 // 순환 참조

const copy = structuredClone(original);
copy.at instanceof Date;      // true
copy.tags instanceof Set;     // true
copy.itself === copy;         // true — 순환도 유지된다
  1. 복사되지 않는 것을 압니다. 이건 버그가 아니라 알고리즘의 범위입니다.
    • 함수DOM 노드DataCloneError 예외
    • 프로토타입 — 클래스 인스턴스를 복사하면 평범한 객체가 됩니다. 메서드가 사라지므로 도메인 객체를 복사할 때 조심해야 합니다.
    • getter/setter와 속성 서술자 — 값만 복사되고 접근자는 사라집니다.
  2. 직렬화 불가 값이 섞일 수 있으면 감쌉니다. 어디서 실패했는지 메시지에 담아 두면 디버깅이 쉬워집니다.
try {
  return structuredClone(value);
} catch (error) {
  if (error.name === "DataCloneError") throw new Error("복사할 수 없는 값이 포함돼 있습니다", { cause: error });
  throw error;
}
  1. 얕은 복사로 충분한지 먼저 확인합니다. 상태 갱신에서 필요한 것은 대개 바뀌는 경로만 새로 만드는 것입니다. { ...state, user: { ...state.user, name } }이면 되는데 전체를 깊은 복사하는 것은 낭비입니다.
  2. 큰 버퍼는 복사 대신 이전할 수 있습니다. transfer 옵션을 쓰면 ArrayBuffer를 복사하지 않고 소유권만 넘깁니다 — 원본은 사용할 수 없게 됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.