자바스크립트 얕은 복사와 깊은 복사 이야기

YooSeok2·2022년 10월 20일
0
post-thumbnail

개요

자바스크립트에서 값을 복사하여 할당하는 일에는 얕은 복사와 깊은 복사로 나뉜다. 두 개념을 이해하지 못한 채로 프로그래밍을 하다보면 분명 예상치 못한 결과에 난감해하는 순간이 생길것이다. 이 글을 읽고 미리 대비할 수 있기를 바란다.

시작하기에 앞서 몇 가지 복사에 대한 예시를 보자

예제1

let a = 'apple';
let b = a;
b = 'banana';
console.log(a === b); // false	

우리가 예상한대로 a와 b는 다르다는 결과를 확인할 수 있다. 그럼 이어서 아래의 코드를 보자

예제2

let obj = {
  a: 'apple'
};
let obj2 = obj;
obj2.a = 'banana';
console.log(obj2.a); // banana
console.log(obj.a);  // banana
console.log(obj.a === obj2.a); // true

뭔가 이상하다. 분명 새로운 변수 obj2를 선언해서 obj를 복사하였고 obj2의 a값만 변경하였는데 왜 obj의 값도 변경되어져 있을까? 그 이유는 자바스크립트는 원시형 데이터와 참조형 데이터를 메모리에 다르게 관리하게 때문이다. 참고로 원시형 데이터에는 정수, 문자열, 심볼, 불리언, undefined, null이 있고 참조형 데이터는 객체, 배열, 함수가 대표적이다.

원시형 데이터 메모리 관리

예제1 메모리 할당 도식화


예제 1의 코드를 자바스크립트에서 어떻게 메모리 할당하는지 도식화한 그림이다. 살펴보면 먼저 변수 a가 선언되면 자바스크립트는 식별자 메모리 공간을 만들고 할당되는 값의 메모리 공간을 또 만든다. 이렇게 따로 메모리 공간을 만드는 이유는 값의 재할당, 동일한 값을 사용하는 변수에 데이터 재활용 등의 여러 상황에 효율적으로 관리하기 위함이다. 이어서 식별자 b의 변수 영역 메모리 공간도 생성하고 a의 값을 할당하면서 최초에 b에는 @5001주소에 있는 "apple"값을 가졌을 것이다. 하지만 이후 값을 재할당 하면서 새로운 값인 "banana"를 위한 메모리 공간이 만들어지고 메모리 주소인 @5003을 가지고 식별자 b를 찾아서 @5001를 @5003으로 변경한다.

이렇듯 변수가 참조하는 메모리 주소에는 값이 들어있어서 값 그대로를 전달받고 사용하게 되는데 이를 PassByValue라고 부른다.

참조형 데이터 메모리 관리

예제2 메모리 할당 도식화


이제 예제2를 살펴보자. 위의 그림은 참조형 데이터를 메모리에 할당했을 때를 도식화한 그림이다. 변수 obj을 선언하면 변수, 데이터 영역 메모리가 생성되고 원시형과 다르게 하나의 메모리가 더 생성되는데 이곳은 obj의 프로퍼티를 위한 메모리 공간이다. 식별자 obj가 등록되면 값을 저장하기 위한 주소 @5001을 확보한다. 이 주소에는 @7002~?라는 또 다른 메모리 주소가 값으로 저장되어 있는데 obj객체의 프로퍼티들이 저장되어 있는 주소이다. obj의 프로퍼티 a는 @7002 주소를 배정받고 할당된 값이 있는 주소 @5003을 가지게 된다. @5003에는 "apple" 값이 있다. 여기서 obj2에 obj를 할당하면 obj의 프로퍼티로 접근하는 메모리 주소를 갖게된다.

이렇듯 변수가 참조하는 메모리 주소에 또 다른 메모리 주소가 값으로 들어있어 주소를 전달받고 사용하게 되는데 이를 PassByReference라고 부른다. 또한 단순히 obj2에 obj를 할당하는 행위를 얕은 복사(ShallowCopy)라 한다.

얕은 복사

얕은 복사는 위에서 언급했듯이 메모리 주소를 공유하기 때문에 복사한 객체를 수정 할 때 원래 원본 객체에도 영향을 끼친다. 모르고 쓸 경우 개발자의 의도와는 다른 결과를 초래할 수 있다는 단점이 있지만 메모리에 새로운 공간을 만들고 할당하지 않아도 된다는 아주 큰 장점을 가지고 있어서 대체로 많이 사용되는 복사 방법이다.

깊은 복사

깊은 복사얕은 복사와 다르게 메모리 참조 주소를 공유하지 않고 각각 독립된 메모리 주소를 갖도록 복사한다. 앞서 얕은 복사의 단점으로 지목되었던 복사 객체가 원본 객체에게 영향을 끼치는 일은 없지만 복사된 객체를 위한 새로운 메모리가 만들어지기 때문에 효율성이 떨어진다는 치명적인 단점이 있어서 잘 사용되지 않는 방법이다.

복사하는 방법

1. Array.prototype.slice

slice(start, end)함수는 배열을 복사할 때 많이 사용하는 함수이다. start부터 end까지 대상 배열을 꺼내와 새로운 배열을 만든다. start와 end가 주어지지 않으면 전체를 복사한다.

// 원시형 데이터만 있을 경우
let arr = [1,2,3];
let arr2 = arr.slice();

console.log(arr === arr2) // 출력 결과: false

// 배열 안에 객체가 있을 경우
let arr = [1,2,[3,4]];
let arr2 = arr.slice();

arr2[2].push(5);

console.log(arr[2] === arr2[2]) // 출력 결과: true

예제에서 주목할 점은 원시형 데이터로만 이루어진 배열과 내부에 다른 객체가 포함되어 있는 배열을 복사할 때의 차이이다. 원시형 데이터 배열을 복사할 때에는 깊은 복사가 이루어지는 반면 중첩 구조의 배열을 복사하면 얕은 복사를 수행한다.

2. Object.assign

Object.assign(target, ...sources) 메서드는 target 객체에 sources 객체를 복사하여 반환해준다. 객체({}, [])를 복사하는 용도로도 자주 쓰인다.

let arr = [1,2,[3,4]];
let arr2 = Object.assign([], arr);

console.log(arr === arr2) // 출력 결과: false

// 배열 안에 객체가 있을 경우
let arr = [1,2,[3,4]];
let arr2 = Object.assign([], arr);

arr2[2].push(5);

console.log(arr[2] === arr2[2]) // 출력 결과: true

결과는 얕은 복사는 깔끔하게 잘 처리되지만 아쉽게도 깊은 복사는 제대로 수행하지 못하고 있다.

3. Spread Operator

es6문법에서 새로 추가된 펼침 연산자(Spread Operator)로 복사가 가능하다. 사용법이 굉장히 간단하다. 원본 객체의 접두어에 ...를 붙여주면 끝이다. 대신 사용하기 위한 조건이 있는데 iterable한 객체 즉 iterlator여야 한다.

Iterable과 Iterator

이터러블은 반복 가능한 객체를 의미하는데 객체 내에 Symbol.iterator 메서드가 있어야 하고, 이는 Iterator를 반환해야 한다.

이터레이터는 이터러블한 객체에서 반복을 수행하는 반복기이다. 이터레이터가 되기 위해서는 Symbol.iterator에서 반환한 Iterator에 next()함수가 반드시 있어야 하며 이를 통해 데이터에 접근할 수 있어야 한다.

let arr = [1,2,[3,4]];
let arr2 = [...arr];

console.log(arr === arr2) // 출력 결과: false

// 배열 안에 객체가 있을 경우
let arr = [1,2,[3,4]];
let arr2 = [...arr];

arr2[2].push(5);

console.log(arr[2] === arr2[2]) // 출력 결과: true

결과에서 보이듯이 펼침 연사자로도 깊은 복사는 수행이 안되고 얕은 복사만 가능하다. 지금까지 알아본 방법들은 하나같이 얕은 복사까지만 정상적으로 수행한다. 그렇다면 깊은 복사까지 할 수 있는 방법에는 어떤게 있을까?

프로그래밍하면서 깊은 복사까지 할 일은 굉장히 드물다. 그래도 필요한 경우를 대비해서 ramda나 lodash 라이브러리에서 깊은 복사를 수행하는 메서드를 제공해준다. 가장 보편적으로lodashcloneDeep() 메서드가 있다.

정리

얕은 복사와 깊은 복사에 대해 알아보면서 다른 필요한 개념까지 함께 서술하였다. 지금까지 주저리 떠들었던 내용을 간략히 요약하자면 아래와 같다.

  1. 데이터 타입에는 원시형과 참조형이 있고 메모리 관리 방식이 서로 다르다
  2. 참조형 데이터를 복사할 때 얕은 복사와 깊은 복사로 나뉘는데 얕은 복사는 복사 객체와 원본 객체가 같은 메모리 참조 주소를 갖고 깊은 복사는 서로 다르다.
  3. 복사를 할 때 자바스크립트에서 제공하는 함수들은 단일 원시형 데이터이거나 참조형이지만 원시형으로 이루어진 데이터이면 깊은 복사를 하지만 중첩된 참조형 데이터는 모든 요소를 완벽히 복사하지 못하는 얕은 복사를 한다.
  4. 깊은 복사를 위해서는 lodash에서 제공하는 cloneDeep() 메서드를 이용하면 된다.
profile
아는만큼 보인다

0개의 댓글