Javascript 깊은 복사의 함정

ashnamuh·2019년 6월 26일
5

think about js

목록 보기
4/5
post-thumbnail

오늘 자바스크립트의 얕은 복사(shallow copy), 깊은 복사(deep copy)에 관해서 써보고자 한다.

공부해보니 내가 알던 깊은 복사에 커다란 함정이 있었고, 내가 알던 깊은 복사의 기능이 맞는지 의구심이 들었다.

그래서 제목을 자바스크립트 깊은 복사의 함정으로 지었다.

내가 알던 얕은 복사와 깊은 복사

우선 이를 이해하려면 자바스크립트의 value-typereference-type을 이해해야한다.

https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0

value typereference type에 대한 이해가 모자라다면 위 링크를 한번 읽어보기 바란다.

객체는 기본적으로 참조 타입(reference type)이다.

참조 타입의 동작은 어떻게 다를까? 다음 코드를 보자

let valueType1 = 1
let valueType2 = valueType1

valueType1 = 3
console.log(valueType2) // 1

let referenceType1 = {a: 1}
let referenceType2 = referenceType1

referenceType1.a = 3
console.log(referenceType2) // {"a": 3}

위 코드를 보면 =로 할당을 했을 때 값 타입의 경우 1이라는 값 자체를 복사했다.

그래서 원본 값이 수정되더라도 복사된 값은 변하지 않는다.

하지만 객체의 경우 똑같이 =을 사용해서 할당했고 원본 객체를 수정했다.

하지만 referenceType2의 값도 변경되었다.

왜 이런 동작이 발생할까?

바로 객체는 참조 타입(reference type)이기 때문이다.

객체를 =로 복사할 때 값 자체를 복사하는게 아닌, 메모리 주소값 참조를 복사한다.

결국 referenceType1referenceType2은 같은 메모리를 사용한다.

참고로 ==, ===로 객체를 비교할 땐 참조가 같은지 비교한다.

똑같이 {a: 3}라는 값을 가진 객체가 있더라도 메모리 참조가 다르면 다음 코드를 false를 출력한다.

다음 코드는 true를 출력한다.

console.log(referenceType1 === referenceType2)

나는 이렇게 객체를 단순히 =로 복사하는걸 얕은 복사(Shallow copy)라고 이해하고 있다.

그러면 객체를 참조가 아닌 완전히 복사하려면 어떻게 해야할까?

Javascript의 Object.prototype.assignspread operator를 사용하면 된다.

다음 코드를 보자.

let obj1 = {foo: 'bar'}
let obj2 = Object.assign({}, obj1)
let obj3 = {...obj1}

obj1.foo = 'hello'
console.log(obj2) // {"foo": "bar"}
console.log(obj3) // {"foo": "bar"}

console.log(obj1 === obj2) // false
console.log(obj1 === obj3) // false
console.log(obj2 === obj3) // false

Object.prototype.assign은 ES6에 새로생긴 문법이다. spread operator도 ES6 이후에 생긴 문법인데, 정확이 언제인진 잘 모르겠다.

이 두가지 외에도 객체를 스트링으로 바꿔서 할당 후 다시 객체로 바꾸는 방법도 ES5 이하에선 쓰였지만 이제는 추천하지 않는다.

두가지를 이용해서 객체를 복사하면 원본 객체인 obj1를 수정해도 obj2, obj3의 값은 변하지 않는다.

===로 비교해봐도 세가지 객체 모두 다른 메모리 참조값을 가지고 있는걸 확인할 수 있다.

나는 이렇게 참조값이 아닌 값 자체를 복사하는 걸 깊은 복사(Deep copy)라고 이해하고 있다.

함정..

함정에 빠지기 쉬운게 깊은 복사라도 완전히 객체의 모든 걸 복사하진 않는다.

다음 코드를 보자.

let obj4 = {
  a: 1,
  innerObj: {
    b: 3
  }
}
let obj5 = {...obj4}

obj4.a = 6
obj4.innerObj.b = 6
console.log(obj5) // { "a": 1, "innerObj": { "b": 6 } }

console.log(obj5 === obj4) // false
console.log(obj4.innerObj === obj5.innerObj) // true

obj4는 내부에 innerObj라는 객체가 있다.

이를 깊은 복사를 이용해도 innerObj 의 경우 같은 참조를 가리킨다.

왜 이럴까?

답은 간단하다. depth가 있는 객체도 결국은 참조타입이기 때문이다.

하위 객체는 완전히 복사된게 아니다.

이것을 깊은 복사(Deep copy)라고 할 수 있을까?

하위 객체까지 완전히 복사하는건 real deep copy라고 불러야하나?

다른 이 개념에 대해 다시 공부하면서

깊은 복사든 얕은 복사든 용어는 잊어버리기로 했다.

그냥 원래 기능만 생각하기로 했다.

실제 개발을 하다보면 depth가 깊게 들어간 객체를 은근 쉽게 만날 수 있다.

이를 어떻게 해결해야할까?

이론상으로 쉽다.

깊이가 얼마나 깊든 재귀적으로 깊은 복사를 하면 된다.

근데 자체적으로 재귀적인 깊은 복사 함수를 깊은 복사 함수를 만들어도

성능상 좋은지 잘 확인할 수 있을까?

간단하지만 작업이지만 자체 구현은 귀찮은 작업이기도 하다.

나는 이런 상황에 직면하면 오픈소스를 잘 활용하자는 생각이다.

lodash를 적극 추천한다.

lodash는 자바스크립트 고차함수 집합 및 함수형 라이브러리이다.

깊이가 얼마나 깊은 객체를 완전 복사하려면 lodash의 _.cloneDeep을 사용하길 추천한다.

lodash엔 이미 _.clone이 있는데, 이를 재귀적으로 수행해주는 함수다.

깊은 복사 외에도 유용한 함수가 많다.

Javascript 개발자라면 이미 많이 사용하고 있을것이다.

lodash는 아래 링크를 참고하길 바란다.

https://lodash.com/docs/4.17.11#cloneDeep

결론: 완전히 객체를 복사라혀면 lodash 같은 라이브러리의 cloneDeep 등의 함수를 이용하자. 또는 재귀적인 clone 함수를 직접 구현하자

profile
프론트엔드 개발자입니다

7개의 댓글

comment-user-thumbnail
2019년 7월 31일

깊은 복사의 함정보다는 spread 연산자의 한계로 와닿는 포스트네요

1개의 답글
comment-user-thumbnail
2019년 8월 21일

spread 연산자는 원래 기능이 얕은 복사아닌가요 ??

1개의 답글
comment-user-thumbnail
2019년 10월 16일

spread operator 나 object.assign 을 이용하는 복사가 얕은 복사라고 기억하시면 될것같아요.

위에 얕은복사 예시는 사실 얕은 복사개념이 아니라 그냥 별명을 짓는 것이고요.
C에서 포인터와 포인터주소 개념이 묻어나온것이라 이걸 이해하면 도움이 될것 같습니다.

얕은 복사라도 어찌되었던간에 대표 참조 값을 바꾸는것이다보니, 리액트나 vue등의 프레임웍에서는 어바뀐건가보다 하고 알아차리긴 하거든요;;

물론 데이터를 이뮤터블하게 처리해야하는 데이터 객체입장에선 만부당천부당 할 일인데, 이걸 잘 구별해야하죠;

1개의 답글
comment-user-thumbnail
2019년 12월 28일

얕은복사와 깊은복사 의 개념이 없던 제게는
정말 좋은 글이었습니다
감사합니다 ~

답글 달기