요약

특성얕은 복사 (Shallow Copy)깊은 복사 (Deep Copy)
복사 범위최상위 수준의 속성만 복사모든 수준의 속성을 복사
중첩된 객체/배열원본과 복사본이 중첩된 객체/배열을 공유중첩된 객체/배열도 복사하여 완전히 독립적
메모리 참조원본과 복사본이 중첩된 객체/배열의 메모리 참조를 공유원본과 복사본이 서로 다른 메모리 참조를 가짐
변경 시 영향중첩된 객체/배열을 변경하면 원본과 복사본 모두에 영향복사본에서의 변경이 원본에 영향을 미치지 않음
사용 방법 예Array.prototype.slice(), Object.assign({}, obj), 스프레드 연산자재귀 함수, JSON.parse(JSON.stringify(obj)), 라이브러리(예: lodash의 _.cloneDeep(obj))
적용 시나리오간단한 데이터 구조, 중첩된 구조가 없거나 공유해도 되는 경우복잡한 데이터 구조, 완전히 독립적인 복사본이 필요한 경우

얕은 복사(Shallow Copy)란?

  • 객체의 최상위 수준만 복사한다.
  • 중첩된 객체나 배열은 참조값을 공유한다.
  • 예: { a: 1, b: { c: 2 } }를 얕은 복사하면, b 속성 내의 객체는 원본과 복사본 간에 공유된다.

얕은 복사 방법 3가지

  1. 스프레드 연산자 사용
// 원본 객체 생성
const original = {
  a: 1,
  b: 'string',
  c: true
};

// 얕은 복사 수행
const shallowCopy = { ...original };

// 복사본 수정
shallowCopy.a = 2;
shallowCopy.b = 'modified';
shallowCopy.c = false;

// 결과 출력
console.log(original);  // { a: 1, b: 'string', c: true }
console.log(shallowCopy); // { a: 2, b: 'modified', c: false }

중첩된 구조가 없다면 일반적으로 생각하는 원본과 다른 또 하나의 복사본이 생성된다.

그러나 배열이나 객체가 중첩된 구조를 가진다면 얕은 복사의 주의 사항이 드러난다.

// 원본 객체에 중첩된 객체 포함
const original = {
  a: 1,
  b: {
    c: 2,
    d: 3
  }
};

// 얕은 복사 수행
const shallowCopy = { ...original };

// 복사본의 중첩된 객체 수정
shallowCopy.b.c = 20;

// 결과 출력
console.log(original);  // { a: 1, b: { c: 20, d: 3 } }
console.log(shallowCopy); // { a: 1, b: { c: 20, d: 3 } }

이와 같이 중첩이 존재한다면 얕은 복제는 원본의 참조 주소만 복사하므로 복사본을 수정했을 때 원본이 수정되면서 불변성을 유지할 수 없게 된다.

  1. Array.prototype.slice()
// 원본 배열
const originalArray = [1, 2, 3, 4];
// slice()를 사용한 얕은 복사
const shallowCopiedArray = originalArray.slice();

// 복사본 수정
shallowCopiedArray.push(5);

console.log(originalArray); // [1, 2, 3, 4]
console.log(shallowCopiedArray); // [1, 2, 3, 4, 5]
  1. Object.assign(생성할 객체, 복사할 객체)
// 원본 객체
const originalObject = { a: 1, b: { c: 2 } };
// Object.assign()을 사용한 얕은 복사
const shallowCopiedObject = Object.assign({}, originalObject);

// 복사본의 중첩된 객체 수정
shallowCopiedObject.b.c = 20;

console.log(originalObject); // { a: 1, b: { c: 20 } }
console.log(shallowCopiedObject); // { a: 1, b: { c: 20 } }

복사를 하려는 배열이나 객체가 중첩된 구조를 가지고 있는 지 판단하여 얕은 복사와 깊은 복사 중 무엇을 할 지 판단해야 한다.

깊은 복사(Deep Copy)란?

  • 객체의 모든 수준을 복사하여 완전히 독립적인 복사본을 만든다.
  • 중첩된 객체나 배열도 새로운 메모리에 할당하여 복사한다.
  • 예: { a: 1, b: { c: 2 } }를 깊은 복사하면, b 속성 내의 객체도 새롭게 복사되어 원본과는 독립적인 상태가 된다.

깊은 복사를 하는 법 3가지

  1. JSON을 사용하는 방법
const original = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4],
    e: new Date()
  }
};

const deepCopy = JSON.parse(JSON.stringify(original));

// 복사본을 수정하여도 원본에 영향을 주지 않음
deepCopy.b.c = 20;
deepCopy.b.d.push(5);

console.log(original); // { a: 1, b: { c: 2, d: [3, 4], e: Date 객체 } }
console.log(deepCopy); // { a: 1, b: { c: 20, d: [3, 4, 5], e: 문자열화된 Date 객체 } }
  • 이 방법은 직관적으로 사용할 수 있지만 Date 객체, undefined, 심볼, 순환 참조 등은 올바르게 복사가 되지 않는다.
  • JSON이란? 경량의 데이터 교환 방식으로 사람이 읽고 쓰기 쉽고 기계도 파싱하고 생성하기 편리한 텍스트 기반의 표준화된 구조이다. 그래서 웹의 클라이언트와 서버 간 데이터 교환에 주로 사용되는 포맷이다.
  • JSON을 사용해서 객체를 문자열로 변환(직렬화)하고 다시 문자열을 객체로 변환(역직렬화)하면 중첩 구조의 데이터도 복사가 된다.
  • 직렬화(JSON.stringify())하면서 중첩된 데이터도 문자열로 변환되므로 메모리 주소나 참조는 포함되지 않는다. 다시 역직렬화(JSON.parse())하면 중첩 구조의 데이터도 같이 문자열에서 데이터로 변환되는 것이다.
  1. 재귀함수를 사용
function deepCopy(obj, hash = new WeakMap()) {
  if (obj === null) return null; // null은 그대로 반환
  if (typeof obj !== "object") return obj; // 기본 타입은 그대로 반환
  if (obj instanceof Date) return new Date(obj); // Date 객체 복사
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags); // RegExp 객체 복사
  if (hash.has(obj)) return hash.get(obj); // 순환 참조 처리

  const result = Array.isArray(obj) ? [] : {};
  hash.set(obj, result);

  for (const key of Object.keys(obj)) {
    result[key] = deepCopy(obj[key], hash); // 재귀 호출
  }

  return result;
}

const original = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4]
  },
  e: new Date(),
  f: function() { console.log("f()"); },
  g: new RegExp("\\w+")
};

const deepCopyResult = deepCopy(original);

console.log(original);
console.log(deepCopyResult);
  • 이 방법은 첫 번째 방법에서 다루지 못한 다양한 유형의 데이터를 처리할 수 있다.
  • 함수가 obj를 인자로 받으면 obj의 모든 속성들을 탐색하는 재귀 함수를 사용하여 Date 객체 등을 탐색하고 새로운 Date 객체를 만들어서 할당하는 방식으로 복사가 이뤄진다.
  1. 라이브러리 사용
# lodash 라이브러리 설치
npm install lodash
// lodash 라이브러리 불러오기
const _ = require('lodash');

// 깊은 복사할 객체
const original = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4],
    e: { f: 5 }
  }
};

// _.cloneDeep()을 사용한 깊은 복사
const deepCopy = _.cloneDeep(original);

// 복사본 수정
deepCopy.b.c = 20;
deepCopy.b.d.push(5);

// 결과 출력
console.log(original); // { a: 1, b: { c: 2, d: [3, 4], e: { f: 5 } } }
console.log(deepCopy); // { a: 1, b: { c: 20, d: [3, 4, 5], e: { f: 5 } } }
  • 깊은 복사와 같은 복잡한 작업을 수행할 때는 검증된 라이브러리를 도입하는 것이 프로젝트의 안정성, 시간 절약, 기능성, 유지 보수에 좋다고 판단할 수 있다.
  • 또는 의존성과 빌드 크기, 성능 측면에서 도입을 하지 않는 판단을 할 수도 있다.
  • 기타 다른 라이브러리 : Immutable.js Ramda

요약

자바스크립트의 데이터 구조는 두 가지가 있습니다.
원시타입과 참조 타입입니다.
원시 타입은 변수에 값이 직접 저장됩니다.
참조 타입은 값이 저장된 메모리의 주소를 저장합니다. 주소를 참조라고 부르기 때문에 참조를 변수에 저장하므로 참조 타입이라고 합니다.

이런 구조 때문에 참조 타입에는 깊은 복사와 얕은 복사의 구분이 생깁니다.
참조 타입 예시로 객체를 활용하겠습니다.
깊은 복사는 객체의 모든 레벨을 새롭게 복사합니다. 참조가 가리키는 메모리를 따라가서 전부 복사합니다. 객체 안에 객체가 중첩된 형태를 생각할 수 있습니다. 참조를 따라 메모리 깊숙히 들어가서 값을 가져오는 이미지입니다.

얕은 복사는 최상위 레벨의 속성만 복사합니다.
참조만 복사하게 됩니다.

그 결과 얕은 복사로 만들어진 변수와 원본 변수가 하나의 메모리를 참조하는 상태가 됩니다.
이제 복사한 변수를 수정하면 원본의 데이터가 수정되어 처음 선언한 변수의 값도 같이 변경되는 일이 발생합니다.

이 개념을 알고 자신이 다루는 변수의 타입을 인지하고 필요에 따라 복사하는 방법을 선택할 수 있어야겠습니다.


참고자료
https://bbangson.tistory.com/78

profile
문제를 컴퓨터로 해결하는 데서 즐거움을 찾는 프론트엔드 개발자

1개의 댓글

comment-user-thumbnail
2024년 3월 27일

깊은 복사와 얕은 복사를 수행하는 다양한 방법에 대해 배웠습니다!
감사합니다~ :)

답글 달기

관련 채용 정보