JSON.parse(JSON.stringify(obj))로 깊은 복사를 하다가 Date가 문자열이 된 이유
문제 발생
중첩 객체를 깊은 복사하려고 관습적으로 쓰던 이 한 줄이 문제를 만들었습니다.
const copy = JSON.parse(JSON.stringify(state));
copy.createdAt.getFullYear(); // TypeError: copy.createdAt.getFullYear is not a functionDate가 문자열이 됐고, Map은 {}가 됐으며, undefined인 필드는 아예 사라졌습니다. 순환 참조가 있는 객체에서는 TypeError: Converting circular structure to JSON으로 죽었습니다.
원인 분석
JSON.stringify는 JSON이라는 형식으로 직렬화합니다. JSON에는 날짜, 맵, 집합, undefined, BigInt, 순환 참조라는 개념이 없습니다. 그러니 깊은 복사 도구가 아니라 "JSON으로 표현 가능한 부분만 남기는 필터"에 가깝습니다.
| 값 | JSON 왕복 결과 |
|---|---|
Date | ISO 문자열 |
Map, Set | {} |
undefined 필드 | 사라짐 |
NaN, Infinity | null |
BigInt | TypeError |
| 순환 참조 | TypeError |
structuredClone()은 구조적 복제 알고리즘으로 깊은 복사를 수행합니다. 2022년 3월부터 Baseline이고, 위 표의 항목을 모두 제대로 처리합니다 — Date는 Date로, Map은 Map으로, 순환 참조는 순환 참조로 복사됩니다.
해결 방안
- 깊은 복사에는
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 — 순환도 유지된다- 복사되지 않는 것을 압니다. 이건 버그가 아니라 알고리즘의 범위입니다.
- 함수와 DOM 노드 —
DataCloneError예외 - 프로토타입 — 클래스 인스턴스를 복사하면 평범한 객체가 됩니다. 메서드가 사라지므로 도메인 객체를 복사할 때 조심해야 합니다.
- getter/setter와 속성 서술자 — 값만 복사되고 접근자는 사라집니다.
- 함수와 DOM 노드 —
- 직렬화 불가 값이 섞일 수 있으면 감쌉니다. 어디서 실패했는지 메시지에 담아 두면 디버깅이 쉬워집니다.
try {
return structuredClone(value);
} catch (error) {
if (error.name === "DataCloneError") throw new Error("복사할 수 없는 값이 포함돼 있습니다", { cause: error });
throw error;
}- 얕은 복사로 충분한지 먼저 확인합니다. 상태 갱신에서 필요한 것은 대개 바뀌는 경로만 새로 만드는 것입니다.
{ ...state, user: { ...state.user, name } }이면 되는데 전체를 깊은 복사하는 것은 낭비입니다. - 큰 버퍼는 복사 대신 이전할 수 있습니다.
transfer옵션을 쓰면ArrayBuffer를 복사하지 않고 소유권만 넘깁니다 — 원본은 사용할 수 없게 됩니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.