기본을 알아야 깊은 복사와 얕은 복사를 정확히 이해할 수 있다.

이주영·2022년 12월 18일
1

Javascript

목록 보기
5/11
post-thumbnail

학습 배경

클린코드 강의를 듣고 객체를 불변하게 다루는 방법에 대해 공부를 하고 있었습니다. 그러던 와중 얕은 복사깊은 복사 의 차이를 잘 모르고 있었다는 것을 깨닫게 됐고 찾아보기 시작했습니다. 그러던 와중 어느 글에서는 '얕은 복사와 깊은 복사의 차이가 동일한 참조값를 공유하고 있는가있는가….'이고 어느 곳에서는 '복사되는 depth에 따라 차이를 불 수 있다'라고 했습니다. 그 당시 제 머릿속에서 충돌이 일어났습니다. 이 글을 읽는 분들도 헷갈리셨더라면 이 글을 통해서 명확하게 이해하시길 바랍니다.

우선 얕은 복사와 깊은 복사를 이해하기 위해서 사전에 알아야 하는 개념들이 있습니다.

서론 (사전 지식)

1.원시타입과 참조타입인 자바스크립트 데이터 타입

  • 원시형
    - undefined ,null, Boolean, Number, String, Symbol
  • 참조형
    - Object : new Object Array, Map, Set, Weak map, Date
    - Function

여기서 우리가 기억해야 하는 것은 "원시형 타입은 불변하다" 입니다.
그렇기에 원시형을 복사할 땐 걱정할 필요가 없습니다. Why?? 원시형 데이터 타입은 불변하기에 값을 바꾸지 못합니다. 만약 값을 바꾼다면 아예 새로운 메모리를 할당받아서 새로운 값을 만들기 때문입니다.

그에 반해 참조형은 가변합니다. 참조형 타입은 새로운 값이 만들어지지 않고 직접적으로 변경이 가능합니다.

원시형은 불변하고 참조형은 가변하구나...

를 알고 있어야 그다음으로 깊은 복사와 얕은 복사 간에 정의와 차이를 더욱 명확히 이해할 수 있습니다.

2. 참조형 타입의 객체나 배열을 조금 더 살펴보자

const array = [1,2,3]
const sameElementArray = [1,2,3]

console.log(array === sameElementArray) // false


const Object = {x : 1}
const sameElementObject = {x : 1}

console.log(Object === sameElementObject) // false

위의 예시를 통해서 다시 확인할 수 있는 것은 원시형은 실제 값을 메모리에 적재되지만, 참조형은 reference가 저장된다는 것을 역으로 생각해보면 이해할 수 있습니다.

위의 예시를 살펴보면 언뜻 보기에 동일한 요소들을 포함한 객체나 배열이 변수에 할당되어있기에 동일하다고 느껴질 수 있지만 참조형 타입은 주소를 메모리에 저장하기에 객체나 배열 안에 있는 값의 동일 여부를 보지 않고 새롭게 만들어진 객체나 배열은 다른 것으로 판단합니다.

이 기본적인 부분을 이해하면 이후 얕은 복사에서 한 단계만 불변하게 복사되는 이유를 정확히 이해할 수 있습니다.

본론 : 깊은 복사와 얕은 복사 정리

다양한 정의를 살펴보았지만 역시 MDN 정의가 가장 신뢰할 수 있었습니다.

깊은 복사

(위의 사진의 오른쪽에 해당합니다!)
deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values)

동일한 참조값을 공유하지 않는다는 것이 핵심이며 동일한 참조값을 공유하고 있지 않기 때문에 서로의 공간에서 값을 바꾼다고 할지라도 영향을 끼치지 않게 됩니다.

얕은 복사

(위의 사진의 왼쪽에 해당합니다!)
shallow copy of an object is a copy whose properties share the same references (point to the same underlying values)

동일한 참조값을 공유하여 서로 같은 values를 가르키고 있습니다.

여기서 잠깐 원시형 데이터 타입을 복사할 경우는???

const string = 'hi '

let copiedString = string 

copiedString = 'bye'

console.log(string) // hi

원시형은 사전 지식 섹션에서 말했듯, 불변하다는 특징을 가지고 있습니다. 그렇기 때문에 서로 영향을 끼치지 않습니다.

객체를 복사하는 방법들을 살펴보자!

1. Spread operator (ES6)

const a = {
  en : 'Bye',
  de : 'Hi',
  text : 'text',
}

let b = {...a}

b.de = 'ju'
console.log(a.de) // 'Hi'

const c = {...a ,...b}

console.log(c)  // { en: 'Bye', de: 'ju', text: 'text' }

이번 블로그의 핵심 궁금증

Q. 얕은 복사는 한단계의 복사를 의미하는건가?
언뜻 보면 얕은 복사가 한 단계의 복사만 이루어지는 것같이 보이기에 그렇게 생각하고 넘어갈 수 있다고 생각합니다. 이 부분이 제가 얕은 복사와 깊은 복사를 이해하는 데 있어 헷갈리게 했던 부분입니다.

const a = {
  x : 'Hi',
  y : 'Hello',
  z : 'Sup',
}

let b = {...a}

A. 얕은 복사와 깊은 복사의 개념에서 잠시 내려놓고 간단하게 생각해보니 이해가 됐습니다.

예제를 보면, a라는 변수와 b라는 변수는 각각 다른 객체입니다. Why? object literal 방식으로 각각 다른 변수에 할당돼 서로 다른 메모리 주소를 가지고 있기 때문입니다. 그래서 현재 두 개의 변수 간에 영향을 끼치지 않는 상황입니다.

하지만 만약

const a = {
  x : 'Hi',
  y : 'Hello',
  z : 'Sup',
  m : { a : 'bye'}
}

let b = {...a}

새로운 객체인 b에 spread operator를 활용하여 a의 properties를 가지고 올 경우, 원시형 데이터 타입인 x, y, z를 복사하면 아예 새로운 값으로 변경되는 반면 우리가 위에서 배웠던 대로 참조형 데이터 타입은 주소만을 가지고 있기 때문에 서로 다른 두 객체 안에서 동일한 주솟값을 가지고 있게 됩니다. 바로 properties 중에 m을 말하고 있습니다. 그래서 결과적으로 a, b의 x,y,z를 수정 혹은 삭제해도 서로 영향을 끼치지 않지만 (one depth) m 의 값을 추가, 삭제할 경우 서로 동일한 참조를 가지고 있기에 서로 영향을 끼친다고 할 수 있습니다.

즉 얕은 복사는 한 단계만 복사한다. 깊은 복사는 전체를 복사한다는 설명은 지금의 제 시각으로는 완벽한 정의는 아니다! 라고 생각합니다.

동일한 참조 값을 가르키고 있는가 아닌가에 중점을 두고 있다고 생각합니다.

2. Object.assign

spread operator이 나오기 전에 가장 많이 사용됐던 방법

const a = {
  en : 'Bye',
  de : 'Hi',
  text : 'text',
}

let b = Object.assign({},a)

b.de = 'ju'
a.de

3. Pitfall : Nested Objects

중첩된 객체나 배열을 복사하면 중첩된 객체, 그 객체는 복사가 되지 않는다. 왜냐하면 중첩된 객체는 참조값만 가지고 있기 때문이다. (위에서 자세하게 설명했습니다. 햇갈리면 참고해주세요~ )

그래서 중첩객체를 deep copy(서로 다른 참조를 하도록) 하기 위해서는 수동적으로 모든 중첩 객체를 복사해주는 방법이 있습니다.

const a = {
  foods : {
  dinner : 'pasta'
}
}

let b = {foods:{...a.foods}}

b.foods.dinner = 'Soup'

a.foods.dinner

4. 위의 같이 어렵게 하지 않고 쉽게 deep copy를 하는 방법

단순하게 객체에 stringify를 해주고 parse를 바로 해주면 어렵지 않게 객체의 모든 중첩된 객체까지 자동적으로 copy를 해줍니다.

const a = {
  foods: {
    dinner: 'Pasta'
  }
}
let b = JSON.parse(JSON.stringify(a))

b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

배열의 복사를 알아보면

자바스크립트에서는 배열은 객체입니다. 이 내용은 이번 블로그에서는 넘어가겠습니다. 배열의 복사를 알아보면

1. spread operator

const a = [1,2,3]
let b = [...a]

b[1] = 4

console.log(b[1]) // 4
console.log(a[1]) // 2

이부분이 나에게 미지수!!
Q. 얕은 복사는 한단계의 깊은 복사를 의미하는건가?
-> 해결했습니다. 이게 제가 공부하면서 혼란스러웠던 것인데 위에 객체 복사하기 에서 설명을 했습니다.

2. 고차 함수 (map, filter, reduce)

세개의 메소드는 원본의 모든 요소들을 가진 새로운 배열을 반환하는 특징이 있습니다. 원본에 영향을 끼치지 않는 새로운 배열을 만들 수 있다.

3. Array.slice

const a = [1,2,3]
let b = a.slice(0)
b[1] = 4
console.log(b[1]) // 4
console.log(a[1]) // 2

4. Nested arrays

객체와 마찬가지로 위의 3개의 방법으로 복사할 경우 배열 안에 있는 객체 혹은 배열을 복사하지는 못합니다. 얕은 복사만 가능하다고 합니다. 그래서 막기 위해 또 등장한 stringify -> parse

JSON.parse(JSON.stringify(someArray))

결론

얕은 복사와 깊은 복사가 왜 갑자기 헷갈렸을까 생각해보면 기본적인 내용들을 모르고 있었기 때문이라고 생각이 듭니다. 그래서 내가 무엇을 모르고 있어서 이해가 잘 안 가는지 찾아가는데 재미가 있었던 시간이었습니다.

마지막으로 정리해보면

copy = {...array} 라는 것이 copy라는 변수에 새로운 객체를 만들어줌으로써 아예 다른 메모리를 갖게 되는 변수를 만들어준 것이고 그 안에 spread 문법을 활용해서 array에 있는 요소만 쓱 넣어준 것이기 때문에 요소 안에 있는 불변하다는 특징을 가지고 있는 원시형 당연하게 복사한 객체나 배열에서 값을 바꿔도 원본에 영향을 끼치지 않고 대신 참조형인 객체나 배열이 있을 경우 참조값만 가지고 있기 때문에 copy에서 값을 바꾼다면 원본 array에도 영향을 미치게 됩니다. 그래서 한 단계만 불변하게 복사가 된다고 정리할 수 있습니다.

profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

1개의 댓글

comment-user-thumbnail
2022년 12월 19일

👍

답글 달기