개발 블로깅/오늘의 TIL

[2020.04.26] Javascript 깊은 복사의 함정... 모르고 사용하다 뒤통수 맞았다...

Hello이뇽 2020. 4. 26. 22:32

 

사이드 프로젝트 진행 중, 내게 엄청나게 스트레스를 안겨준 요소가 있었다.

Javascript의 깊은 복사의 함정..

우선 이 깊은 복사의 함정을 설명하기 위해서, Object 복사의 개념에 대해 간단히 설명을 해보려고 한다.

 

Javascript의 복사 개념

얕은 복사(Shallow copy)

우리가 흔히 알고 있듯이, 객체가 담겨있는 변수를 다른 변수에 할당하면 call by reference (데이터 복사가 아닌 참조)가 일어나게 되어, 한 변수의 데이터를 변경하면 다른 변수의 데이터도 함께 변경이 된다.

const person1 = {name:'inyong'};

const person2 = person1;

person1.name = 'jung';

// result

person2.name // 'jung'

person1 === person2; // true - 같은 데이터 주소를 바라보고 있는 두 변수

 

데이터가 그대로 하나 더 생성된 것이 아닌 해당 데이터의 메모리 주소를 전달하게 돼서, 결국 한 데이터를 공유하게 되는 것이다.

 

깊은 복사(Depth copy)

한 데이터의 공유가 아닌, 똑같은 구조의 객체를 하나 더 생성하여 따로 사용하고자 할 때가 있다.

이럴 때 우리는 '깊은 복사'라는 개념을 사용한다.

const person1 = { name: "inyong" };

const person2 = Object.assign({}, person1);

person1.name = "jung";

// result

person2.name // 'inyong' - 전혀 다른 메모리 주소의 데이터이므로, person2의 값은 변하지 않음.

person1 === person2 // false - 형태만 같고, 각자 다른 메모리 주소에 저장되어 있는 데이터이다.

데이터 참조가 아닌 객체의 형태를 그대로 복사하게 함으로써, 한 객체가 변경되도 다른 객체의 데이터에는 영향이 없게 된다.

 

깊은 복사(Depth copy)를 하는 일반적인 방법

Object.assign()

Object.assign()은 ES6인가 ES7인가...Object 형태의 데이터를 쉽게 병합할 수 있게 해주는 함수이다.

const originObj = {a:1};

const newObj = Object.assign({}, originObj); // 빈 Object에 originObj를 병합하여 반환.

// result

newObj // {a:1} 

originObj === newObj // false

 

빈 Object에다가 복사를 하려는 Object를 병합한다. 그러면 형태는 originObj이지만 실제로는 빈 Object와 originObj가 병합된 새로운 Object가 반환된다.

 

전개 연산자 (Spread Operation)

ES6에서 새롭게 추가된 Javascript 문법이다. 

const Obj1 = {a:1, b:2};

const Obj2 = {c:3};

const Obj3 = {...Obj1, ...Obj2};

// result

Obj3 // {a:1, b:2, c:3}

...(쩜쩜쩜)을 이용하여, 배열이나 Object 형태의 괄호를 이용하여 풀어서(?) 사용해버리는 방식이다. 

Object 뿐 아니라, 배열도 이와 같은 방식으로 사용할 수 있다.

 

전개 연산자를 이용한 깊은 복사를 하려면, 아래와 같은 방법으로 할 수 있다.

const Obj1 = {a:1, b:2};

const Obj2 = {...Obj1};

// result

Obj2 // {a:1, b:2}

Obj1 === Obj2 // false

 

그러나 이러한 깊은 복사를 온전히 믿고 사용하다간 뒤통수 맞는다...

왜냐하면, 이 깊은 복사는 현재의 Depth 이상으로는 깊은 복사를 하지 않기 때문이다.

 

깊은 복사(Depth copy)의 함정

예시를 들어서 보도록 하자.

const Obj1 = { a: { b: 1 } };

const Obj2 = { ...Obj1 };

// result

Obj2 // { a: { b: 1 } }

Obj1 === Obj2 // false

 

Depth가 2인 Object를 전개연사자를 이용하여 Obj2 변수에 할당했다. 

확인해보면 Obj1과 Obj2는 형태가 똑같다. 그리고 === 연산자를 이용해 확인해보면, 각자 다른 메모리 주소에 저장된 데이터로 제대로 깊은 복사가 된 것처럼 보인다.

그러나 깊은 복사가 된 것은 제일 바깥의 Depth뿐이다.

const Obj1 = { a: { b: 1 } };

const Obj2 = { ...Obj1 };

// result

Obj2 // { a: { b: 1 } }

Obj1 === Obj2 // false

Obj1.a === Obj2.a // true...!!

 

두 번째 Depth 이상의 요소들은 참조 값을 전달하는... 즉, 얕은 복사(Shallow copy)를 하게 되는 것이다.
(Object.assign()을 이용해도 동일)

 

완벽한 깊은 복사를 하는 방법

재귀적으로 깊은 복사를 수행

깊은 복사를 하려는 형태에 맞게 재귀 함수를 만들어서 복사를 해야한다. 여간 귀찮은 일이 아니다...

사용하는 Object의 Depth가 길어질수록 Time Complexity(시간 복잡도)도 늘어나게 된다.

 

Lodash의 cloneDeep 함수 사용

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

Lodash의 cloneDeep 함수를 사용하면, 완벽하게 깊은 복사를 할 수 있다.

 

JSON.parse()와 JSON.stringify()함수 사용

JSON.stringify 함수를 이용해서, Object 전체를 문자열로 변환 후, 다시 JSON.parse 함수를 이용해서 문자열을 Object 형태로 변환한다. 

그러면 문자열로 변환하는 순간 참조 값이 끊기기 때문에 새로운 Object로 만들어 사용할 수 있다. 

하지만 JSON 함수는 엄청나게 리소스를 잡아먹는 함수인 만큼, 성능이 좋지 않은 부분을 고려해야 한다.


 

나는 이 함정으로 인해, 반나절이란 시간을 허황 없이 버리고 말았다...

데이터를 변경하지도 않았는데 마음대로 변경이 되어 있고, 그 원인은 당최 찾아봐도 알 수가 없었다.

 

깊은 복사를 굳게 믿었기 때문에...

 

반응형