전개구문과 얕은 복사 & 깊은 복사
전개구문
ES6 에 추가된 문법으로 '...' 으로 표시하며, 구조분해할당과 함께 사용할 수 있고, 배열과 객체 등에 할당된 값을 전개해서 사용한다.
참조 자료형과 얕은 복사 & 깊은 복사
- 얕은 복사
참조 자료형에 대한 내용은 이전 게시물에서 다뤘기 때문에 이전 게시물을 참고바란다.
배열, 객체 등 참조 자료형은 원시 자료형에 비해 메모리 공간을 많이 차지하므로 데이터의 효율적인 관리를 위해 각각의 다른 변수에 할당하더라도 같은 메모리 공간을 참조한다.
아래의 예시를 보자.
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 이라는 메소드를 제공하고 있다고 한다.
라이브러리를 설치해야 해서 귀찮은 점이 있지만 필요하다면 사용해봐도 좋을 것 같다.