상태를 직접 건드리면 안 되니까 객체를 복사해서 쓰는 일이 잦다. 근데 스프레드(...)는 얕은 복사라서 중첩된 객체는 여전히 같은 참조를 가리킨다. 안쪽 값을 바꾸면 원본까지 같이 바뀐다.
const state = { user: { name: '나나', tags: ['a', 'b'] } };
const copy = { ...state };
copy.user.name = '바뀜';
console.log(state.user.name); // '바뀜' — 원본이 오염됐다
그래서 예전엔 다들 이걸 썼다.
const deep = JSON.parse(JSON.stringify(state));
동작은 하는데 함정이 많다. Date는 문자열로 변해버리고, undefined와 함수는 통째로 사라지고, Map·Set은 빈 객체 {}가 된다. NaN은 null이 되고, 순환 참조가 있으면 그냥 에러를 던진다. "잘 되는 것 같은데 특정 값만 이상하게 깨진다"는 버그가 여기서 나온다.
브라우저에 이미 답이 있다
structuredClone()은 이런 케이스를 알아서 처리해주는 내장 함수다. 별도 설치도 import도 필요 없다.
const state = {
user: { name: '나나', tags: ['a', 'b'] },
createdAt: new Date(),
scores: new Map([['math', 90]]),
};
const clone = structuredClone(state);
clone.user.tags.push('c');
console.log(state.user.tags); // ['a', 'b'] — 원본은 그대로
console.log(clone.createdAt instanceof Date); // true — Date 유지
console.log(clone.scores.get('math')); // 90 — Map도 그대로
Date, Map, Set, ArrayBuffer, 심지어 순환 참조까지 정상 복제된다. JSON 왕복으로 깨지던 것들이 대부분 해결된다.
한계도 알아두자
만능은 아니다. 함수와 DOM 노드는 복제할 수 없어서 넣으면 DataCloneError가 난다. 클래스 인스턴스를 넣으면 프로토타입이 사라지고 평범한 객체(plain object)로 바뀐다. getter/setter나 심볼 키도 넘어오지 않는다.
structuredClone({ fn: () => {} }); // ❌ DataCloneError
정리하자면 이렇다.
- 순수 데이터(폼 상태, API 응답, 설정 객체) 복사 →
structuredClone() - 얕은 한 겹만 필요 → 스프레드가 더 빠르고 충분
- 함수·클래스 메서드까지 살려야 함 → lodash
cloneDeep같은 라이브러리
마무리
structuredClone()은 Chrome·Firefox·Safari·Node 17+에서 전부 쓸 수 있는 Baseline 기능이다. JSON.parse(JSON.stringify())를 습관처럼 쓰고 있었다면 이제 바꿀 때가 됐다. 데이터 객체를 복사할 땐 이거 한 줄이면 Date가 문자열로 둔갑하는 미묘한 버그를 애초에 안 만난다.