JavaScript - 깊은 복사 / 얕은 복사

주세환·2024년 5월 3일
0

JavaScript

목록 보기
12/14
post-thumbnail
  • 얕은 복사란, 참조형 타입의 값이 바로 아래 단계의 값만 복사하는 방법이다.
  • 깊은 복사란, 참조형 타입 안의 모든 참조가 끊어지는 방법이다.

데이터 타입

JavaScript의 얕은 복사와 깊은 복사를 알아보기 전에 데이터 타입에 대해 알아보자.

  • 기본형(Primitive) 타입
  • 참조형(Reference) 타입

기본형 타입에는 Number, String, Boolean, undefined, null 그리고 ES6에서 추가된 Symbol이 있다.

참조형 타입은 Object, Array, Function, Date, RegExp, 그리고 ES6에서 추가된 Map, WeakMap, Set, WeakSet이 있다.

기본형과 참조형을 구분할 수 있는 방법은, 할당이나 연산 시 값을 복사하면 기본형이고, 값을 복사할 때 참조하면 참조형이다. 그럼 할당을 해보자.

var a = 10;

코드에서는 변수를 선언하고, 식별자를 a로 주었으며 number 타입인 10을 할당했다.

이 코드를 실행했을 때, 메모리의 한 공간을 확보한 다음, 식별자를 붙인다. 그리고 바로 10을 할당하지 않고 데이터를 저장하기 위한 다른 메모리 공간을 하나 더 확보하고 그곳의 주소를 식별자가 저장된 곳의 값으로 집어넣는다. 그리고 10이라는 값을 집어넣는다.

즉 변수는 값의 위치(주소)를 기억하는 메모리 공간인데, 값의 위치란 값이 위치하고 있는 메모리 상의 주소(address)를 의미한다.

변수란 값이 위치하고 있는 메모리 주소에 접근하기 위해 사람이 이해할 수 있는 언어로 지정한 식별자다.

값을 가져오는 과정은 위 과정과 반대다. console.log로 a에 접근했을 때 먼저 메모리 공간에서 식별자가 a인 메모리 공간을 찾고, 메모리 공간에 저장된 실제 값이 들어있는 공간의 주소로 값을 가져오는 것이다.

number 타입은 기본형 타입으로 주소가 한번 연결되었다. 그러면 참조형 타입을 알아보자.

var obj = {
  a : 10,
  b : 'abc',
};

  1. obj라는 식별자를 가진 변수를 선언하고 참조형 타입인 객체 하나를 할당했다. 기본형과 동일하게 메모리 공간(@1002)을 하나 확보하고 obj라는 식별자를 주었다. 그리고 값을 저장하려고 다른 메모리 공간(@5001)을 하나 더 확보했더니 기본형으로 저장해야 하는 값이 여러 개가 들어있다.

  2. 이 프로퍼티들을 저장하기 위해 방금 확보한 공간 외 프로퍼티 갯수에 맞게 또 여러 개의 공간(@7103, @7104)을 확보하고 식별자를 지정한다.

  3. 그리고 그 식별자의 실제 데이터가 저장할 공간을 다시 확보하고 (@5003, @5004) 실제 값을 저장한다.

  4. @7103, @7104에 방금 저장한 곳의 주소를 저장 후, @5001에 그룹의 주소를 저장한다.

  5. 마지막으로, 아까 확보해둔 메모리 공간(@5001)에 값이 저장된 (@7103, @7104)의 공간 주소를 저장한다.

기본형 데이터와의 차이는 참조형 데이터 안에 있는 기본형 데이터를 저장하기 위해 기본형 데이터의 주소를 담은 공간을 새로 생성했다는 점이다.


불변 값이란?

불변 값은 변하지 않는 값이라는 뜻이다. 위에서 살펴본 기본형 타입은 불변 값이다. 그리고 참조형 타입은 대체적으로 가변 값이다.

var a = 10;
a = 20;
console.log(a); // 20

처음에는 a에 10 값을 주었다. 그리고 20 값을 다시 할당하고 출력하니 20이 나왔다. 자바스크립트 입장에서 설명해보자.

  • 메모리 공간을 확보 후 다른 메모리 공간에 10을 할당했다.
  • 그리고 10이 저장된 주소를 a에 넣는다.
  • 이후, 20을 재할당할 때, 10이 저장되어 있는 메모리 공간을 그대로 두고, 20을 저장하는 메모리 공간을 추가로 확보 후 저장한다.
  • 그리고 그 공간의 주소를 a가 저장한다.

흔히 사람의 입장에서는 10이 저장되어 있는 메모리 공간에 10을 삭제하고 20을 집어넣는 것을 생각한다. 하지만 자바스크립트는 값을 새로 생성하고 주소를 수정해주었다. 즉, 기본형 타입은 새로 생성되었고 변경되지 않았다. (10이라는 값은 추후 GC(Garbage Collection)가 수집하여 없어질 것임)

하지만 레퍼런스 타입은 다르다.

var obj = {
  a: 10,
  b: 'abc',
}
obj.a = 20;

console.log(obj.a) // 20

obj의 프로퍼티의 값을 재할당하면, obj가 가지고 있는 메모리 주소는 변경되지 않고 obj.a가 가지고 있는 주소가 변경된다. 즉, '새로운 객체'가 만들어진 것이 아니로 기존 객체 내부의 값만 바뀐 것이다.


깊은 복사란?

여기서 말하는 복사란, 원본을 베낌. 종이를 포개고 그 사이사이에 복사지를 받쳐 한 번에 여러 장을 쓴다는 의미를 가지고 있다. 즉 동일한 내용을 그대로 다른 곳에서 사용하는 것을 말한다.

그리고 깊은 복사란, 기존 값의 모든 참조가 끊어지는 것을 말한다. 특히 복사할 때, 참조형 타입 값(객체)에서 내부에 있는 모든 값이 새로운 값이 되는 것을 말한다.

기본형 타입의 깊은 복사

자바스크립트에서는 할당 연산자(=)를 사용해 쉽게 복사를 할 수 있다.

var a = 10;
var b = a;
console.log(a); // 10
console.log(b); // 10

b에 10을 직접 할당해주지 않아도 a를 할당했기 때문에 10이라는 값이 도출된다. 하지만, 정확하게 말하자면 할당 연산자(=)는 객체를 복사해서 값에 집어넣는 것이 아니다. 단지 a가 가지고있는 주소를 b에게 주어 b도 그 대상을 바라보게 만든 것이다. 기본형 타입은 불변 값이라고 했다. 기본형 타입의 값을 재할당 할 때는 기존 값을 변경하는 것이 아닌 새로 만들어 주소 값을 준다는 의미였다. 따라서, a와 b는 메모리 공간에 생성된 10의 주소값을 가지고 있다.

var a = 10;
var b = 10;
console.log(a === b); // true

var c = 20;
var d = c;
console.log(c === d); // true

d = 30;
console.log(c); // 20
console.log(d); // 30
console.log(c === d); // false
  • 기본형 타입의 값을 바라보는 주소값이 동일하기 때문에 a와 b는 각각 값을 할당 받았지만 동일하다고 판단한다.

  • d도 마찬가지로 c의 주소를 넘겨받았기 때문에 동일하다고 판단한다.

  • c의 주소를 넘겨받은 d에 30을 재할당했다.

  • c와 d는 다른 값을 바라보고 있다.

내부의 값은 없지만 복사했을 때, 서로의 주소가 달라졌다. 기본형 타입의 깊은 복사다. 서로에게 영향을 주지 않는다.


참조형 타입의 깊은 복사

var obj1 = {
  a: 10,
  b: 'abc',
};
var obj2 = obj1;
console.log(obj1 === obj2); // true

obj2.a = 20;
console.log(obj1);	// {a: 20, b: 'abc'}
console.log(obj2);	// {a: 20, b: 'abc'}
console.log(obj1 === obj2);	// true
  • obj1과 obj2가 가지고 있는 주소 값이 동일하기 때문에 true가 나왔다.

  • obj2의 a프로퍼티 값을 20으로 재할당했다.

  • obj1, obj2의 값이 a 프로퍼티 모두 20으로 변경되었다.

  • 여전히 obj1과 obj2가 가지고 있는 주소 값이 동일하다.

위 결과가 나온 이유는, obj2의 프로퍼티를 변경시켰기 때문이다. 프로퍼티 a가 바라보고 있는 주소 자체는 변경되었지만 obj1, obj2가 프로퍼티 그룹을 바라보는 주소 자체는 변경되지 않은 것이다.

객체 자체의 참조 값을 할당하면, 깊은 복사가 일어나지 않는다.

그렇다면 객체의 깊은 복사를 하려면, 즉 내부의 프로퍼티 값의 주소를 전부 다르게 하려면 어떻게 해야할까?

재귀 함수를 이용

var deepCopy = function (obj) {
  var result = {};
  if (typeof obj === 'object' && obj != null) {
    for(var prop in obj) {
      result[obj] = deepCopy(obj[prop]);
    }
  } else {
    result = obj;
  }
  return result;
}

deepCopy 함수는 함수 내부에서 자기 자신을 호출하는 재귀 함수다. 중첩된 객체라고 하더라도 프로퍼티 갯수만큼 돌면서 result 객체에 새롭게 할당해준다.

---

JSON.parse(JSON.stringify(obj))를 사용

var obj1 = {
  a: 10,
  b: 'abc',
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b = 3;
console.log(obj1);	// {a: 10, b: 'abc'}
console.log(obj2);	// {a: 10, b: 3}

JSON.stringify()는 JavaScript 값이나 객체를 JSON 문자열로 변환한다. 그리고 JSON.parse()는 JSON 문자열의 구분을 분석하고, 그 결과에서 JavaScript 값이나 객체를 생성한다. 즉, 객체를 문자열로 변환 후 다시 객체 형태로 만든다. 문자열로 변환 후 다시 객체로 만들면 원본 객체와의 참조가 모두 끊어진다.


깊은 복사를 사용하면 복사했을 때 값은 동일하지만, 객체 내부의 값을 변경해도 서로 영향을 주지 않고 격리된 값을 보장한다.
원시값과 달리 객체는 변경이 가능한 가변값이며 프로퍼티의 집합이다. 클래스에 정의된 멤버대로 객체를 생성하는 자바같은 경우 객체가 생성된 이후 멤버를 추가할 수 없다. 하지만, 자바스크립트는 객체 생성 이후에도 프로퍼티를 추가하거나 삭제하는 등 변경할 수 있다. 따라서, 깊은 복사를 통해 객체를 복사하지 않으면 객체 사이의 관계가 생성되어 예기치 못한 오류가 발생할 수 있다.


얕은 복사란?

얕은 복사란 참조형 타입의 값의 바로 아래 단계의 값만 복사하는 방법이다.

var obj1 = {
  a: 1,
  b: {
    c: 2,
  },
};
var obj2 = { ...obj1 };
console.log(obj1 === obj2);	// false
console.log(obj1.b === obj2.b);	// true

두 개의 객체는 다른 주소를 가지고 있지만, 객체 안 프로퍼티는 동일한 주소를 가지고 있다. 쉽게 말하면 {}이 껍데기는 새로 생성된 객체이며 새로운 주소를 갖게 되었고 spread 연산자로 풀어진 프로퍼티들은 처음 선언된 obj1의 프로퍼티들이 사용되었다.


Object.assign()

assign은 할당이라는 뜻을 가지고 있으며 객체와 객체를 합쳐주는 메서드다.

var obj1 = {
  a: 10,
  b: {
    c: 'abc',
  },
};
var obj2 = Object.assign({},obj1);
obj2.a = 20;
obj2.b.c = 'def';

console.log(obj1);	// { a: 10, b: {c:"def"} }
console.log(obj2);	// { a: 20, b: {c:"def"} }
  • 첫 번쨰 인자로 {} 빈 객체가 들어갔기 떄문에 껍데기가 obj1과 다른 객체가 반환될 것이다.
  • obj1의 내용을 {} 빈 객체 안에 복사해서 집어놓고 반환한다.
  • 프로퍼티 a는 기본형 타입의 값이 들어있기 때문에 변경하면 obj2의 a 프로퍼티만 변경된다.
  • 프로퍼티 b는 참조형 타입의 값이 들어있기 때문에 변경하면 obj1, obj2의 b가 가진 주소값이 동일하기 때문에 둘 다 변경된다.

mdn에 따르면 Object.assign()은 깊은 복사를 해주지 않는다고 나와있다.


for ... in

var copyShallo = function (obj) {
  var result = {};
  for (var prop in obj) {
    result[prop] = obj[prop];
  }
  return result;
};

깊은 복사에서 사용했던 함수와 달리 재귀를 사용하지 않았다. 단순히 첫 번째 프로퍼티만 새로 만든 result 객체에 담는 형태이다.


정리

얕은 복사는 한 단계까지만 복사하고, 깊은 복사는 객체에 중첩된 객체까지 모두 복사한다. 얕은 복사깊은 복사 모두 복사한 대상에 대해서 새로운 객체를 생성하여 기존 객체에는 영향을 주지 않는다. 하지만 얕은 복사깊은 복사는 어느 수준까지 복사하느냐의 차이를 가진다. 얕은 복사를 하면 한 단계만 복사하기 때문에 중첩된 객체에 대해서는 서로 영향을 주고, 깊은 복사는 중첩된 객체 역시 별개의 값으로서 서로 영향을 주지 않는다.

출처 - https://pozafly.github.io/javascript/shallo-copy-and-deep-copy/

0개의 댓글