블로그 목록
frontend

structuredClone()로 객체 깊은 복사하기 — JSON.parse(JSON.stringify()) 꼼수 그만

상태를 직접 건드리면 안 되니까 객체를 복사해서 쓰는 일이 잦다. 근데 스프레드(...)는 얕은 복사라서 중첩된 객체는 여전히 같은 참조를 가리킨다. 안쪽 값을 바꾸면 원본까지 같이 바뀐다.

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가 문자열로 둔갑하는 미묘한 버그를 애초에 안 만난다.