Javascript 객체 배열 깊은 복사 - Javascript gaegche baeyeol gip-eun bogsa

[JavaScript] 깊은 복사, 얕은 복사

  • 2021.07.12 03:24
  • 공부 || 정리/JavaScript

깊은 복사, 얕은 복사 

 결론부터 말하자면 얕은 복사는 객체의 참조값(주소 값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사합니다. 

 먼저, 자바스크립트에서 값은 원시값참조값 두 가지 데이터 타입의 값이 존재합니다.

  • 원시값은 기본 자료형(단순한 데이터)을 의미합니다.  Number, String, Boolean, Null, Undefined 등이 해당합니다. 변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장됩니다. 할당된 변수를 조작하려고 하면 저장된 실제 값이 조작됩니다.
  •  참조값은 여러 자료형으로 구성되는 메모리에 저장된 객체입니다. Object, Symbol 등이 해당합니다. 변수에 객체를 저장하면 독립적인 메모리 공간에 값을 저장하고, 변수에 저장된 메모리 공간의 참조(위치 값)를 저장하게 됩니다. 그래서 할당된 변수를 조작하는 것은 사실 객체 자체를 조작하는 것이 아닌, 해당 객체의 참조를 조작하는 것입니다.    

 원시값을 복사할 때 그 값은 또 다른 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 수정해도 기존 원시값을 저장한 변수에는 영향을 끼치지 않습니다. 이처럼 실제 값을 복사하는 것을 깊은 복사라고 합니다. 하지만 이것은 자료형을 깊은 복사한 것입니다.

const a = 'a';
let b = 'b';

b = 'c';

console.log(a); // 'a';
console.log(b); // 'c';

// 기존 값에 영향을 끼치지 않는다.

 참조값을 복사할 때는 변수가 객체의 참조를 가리키고 있기 때문에 복사된 변수 또한 객체가 저장된 메모리 공간의 참조를 가리키고 있습니다. 그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 참조를 가리키고 있기 때문에 기존 객체를 저장한 변수에 영향을 끼칩니다. 이처럼 객체의 참조값(주소값)을 복사하는 것을 얕은 복사라고 합니다.  

const a = {
  one: 1,
  two: 2,
};
let b = a;

b.one = 3;

console.log(a); // { one: 3, two: 2 } 출력
console.log(b); // { one: 3, two: 2 } 출력

// 기존 값에 영향을 끼친다.

 일반적으로 복사라는 개념을 생각한다면 깊은 복사가 떠오를 것입니다.(제가 그랬습니다 ;;)

하지만 객체를 복사할 때  =  키워드를 사용해서 복사하면 얕은 복사가 돼서 기존 변수 또한 수정돼서 당황하게 될 겁니다. 그렇다면 자바스크립트에서 얕은 복사 혹은 깊은 복사를 하는 방법은 어떤 것이 있을까요?

얕은 복사(shllow Copy) 방법

 얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말합니다.
객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 합니다.

 

Array.prototype.slice()

얕은 복사 방법의 대표적인 예라고 할 수 잇습니다. start부터 end 인덱스까지 기존 배열에서 추출하여 새로운 배열을 리턴하는 메소드 입니다. 만약 start와 end를 설정하지 않는다면, 기존 배열을 전체 얕은 복사합니다. 

const original = ['a',2,true,4,"hi"];
const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

copy.push(10);

console.log(JSON.stringify(original) === JSON.stringify(copy)); // false

console.log(original); // [ 'a', 2, true, 4, 'hi' ]
console.log(copy); // [ 'a', 2, true, 4, 'hi', 10 ]

기존 배열에는 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열일 뿐입니다.

원시값은 기본적으로 깊은 복사입니다. Slice() 메소드는 기본적으로 얕은 복사를 수행합니다. 

const original = [
  [1, 1, 1, 1],
  [0, 0, 0, 0],
  [2, 2, 2, 2],
  [3, 3, 3, 3],
];

const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

// 복사된 배열에만 변경과 추가.
copy[0][0] = 99; 
copy[2].push(98);

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

console.log(original);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력
console.log(copy);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력

만약 1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행하게 됩니다.  

const original = [
  {
    a: 1,
    b: 2,
  },
  true,
];
const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

// 복사된 배열에만 변경.
copy[0].a = 99;
copy[1] = false;

console.log(JSON.stringify(original) === JSON.stringify(copy)); // false

console.log(original);
// [ { a: 99, b: 2 }, true ]
console.log(copy);
// [ { a: 99, b: 2 }, false ]

 또 다른 예시 입니다. 배열 안에 객체를 수정하고자 할 경우 얕은 복사를 수행하는 것을 볼 수 있습니다. 하지만 원시값은 기본적으로 깊은 복사라 기존 변수에 있는 값과는 다른 값을 도출하는 것을 볼 수 있습니다. 

Object.assign()

Object.assign(생성할 객체, 복사할 객체)

메소드의 첫 번째 인자로 빈 객체를 넣어주고 두 번째 인자로 복사할 객체를 넣어주면 됩니다. 

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
};

const copy = Object.assign({}, object);

copy.number.one = 3;

console.log(object === copy); // false
console.log(object.number.one  === copy.number.one); // true

복사된 객체 copy 자체는 기존 object와 다른 객체지만 그 안에 들어가 있는 값은 기존 object안의 값과 같은 참조 값을 가리키고 있습니다. 

MDN에서는 Object.assign()에 대해 이렇게 설명했습니다. 

깊은 클로닝에 대한 주의사항

깊은 클로닝에 대해서, Object.assign() 은 속성의 값을 복사하기때문에 다른 대안을 사용해야합니다. 출처 값이 객체에 대한 참조인 경우, 참조 값만을 복사합니다.

Spread 연산자 (전개 연산자)

마찬가지로 얕은 복사입니다.

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
};

const copy = {...object}

copy.number.one = 3;

console.log(object === copy); // false
console.log(object.number.one  === copy.number.one); // true

깊은 복사(Deep Copy) 방법

깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말합니다.

 사실 이 글을 쓰는 진짜 이유입니다. 복사를 하는 목적은 기존 객체의 값만 복사본으로 가져와 별도로 활용하기 위함이 대부분이라고 생각합니다. 기존 객체까지 건드린다면 이것은 복사를 하는 목적에 벗어난다고 생각합니다. 

아 물론, 필자가 부족하여 얕은 복사의 목적을 경험하지 못한 것일 수도 있습니다.

JSON.parse && JSON.stringify

 JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어집니다.

객체를 json 문자열로 변환 후, JSON.parse()를 이용해 다시 원래 객체(자바스크립트 객체)로 만들어줍니다.

이 방법이 가장 간단하고 쉽지만 다른 방법에 비해 느리다는 것과 객체가 function일 경우,  undefined로 처리한다는 것이 단점입니다.

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
  arr: [1, 2, [3, 4]],
};

const copy = JSON.parse(JSON.stringify(object));

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

재귀 함수를 구현한 복사

 복잡하다는 것이 단점입니다.

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
  arr: [1, 2, [3, 4]],
};

function deepCopy(object) {
  if (object === null || typeof object !== "object") {
    return object;
  }
  // 객체인지 배열인지 판단
  const copy = Array.isArray(object) ? [] : {};

  for (let key of Object.keys(object)) {
    copy[key] = deepCopy(object[key]);
  }

  return copy;
}

const copy = deepCopy(object);

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

Lodash 라이브러리 사용 

 라이브러리를 사용하면 더 쉽고 안전하게 깊은 복사를 할 수 있습니다. 설치를 해야 한다는 점과 일반적인 개발에는 효율적이겠지만, 코딩 테스트에는 사용할 수 없다는 것이 단점입니다.

const deepCopy = require("lodash.clonedeep")

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
  arr: [1, 2, [3, 4]],
};

const copy = deepCopy(object);

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

피드백 환영합니다.