콘텐츠로 이동

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"

중첩 객체를 고치면 원본도 바뀐다. 구조가 단순하거나 중첩 객체를 고치지 않을 때 쓴다.

중첩 구조까지 재귀적으로 복사해 원본과 참조를 공유하지 않는 새 객체를 만든다.

구조화된 클론 알고리즘으로 값을 깊은 복사한다. 순환 참조를 지원하고, 2022년 3월부터 주요 브라우저에서 쓸 수 있다(MDN: structuredClone()).

복사할 수 있는 타입은 Array, ArrayBuffer, Date, Map, Set, RegExp, TypedArray, 일반 객체, symbol을 제외한 원시값, 일부 Error 타입 등이다. 다음은 복사되지 않는다(MDN: 구조화된 클론 알고리즘).

  • 함수: DataCloneError 예외가 발생한다.
  • DOM 노드: DataCloneError 예외가 발생한다.
  • 속성 설명자, getter, setter: 복제되지 않는다. 읽기 전용 속성도 복사본에서는 읽기·쓰기가 된다.
  • 프로토타입 체인: 따라가지 않는다. 클래스 인스턴스는 메서드가 없는 일반 객체가 된다. 예외는 발생하지 않는다.
  • 클래스 private 요소, RegExp의 lastIndex: 복제되지 않는다.

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); // true
console.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()을 지원하는지 확인한다.