코딩 공부 일지/JavaScript

전개구문과 얕은 복사 & 깊은 복사

헬로코딩 2022. 3. 22. 00:46
728x90

전개구문


ES6 에 추가된 문법으로 '...' 으로 표시하며, 구조분해할당과 함께 사용할 수 있고, 배열과 객체 등에 할당된 값을 전개해서 사용한다.

참조 자료형과 얕은 복사 & 깊은 복사

- 얕은 복사

참조 자료형에 대한 내용은 이전 게시물에서 다뤘기 때문에 이전 게시물을 참고바란다.

원시 자료형과 참조 자료형

원시 자료형과 참조 자료형 원시 자료형(Primitive Data Type) 원시 자료형은 객체가 아니면서 동시에 메소드도 가지지 않는 자료형을 말하며, 다음의 자료형을 말한다. string, number, bigint, boolean, undefin

babycoder05.tistory.com


배열, 객체 등 참조 자료형은 원시 자료형에 비해 메모리 공간을 많이 차지하므로 데이터의 효율적인 관리를 위해 각각의 다른 변수에 할당하더라도 같은 메모리 공간을 참조한다.

아래의 예시를 보자.

const array = ['a', 'b', 'c']
const newArray = array
newArray.push('d')
console.log(array) // ['a', 'b', 'c', 'd']
console.log(newArray) // ['a', 'b', 'c', 'd']
array.push('e')
console.log(array) // ['a', 'b', 'c', 'd', 'e']
console.log(newArray) // ['a', 'b', 'c', 'd', 'e']
console.log(array === newArray) // true


새로운 변수인 newArray에 기존 배열 array를 할당하면 두 변수는 같은 메모리 저장 공간을 참조하게 된다. 그래서 각각에 데이터를 다르게 추가하더라도 두 배열의 데이터는 같다. 이를 '얕은 복사'라고 한다.

- 깊은 복사

각각의 변수에 모양이 같은 배열을 각각 다르게 할당해서 관리하고 싶다면 assign() 메소드를 사용할 수도 있지만, ES6 문법인 전개구문을 이용할 수도 있다.

아래의 예시를 보자.

const array = ['a', 'b', 'c']
const newArray = [...array]
console.log(array === newArray) // false
newArray.push('d')
console.log(array) // ['a', 'b', 'c']
console.log(newArray) // ['a', 'b', 'c', 'd']
array.push('e')
console.log(array) // ['a', 'b', 'c', 'e']
console.log(newArray) // ['a', 'b', 'c', 'd']

array에 할당된 배열을 전개구문을 이용해 newArray에 할당했다. 그리고 나서 그 둘이 같은지를 비교해보면 false가 나온다. 둘은 같은 메모리 공간을 참조하고 있지 않다. 그 이후에 각각의 배열에 데이터를 각각 추가해봐도 서로 연관이 없이 각각의 배열 데이터를 가지고 있는 것을 확인할 수 있다. 이를 '깊은 복사'라고 한다.

- 얕은 복사와 깊은 복사를 신경써야 하는 이유?

어떤 참조 자료형 데이터가 여러 함수에서 쓰이는 상황을 가정해보자. 그렇다면 원본 데이터는 보존하고 사용하는 함수에서만 데이터를 변형해야할 수도 있다.

아래의 예시를 보자.

const array = ['a', 'b', 'c']
const newArray = array.reverse()
console.log(array) // ['c', 'b', 'a']
console.log(newArray) // ['c', 'b', 'a']
const array = ['a', 'b', 'c']
const newArray = [...array].reverse()
console.log(array) // ['a', 'b', 'c']
console.log(newArray) // ['c', 'b', 'a']

첫 번째 예시는 전개구문을 사용하지 않고 직접적으로 배열을 할당한 경우이고, 두 번째 예시는 전개구문을 사용해서 새롭게 배열을 할당한 경우다.
array가 원본 데이터고 원본 데이터를 훼손시키지 말아야 한다고 가정해보자. (예를 들어, 다른 함수에서도 같은 데이터가 쓰이는 경우) 첫 번째 예시에서는 array, newArray 두 변수가 같은 메모리를 참조하고 있으므로, newArray에만 reverse() 함수를 사용해서 할당했어도 array newArray 모두 데이터가 변한다.
두 번째 예시에서는 전개구문을 이용해서 새로운 메모리에 변형된 데이터를 할당했으므로, array와 newArray의 데이터가 다른 것을 확인할 수 있다.

응용

- 객체도 전개구문을 통한 깊은 복사가 가능하다.

객체도 마찬가지로 전개구문을 통한 깊은 복사가 가능하다.

아래의 예시를 보자.

const obj = {name: 'strawberry', color: 'red'}
const newObj = obj
console.log(obj) // {name: 'strawberry', color: 'red'}
console.log(newObj) // {name: 'strawberry', color: 'red'}
newObj.name = 'banana'
newObj.color = 'yellow'
console.log(obj) // {name: 'banana', color: 'yellow'}
console.log(newObj) // {name: 'banana', color: 'yellow'}
console.log(obj === newObj) // true
const obj = {name: 'strawberry', color: 'red'}
const newObj = {...obj}
console.log(obj === newObj) // false
console.log(obj) // {name: 'strawberry', color: 'red'}
console.log(newObj) // {name: 'strawberry', color: 'red'}
newObj.name = 'banana'
newObj.color = 'yellow'
console.log(obj) // {name: 'strawberry', color: 'red'}
console.log(newObj) // {name: 'banana', color: 'yellow'}

배열과 마찬가지로 전개구문을 통해서 복사를 했을 때 깊은 복사가 일어난 것을 확인할 수 있다.

- 전개구문을 이용한 깊은 복사의 함정

이러한 깊은 복사도 같은 레벨의 데이터만 깊은 복사를 한다는 것이 함정이다.

예시를 보면 이해가 될 것이다.

const obj = {name: 'strawberry', color: {leaf: 'green', body: 'red'}}
const newObj = {...obj}
console.log(obj === newObj) // false
console.log(obj.color.leaf === newObj.color.leaf) // true

객체 안의 하위 레벨 객체가 또 있는 형태다. 이 경우에 전개구문으로 복사를 하더라도 같은 레벨만 깊은 복사가 되고 하위 레벨인 leaf 에는 얕은 복사가 일어난 것을 확인할 수 있다. newObj.color.leaf 의데이터를 변경할 경우 원본 데이터도 변경된다는 말이다.

하위 레벨까지 깊은 복사를 하기 위해서는 재귀적으로 계속해서 깊은 복사를 해야한다. 그러나 이 방법은 레벨이 깊으면 깊을 수록 여간 복잡하고 귀찮은 일이 아닐 수 없다.

이를 위해 Lodash 라는 라이브러리에서는 깊은 복사를 위한 cloneDeep 이라는 메소드를 제공하고 있다고 한다.
라이브러리를 설치해야 해서 귀찮은 점이 있지만 필요하다면 사용해봐도 좋을 것 같다.

728x90