얕은 복사 vs 깊은 복사

이민선(Jasmine)·2023년 3월 2일
0
post-thumbnail

1. 원시 자료형 vs 참조 자료형

1) 원시 자료형(primitive type)

변경 불가능한 값(immutable)임을 의미한다. (ex. number, string, boolean, undefined, null, symbol)
변경 불가능하다는 의미가 무엇인지 알아보자.

  • 읽기 전용
    문자열의 일부분을 변경하려고 시도해보자.
let str = "deer";
str[2] = "a";
console.log(str) = "deer";

일부 문자열 변경을 시도해도 str은 변하지 않는다. (에러도 나지 않는다.)

  • 원시자료형을 어떤 변수에 할당하고 나면, 참조자료형과 달리 변수 값을 변경할 수 있는 방법으로는 재할당 밖에 없다.
let score;
score = 80;
score = 90;

이때 변수에 다른 값을 재할당하려면 (변수가 참조하던 메모리 공간 내의 값 80을 지우고 90으로 변경하는 것이 아니라) 변수가 가리키던 메모리 공간의 주소를 다른 곳으로 바꿔줘야 한다.

더 이상 참조하지 않는 기존의 원시값(그림에서는 undefined와 80)은 어떻게 될까? 가비지 콜렉터가 예상치 못한 시점에 청소해준다!

2) 참조 자료형(reference value)

변경 가능한 값(mutable)임을 의미한다.
변경 가능하다는 의미가 무엇인지 알아보자.

  • 원시값을 저장한 변수와 달리, 참조값을 저장한 변수에는 힙에 별도로 저장되어 있는 객체의 메모리주소가 저장된다. 따라서 힙에 저장된 객체를 수정하고 싶다? (원시값처럼 변수에 새로운 값을 재할당할 필요 없이) 직접 프로퍼티 추가, 프로퍼티 값 갱신, 프로퍼티 삭제 등이 가능하다.
const person = {
    name : "Jasmine"
}
console.log(person) // { name : "Jasmine" }
person.name = "자스민"
console.log(person) // { name : "자스민" }

  • 재미있는 사실! const 키워드로 객체를 변수에 할당하면 재할당은 불가능하지만 프로퍼티 변경(프로퍼티 동적 생성, 삭제, 변경)은 가능하다. 물론 재할당 과정을 거치지 않으므로 프로퍼티 변경 후에도 객체를 할당한 변수의 레퍼런스는 변하지 않는다.

참조 자료형이 변경 가능한 값이기 때문에 생기는 장점과 단점이 있다.

장점:

객체 프로퍼티 변경 시, 원시값마냥 객체 자체를 복사하지 않아도 되기 때문에
메모리 사용 효율성과 성능 향상 측면에서 이점이 있다.

단점:

여러개의 식별자(변수)가 하나의 객체를 공유하게 되는 경우,
객체 원본과 사본 중 어느 한쪽의 프로퍼티 값을 변경하면 다른 한쪽도 변한다.

따라서 원본 객체를 유지한 상태로 값을 변경한 객체를 사용하고 싶다면 복사해야 한다. 이제 얕은 복사와 깊은 복사의 차이에 대해 알아보자.

2. 얕은 복사 vs 깊은 복사

얕은 복사(shallow copy)와 깊은 복사(deep copy)는 맥락에 따라서 의미가 달라진다. (두통 유발 원인이었지만 유어클래스를 잘 읽어보고 해소되었다!)

맥락 1) 원시값이냐 객체냐

깊은 복사

원시 값을 할당한 변수를 다른 변수에 할당하는 것
원시값 1이 할당된 변수 a를 b에 할당하는 것을 깊은 복사라고 할 때가 있다.

const a = 1;
const b = a;
console.log(a === b) // true (깊은 복사) => 값 자체를 복사

얕은 복사

객체를 할당한 변수를 다른 변수에 할당하는 것을 얕은 복사라고 할 때가 있다.

const obj = {x : 1};
const copiedObj = obj;
console.log(obj === copiedObj) (얕은 복사) => 메모리 주소만 복사

헷갈리니까 맥락1을 잠시 머릿속에서 비워내고 맥락2를 보자.

맥락 2) ⭐️참조자료형이 중첩된 경우⭐️

2개 이상의 참조자료형(객체, 배열 등)이 중첩되었다고 가정하고 설명하는 맥락이다.
내부에 중첩된 참조자료형(이하 '중첩된 객체'로 표현하겠음)이 있는 참조자료형 arr와 obj가 있다고 하자.

let arr = [1, 2, [3, 4], 5];
let obj = [{ coffee1 : "MegaCoffee"},{coffee2 : "Starbucks"}];

얕은 복사

중첩된 객체를 가진 객체를 복사하면 한 단계의 복사본만 만듦.

- 배열: slice(), spread 연산자

// slice()로 복사
const copiedArr1 = arr.slice();
console.log(arr === copiedArr1) // false
console.log(arr[2] === copiedArr1[2]) // true

// spread 연산자로 복사
const copiedArr2 = [...arr];
console.log(arr === copiedArr2) // false
console.log(arr[2] === copiedArr2[2] // true

arr의 복사본이 만들어졌지만(1단계만 복사), 중첩된 객체(배열)인 arr[2] = [3, 4]는 복사본이 만들어지지 않았다. arr[2], copiedArr1[2], copiedArr2[2]는 복사된 적이 없어 유일한 중첩된 객체를 가리키고 있다.

- 객체: Object.assign(), spread 연산자

const copiedObj1 = Object.assign({},obj);
const copiedObj2 = {...obj};
console.log(obj === copiedObj1) // false
console.log(obj === copiedObj2) // false
console.log(obj[1] === copiedObj1[1]) // true
console.log(obj[1] === copiedObj2[1] // true

obj의 복사본이 만들어졌지만(1단계만 복사), 중첩된 객체인 { coffee1 : "MegaCoffee"}와 {coffee2 : "Starbucks"}는 복사본이 만들어지지 않았다. arr[1], copiedObj1[1], copiedObj2[1]는 복사된 적 없어 유일한 중첩된 객체를 가리키고 있다.

깊은 복사

객체가 중첩되어 있을 때 중첩된 객체의 복사본까지 전부 만듦.

- JSON.parse(JSON.stringify(arr))

const copiedArr_JSON = JSON.parse(JSON.stringify(arr));
console.log(arr === copiedArr_JSON); // false
console.log(arr[2] === copiedArr1[2]); // false

❗️ 단 이 방법을 함수가 중첩되어 있는 객체에 사용하면 함수는 복사되지 않고 null로 바뀐다.

const copiedArrContainsFn_JSON = JSON.parse(JSON.stringify(["1", function(){}]))
console.log(copiedArrContainsFn_JSON) // ["1", null]

- lodash의 cloneDeep 사용(외부 라이브러리)

npm install lodash

깊복을 원한다면 lodash를 설치하자!! (node.js 환경에서 실행)

const copiedArr_JSON = JSON.parse(JSON.stringify(arr));
console.log(arr === copiedArr_JSON); // false
console.log(arr[2] === copiedArr1[2]); // false

중첩된 객체인 arr[2]까지 모두 복사되었다.

틀린 부분이 있다면 지적 감사하겠습니다 ~~!!

profile
기록에 진심인 개발자 🌿

0개의 댓글