JavaScript 객체 복사, 얕은 복사와 깊은 복사
복사가 필요한 이유
섹션 제목: “복사가 필요한 이유”객체는 참조형이다. 변수에 객체를 할당하면 객체에 대한 참조가 들어간다. const b = a로 대입하면 두 변수가 같은 객체를 가리키므로, b를 고치면 a도 바뀐다.
- 원본 데이터가 의도치 않게 바뀌는 부작용을 막는다.
- React, Vue, Redux 같은 상태 관리에서 불변성을 지킨다.
- API 응답을 가공하거나 컴포넌트 사이에 데이터를 넘길 때 원본을 보존한다.
얕은 복사
섹션 제목: “얕은 복사”객체의 첫 단계 속성만 새 객체에 복사한다. 중첩된 객체나 배열은 원본과 같은 참조를 공유한다.
- 전개 구문:
{ ...original } Object.assign({}, original)
전개 구문은 자기 소유의 열거 가능한 속성만 복사하고, 프로토타입과 열거 불가능한 속성은 복사하지 않는다. Object.assign()은 대상 객체의 setter를 호출하지만 전개 구문은 호출하지 않는다(MDN: 전개 구문).
const user = { name: 'Kim', address: { city: 'Seoul', },};
const shallowCopy = { ...user };shallowCopy.address.city = 'Busan';
console.log(user.address.city); // "Busan"중첩 객체를 고치면 원본도 바뀐다. 구조가 단순하거나 중첩 객체를 고치지 않을 때 쓴다.
깊은 복사
섹션 제목: “깊은 복사”중첩 구조까지 재귀적으로 복사해 원본과 참조를 공유하지 않는 새 객체를 만든다.
structuredClone()
섹션 제목: “structuredClone()”구조화된 클론 알고리즘으로 값을 깊은 복사한다. 순환 참조를 지원하고, 2022년 3월부터 주요 브라우저에서 쓸 수 있다(MDN: structuredClone()).
복사할 수 있는 타입은 Array, ArrayBuffer, Date, Map, Set, RegExp, TypedArray, 일반 객체, symbol을 제외한 원시값, 일부 Error 타입 등이다. 다음은 복사되지 않는다(MDN: 구조화된 클론 알고리즘).
- 함수:
DataCloneError예외가 발생한다. - DOM 노드:
DataCloneError예외가 발생한다. - 속성 설명자, getter, setter: 복제되지 않는다. 읽기 전용 속성도 복사본에서는 읽기·쓰기가 된다.
- 프로토타입 체인: 따라가지 않는다. 클래스 인스턴스는 메서드가 없는 일반 객체가 된다. 예외는 발생하지 않는다.
- 클래스 private 요소,
RegExp의lastIndex: 복제되지 않는다.
JSON.parse(JSON.stringify(obj))
섹션 제목: “JSON.parse(JSON.stringify(obj))”JSON 문자열로 바꿨다가 다시 파싱한다. JSON이 표현할 수 있는 값만 남는다(MDN: JSON.stringify()).
Date는toJSON()결과인 ISO 문자열이 되고, 파싱해도 문자열로 남는다.- 객체 속성의
undefined, 함수,Symbol은 사라진다. 배열 안에서는null이 된다. NaN,Infinity는null이 된다.Map,Set은{}가 된다.- 순환 참조나
BigInt가 있으면TypeError가 발생한다.
직접 구현과 라이브러리
섹션 제목: “직접 구현과 라이브러리”재귀 함수를 직접 쓰거나 Lodash의 _.cloneDeep 같은 라이브러리를 쓴다. 특수한 요구에 맞출 수 있지만, 직접 구현은 순환 참조와 특수 타입 처리에서 버그가 생기기 쉽고 라이브러리는 의존성이 늘어난다.
두 방식 비교
섹션 제목: “두 방식 비교”| 항목 | structuredClone() |
JSON.parse(JSON.stringify()) |
|---|---|---|
| 순환 참조 | 복사한다 | TypeError |
Date |
Date로 유지 |
문자열 |
Map, Set, RegExp |
유지 | {} |
undefined |
유지 | 객체 속성이면 사라짐 |
| 함수 | DataCloneError |
객체 속성이면 사라짐 |
| 클래스 인스턴스 | 프로토타입 없는 일반 객체 | 프로토타입 없는 일반 객체 |
const original = { date: new Date(), set: new Set([1, 2, 3]), regex: /foo/,};original.circ = original;
const deepCopy = structuredClone(original);console.log(deepCopy.date instanceof Date); // trueconsole.log(deepCopy.circ === deepCopy); // true
try { JSON.parse(JSON.stringify(original));} catch (e) { console.error(e.name); // "TypeError"}선택 기준
섹션 제목: “선택 기준”| 상황 | 방식 | 이유 |
|---|---|---|
| 단순한 옵션 객체 전달 | 얕은 복사 | 중첩 객체를 고칠 일이 없다. |
| API 요청 데이터 생성 | 얕은 복사 | 대부분 한 단계 구조이고 복사본의 쓰임이 분명하다. |
| React/Redux 중첩 상태 갱신 | 바뀌는 경로만 단계별 얕은 복사 | 바뀌는 중첩 단계마다 새 객체가 필요하고, 바뀌지 않은 부분은 참조를 그대로 둔다. |
| 외부 데이터를 가공하면서 원본 보존 | 깊은 복사 | 중첩 값을 고쳐도 원본이 바뀌지 않아야 한다. |
| Undo/Redo 스냅샷 | 깊은 복사 | 각 시점의 상태가 서로 독립적이어야 한다. |
Redux는 중첩 데이터를 갱신할 때 바뀌는 모든 단계를 복사하라고 안내한다. 최상위만 얕은 복사하면 중첩 객체를 직접 변경하게 된다(Redux: Immutable Update Patterns).
깊은 복사가 필요하면 structuredClone()을 먼저 쓴다. 함수나 클래스 인스턴스처럼 이 방식이 다루지 못하는 값이 있으면 직접 구현이나 라이브러리를 검토한다. 대상 브라우저와 런타임이 structuredClone()을 지원하는지 확인한다.