본문 바로가기
개발(Development)/JS(자바스크립트)

[JS] Shallow Copy & Deep Copy 기본 개념, 원리 (feat. 값 vs. 레퍼런스 전달)

by 카레유 2023. 8. 22.

# Shallow Copy, Deep Copy 기본 이해

이 글에서는 Shallow copy(얕은 복사), Deep copy(깊은 복사)의 기본 개념은 물론,

실질적인 구현 방법과 근본적인 작동 원리까지 파악해보려 한다.

 

  • 자바스크립트의 Data Type과 Value 전달 및 Reference 전달
  • Shallow Copy 와 Deep Copy개념과 구현 방법
  • 객체의 작동 방식과 Shallow Copy의 근본 원리

# Javascript의 데이터 타입

Javascript의 데이터 타입은 원시타입과 참조타입이 있다.

 

1. Primitive Data types

  • undefined
  • Boolean
  • Number
  • String
  • BigInt
  • Symbol
  • ...

2. Structural Data Types

  • Object: new 키워드로 생성 가능한 객체 타입들
  • Functions
  • Array
  • Map
  • ...

# 값 전달 vs. 레퍼런스 전달 (Passing Value vs. Reference)

원시타입과 참조타입의 변수는 다른 변수에 할당될 때 작동하는 방식이 다르다.

 

1. Primitive Types

원시타입은 값(Value)를 전달한다.

 

let x = 2;
let y = x; // 값 2를 전달한다.

y += 1;

// x 와 y는 서로 영향을 미치지 않는다.
console.log(y); // 3
consloe.log(x); // 2

 

2. Structural Types

참조타입은 값(Value)이 위치한 메모리의 참조값(Reference)를 전달한다.

 

let arrX = [1, 2];
let arrY = arrX; // arrX 배열의 레퍼런스를 전달하므로, arrX와 arrY는 같은 레퍼런스를 바라보게 된다.

// arrY에 변화를 주면, arrX에도 영향을 주게 된다.
arrY.push(3);

console.log(arrY); // [1, 2, 3];
console.log(arrX); // [1, 2, 3];

console.log(arrX === arrY); // true

# 객체/배열 복사(Object Copy)

변수를 통한 재할당의 경우,

  • 원시타입(Primitive Type)은 Value를 전달한다.
    • 따라서 복사(copy)가 필요하지 않다. (재할당 만으로 충분하다)
    • 왜냐하면 한 쪽의 변경이 다른 쪽에 영향을 미치지 않기 때문에, 값이 복사된 거나 마찬가지이기 때문이다.
  • 참조타입(Structural Tyle)은 Referece 를 전달한다.
    • 따라서 복사 기능이 필요한 경우가 생긴다.
    • 한쪽의 변경이 다른 쪽에 영향을 미치므로, 서로 독립적으로 작동할 필요가 경우가 있을 수 있기 때문이다.

 

객체의 복사는 아래의 두가지 방법으로 가능하다.

  • Spread Syntax
    • [..array] : 배열 복사
    • {...obj} : 객체 복사
  • Object.assign(target, ...source) 함수

 

// Spread operator
const arrA = [1, 2];
const arrB = [...arrA];

arrB.push(100); // arrB에만 100을 추가한다. arrA에는 영향을 미치지 않음

console.log(arrA); // [1, 2]
console.log(arrB); // [1, 2, 100]
console.log(arrA === arrB); // false

// Object.assign()
const arrC = Object.assign([], arrA);

console.log(arrA); // [1, 2]
console.log(arrC); // [1, 2]

arrC.push(100); // arrC에만 100을 추가한다. arrA에는 영향을 미치지 않음

console.log(arrA); // [1, 2]
console.log(arrC); // [1, 2, 100]
console.log(arrA === arrC); // false

 

그러나, 문제가 발생한다.

원시타입의 값으로 구성된 Top-level Property 들과 다르게,

중첩된 엘리먼트 및 프로퍼티(nested propety) 들은 한 쪽의 변화가 다른 쪽에 영향을 미치게 된다.

 

const arrD = [1, 2, [100, 200, 300]];
const arrE = [...arrD];

arrE[2].push(999);

// nested element 들은 reference를 공유하여, 서로 영향을 미친다.
console.log(arrD); // [1, 2, [100, 200, 300, 999]]
console.log(arrE); // [1, 2, [100, 200, 300, 999]]

// cf. top-level properties 들은 서로 영향을 미치지 않는다.
arrE.push(11);
console.log(arrD); // [1, 2, [100, 200, 300, 999]]
console.log(arrE); // [1, 2, [100, 200, 300, 999], 11]

 

이는 중첩 요소(nested properties) 의 경우, 레퍼런스를 공유하는게 디폴트이기 때문이다.

이를 Shallow Copy라고 부른다.

 

여기서 Shallow Copy, Deep Copy의 개념이 나온다.


# Shallow Copy vs. Deep Copy

객체를 복사할 때는

1-depth 레벨의 "얕은" 깊이까지만 복사를 하는지, 아니면 더 깊은 레벨까지 복사를 하는지에 따라

2가지 타입의 복사 개념이 있다:

  • Shallow Copy(얕은 복사):
    • 1-Depth 레벨의 "얕은" 깊이까지만 복사본을 만든다.
    • 더 깊은 중첩 요소들 간에는 Reference를 공유하여, 원본-복사본이 서로 영향을 미친다.
  • Deep Copy(깊은 복사):
    • "모든 깊이"의 레벨에 대해 복사본을 만든다.
    • 복사되는 객체들의 중첩 요소들 간에 Referece를 공유하지 않아, 원본-복사본이 완전히 별개이다.

 

※ Shallow/Deep Copy 는 "객체를 복사할 때, 중첩 속성이 서로 레퍼런스를 공유하고, 영향을 미치는지" 에 관해서만 한정된 개념이다. 원시타입으로 값(Value)로 구성된 Top-level 속성과는 관련이 없다. 객체/복사는 디폴트로 중첩 요소만 레퍼런스를 공유하여 서로 영향을 미치기 때문이다. 

 

# Shallow Copy(얕은 복사)

자바스크립트의 기본 빌트인 복사 기능들 (spread syntax, Array.prototype.concat(), Array.prototype.slice(), Array.from(), Object.assign(), and Object.create()) 은 모두 Shallow Copy를 만든다.

 

Shallow Copy는 1-depth level 의 "얕은" 깊이까지만 복사본을 만들고, 더 "깊은" 깊이는 그냥 기존 것을 공유한다.

 

즉, 원본과 복사본의 중첩 요소들은 서로 같은 Reference를 공유한다.

그 결과, 한 쪽의 중첩 요소에 변화를 주면, 다른 쪽의 중첩요소도 동일하게 변하게 된다.

(원시 타입 값으로 구성된 Top-level Property는 서로 영향을 미치지 않는다.)

 

const arrD = [1, 2, [100, 200, 300]];
const arrE = [...arrD];

arrE[2].push(999);

// nested elemet들은 같은 Referece를 공유하여 함께 변한다.
console.log(arrD); // [1, 2, [100, 200, 300, 999]]
console.log(arrE); // [1, 2, [100, 200, 300, 999]]

 

Shallow Copy 라는 이름이 붙은 이유도 1-depth level 까지만 "얕은" 깊이로 복사를 하기 때문이 아닐까 한다.

 

# Deep Copy (깊은 복사 - Clone)

Deep Copy는 객체의 모든 깊이까지 복사본을 만드는 완전한 카피 방식이다. (공유X)

 

원본과 복사본의 중첩 요소들은  Reference를 공유하지 않으며, 서로 완전한 별개이다.

따라서, 한 쪽의 중첩요소에 변화를 주어도 다른 쪽의 중첩 요소에 전혀 영향을 미치지 않는다.

 

그러나,Javascript 언어 자체적으로는 Deep Copy 기능을 제공하지 않는다.

하지만 아래의 2가지 방법으로 Deep Copy가 가능하기는 하다.

 

  • JSON.parse(JSON.stringify(srcObj))
    • 객체를 string으로 변환 했다가, 다시 객체로 변환하는 방법이다.
    • 이렇게 하면 완전히 다른 객체/배열을 복사할 수 있다.
    • 단, 이 방법은 직렬화 가능한 객체(Serialiable Object, 일반 객체, 배열 등)에만 적용 가능하며, Functons, Date, Set, HTML Element 등에는 적용이 불가하다.
  • structuredClone(src) 함수
    • 일반 객체/배열 뿐만 아니라 transferable objects  및 Error 객체 등에도 적용 가능하다.
    • 브라우저에 의해 제공되는 Web API 이다. (JS 언어 자체적으로 제공하는 함수가 아니다.)

 

// JSON.parse(JSON.stringify(srcObj)) 사용
const arrF = [1, 2, [100, 200, 300]];

const arrG = JSON.parse(JSON.stringify(arrF));
arrG[2].push(999);

console.log(arrF); // [1, 2, [100, 200, 300]]
console.log(arrG); // [1, 2, [100, 200, 300, 999]]

// structuredClone(src) 사용
const arrH = [1, 2, [100, 200, 300]];

const arrI = structuredClone(arrH);
arrI[2].push(999);

console.log(arrH); // [1, 2, [100, 200, 300]]
console.log(arrI); // [1, 2, [100, 200, 300, 999]]

 

객체/배열을 복사해서 사용할 때,

Shallow Copy가 디폴트이므로 주의해서 사용하면 좋을듯 하다.


# 객체는 사실 중첩(nested)이 없다!

그런데 왜 Shallow Copy가 디폴트일까?

 

사실 자바스크립트 객체에는 중첩이란 개념 자체가 없기 때문이다.

코드 표현상, 여러 속성들이 다양한 깊이로 중첩된 것으로 보일 뿐이다.

 

예를 들어, 아래처럼 속성들이 중첩된 것처럼 보이는 객체가 있다고 해보자.

 

let obj = {
  name: 'Teddy',
  desc: {
    job: '개발자',
    age: '30',
    etc: '호두과자를 좋아함',
  }
};

 

desc 속성 하위에  job, age, etc 중첩 속성이 있는 것처럼 보이지만,

"그렇게 보일 뿐이다"

 

실제로는 이렇게 작동한다고 보는 편이 옳다.

 

let obj2 = {
  job: '개발자',
  age: '30',
  etc: '호두과자를 좋아함',
};

let obj1 = {
  name: 'Teddy',
  desc: obj2 // desc 속성은 외부에 별도로 존재하는 obj2 객체를 가르킬 뿐이다.
};

 

obj2 객체는 obj1 객체와는 별도로 존재하는 외부의 객체이다!

 

desc 속성은 단지, 외부에 별도로 존재하는 obj2 객체를 가르키는 reference 값을 저장하고 있을 뿐이다!

따라서 obj1.desc.job 값을 변경하면, obj2.job 값도 변경된다. (같은 obj2 객체의 job 을 변경하는 것이므로!)

 

마찬가지로 obj1 객체를 복사하면, desc 속성에는 obj2의 reference 주소가 복사된다.

따라서 복사본.desc.job을 변경하면, obj1.desc.job 은 물론 obj2.job 도 모두 함께 변경된다.

 

이것이 객체 복사시, shallow copy가 디폴트인 이유이며 원리이다.


※ 관련 글

[JS] Mutation이란? mutable vs. Immutable (cf. const 키워드)

 

 

 

댓글