Javascript

JavaScript에서 얕은 복사와 깊은 복사 이해하기

Yepchani 2023. 10. 16. 05:51

들어가며

JavaScript를 사용하다 보면 객체나 배열과 같은 참조형 데이터를 다루게 되는데요.

참조형 데이터를 복사할 필요가 있을 때, 얕은 복사와 깊은 복사라는 개념이 중요하게 작용합니다.

 

이번 포스트에서는 JavaScript에서 얕은 복사와 깊은 복사에 대해 알아보겠습니다.

 


얕은 복사 (Shallow Copy)

얕은 복사는 이름처럼 복사하는 깊이가 얕은데요. 객체를 한 단계까지만 복사할 수 있습니다.

객체를 복사할 때 원시 값의 경우 값을 그대로 복사하지만, 객체의 경우 참조 값, 즉 객체가 저장되어 있는 메모리 주소를 복사하는데요.

그렇기 때문에 얕은 복사를 통해 만들어진 객체를 수정하면 원본 객체에 영향을 미칠 수 있으므로 주의해야 합니다.

 

몇 가지 예시를 들어볼게요.

1. 배열의 slice 메서드

const original = [1, 2, { a: 3 }];
const copy = original.slice();

copy[0] = 10;
copy[2].a = 4;
console.log(original); // 출력 결과: [ 1, 2, { a: 4 } ]

 

2. 객체의 Object.assign 메서드

const original = { a: "Hello", b: { c: "World" } };
const copy = Object.assign({}, original);

copy.a = "New";
copy.b.c = "!";
console.log(original); //출력 결과 : { a: 'Hello', b: { c: '!' } }

 

3. 배열과 객체의 스프레드 연산자(...)

// 배열 복사
const originalArray = [1, 2, { a: 3 }];
const copyArray = [...originalArray];

copyArray[0] = 10;
copyArray[2].a = 4;
console.log(originalArray); // 출력 결과: [ 1, 2, { a: 4 } ]

// 객체 복사
const originalObj = { a: "Hello", b: { c: "World" } };
const copyObj = { ...originalObj };

copyObj.b.c = "!";
console.log(originalObj); //출력 결과 : { a: 'Hello', b: { c: '!' } }

 

깊은 복사 (Deep Copy)

깊은 복사는 얕은 복사와 달리 객체에 중첩되어 있는 객체까지 모든 단계를 복사해 완전한 복사본을 만듭니다.

따라서 깊은 복사를 통해 만들어진 객체를 수정하더라도 원본 객체에 영향을 미치지 않습니다.

 

몇 가지 예시를 들어볼게요.

1. JSON.stringify와 JSON.parse

const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));

copy.b.c = 3;
console.log(original); // 출력 결과: { a: 1, b: { c: 2 } }

JSON.stringify 메서드로 객체 전체를 문자열로 변환 후, JSON.parse 메서드로 다시 객체로 변환하는 과정을 거치면서 새로운 참조값을 가진 복사본이 생성됩니다.

 

다만 이 방법은 다음과 같은 문제점이 있습니다.

  1. 함수를 복사하지 못합니다.
    객체에 함수가 포함되어 있다면, JSON 형식으로 변환하는 과정에서 해당 함수는 제거됩니다. 따라서 복사 후의 객체에는 원본 객체의 함수가 존재하지 않게 됩니다.
  2. undefined, Symbol 값을 복사하지 못합니다.
    이들 값 역시 JSON 형식으로 변환하는 과정에서 제거됩니다.
  3. 순환 참조(Circular reference)가 있는 객체를 복사할 수 없습니다.
    순환 참조가 있는 객체를 JSON.stringify로 변환하려고 하면 'Converting circular structure to JSON' 오류가 발생합니다.
  4. Date 객체와 같은 내장 클래스 인스턴스의 프로토타입 체인이 유지되지 않습니다.
    Date, RegExp 등의 내장 클래스 인스턴스는 JSON 형식으로 변환하면서 해당 데이터 타입을 잃어버립니다.

다음 예시들을 살펴보도록 하죠.

 

Date 타입이 있는 객체를 복사하는 경우

const original = { a: new Date() };
const copy = JSON.parse(JSON.stringify(original));

console.log(original.a instanceof Date); // 출력 결과: true
console.log(copy.a instanceof Date); // 출력 결과: false
console.log(typeof copy.a); // 출력 결과: string

위 예시에서 원본 객체의 'a' 속성값은 Date 인스턴스였지만, 깊은 복사 후에는 일반 문자열로 바뀌었습니다.

 

순환 참조가 있는 객체를 복사하는 경우

const obj = {};
obj.myself = obj;

console.log(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON

위 예시에서 obj는 자기 자신을 참조하는 속성 myself를 가지고 있습니다. 이렇게 되면 obj는 순환 참조 구조를 형성하게 됩니다. 순환 참조가 있는 객체를 JSON.stringify로 변환하려고 하여 'TypeError: Converting circular structure to JSON' 오류가 발생합니다.


따라서 이런 경우들에 대해 고려해야 한다면, 다른 방법을 사용하여 깊은 복사를 수행해야 합니다.

 

2. lodash 라이브러리의 _.cloneDeep() 함수

const _ = require('lodash');

const original = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(original);

copy.b.c = 3;
console.log(original); // 출력 결과: { a: 1, b: { c: 2 } }

 

이 함수는 객체 내부의 모든 레벨을 재귀적으로 복사하여, 원본 객체와 완전히 독립된 사본을 생성합니다.

함수나 날짜 객체 등의 데이터 타입도 올바르게 복사되는데요. 하지만 함수를 복사할 때 이는 함수의 참조를 복사하는 것이지, 함수의 클로저 환경까지 분리하지는 못 합니다. 즉, 완전히 독립적인 새로운 함수를 생성하는 것은 아닙니다.

 

다만 이 방법은 다음과 같은 문제점이 있습니다.

  1. 성능 문제
    _.cloneDeep() 함수는 깊이와 넓이 모두에 대해 재귀적인 방식으로 동작하기 때문에, 매우 큰 객체나 배열에서 성능 문제가 발생할 수 있습니다.
  2. 외부 라이브러리 의존성
    lodash 라이브러리를 프로젝트에 추가해야 합니다. 이로 인해 전체 프로젝트 크기가 커질 수 있으며, lodash 라이브러리가 업데이트되거나 문제가 발생했을 때 그 영향을 받게 됩니다.

위의 두 방법 외에 필요한 경우 반복문이나 재귀로 커스텀 코드를 작성할 수도 있습니다.

상황에 맞게 적절한 방법을 사용하면 될 것 같습니다.

 


마치며

JavaScript에서 참조형 데이터를 복사할 때는 얕은 복사와 깊은 복사의 차이를 이해하고, 상황에 맞게 적절한 방법을 선택해야 하는데요.

간단한 경우에는 얕은 복사가 더 빠르고 효율적일 수 있지만, 중첩된 객체의 경우 완전히 복사하려면 깊은 복사를 사용해야 합니다.

 

잘못된 내용이 있다면 지적 부탁드립니다.

감사합니다 :D