Javascript

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

ssseeo0 2024. 3. 28. 23:21

 

얕은 복사는 객체의 참조값(주소값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사한다. 

 

먼저, 자바스크립트에는 원시 타입(Primitive Type)과 참조 타입(Reference Type) 두 가지 타입의 자료형이 있다. 

 

원시값 (기본 자료형) 

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Symnol 

변수에 원시값을 저장하면 변수의 메모리 공간에 실제 데이터 값이 저장된다. 

또한 원시 타입 자료형은 변수 선언, 초기화, 할당 시 값이 저장된 메모리 영역에 직접적으로 접근한다. 

 

즉 변수에 새 값이 할당될 때 변수에 할당된 메모리 블럭에 저장된 값을 바로 변경한다는 뜻이다. 

 

let origin = 100;
let copy = origin; 

console.log(copy); // 100

origin = 200;
console.log(copy); // 100

 

원시 타입은 값 자체를 복사하기 때문에 원본 데이터의 값이 바뀌더라도 기존 데이터 값을 유지한다. 

원시 타입을 복사할 때는 항상 깊은 복사가 되므로, 깊은 복사와 얕은 복사를 구분할 필요가 없다. 

참조값 

  • Object 

변수에 참조값을 저장하면 독립적인 메모리 공간(Heap)에 값을 저장하고, 변수에 저장된 메모리 공간의 참조를 저장하게 된다.  즉 변수의 값이 저장된 메모리 블록의 주소를 가지고 있다. 

 

let origin = {
	name: 'seoyoung'
}
let copy = origin; 

console.log(copy.name); // 'seoyoung'

origin.name = 'kimseoyoung';
console.log(copy.name); // 'kimseoyoung'

 

참조 타입은 주소 값을 참조하기 때문에 원본 데이터의 값이 바뀌면 복사한 데이터의 값도 변경된다. 

 

이렇게 참조 타입의 데이터는 복사 시 데이터의 값이 아닌 '값이 저장된 메모리의 주소'가 저장된다. 

따라서 참조 타입의 복사 방법은 얕은 복사와 깊은 복사로 나뉜다. 

 

📍 얕은 복사 (Shllow Copy)

얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말한다. 

다시 말해 참조 데이터가 저장한 '메모리 주소 값'을 복사한 것을 의미한다. 

 

const arr = [];
const brr = arr;

brr.push(1);
console.log(arr === brr); // true
console.log(arr); // [1]
console.log(brr); // [1]

 

brr 배열에만 1이라는 값을 push 해줬음에도 arr 배열에도 1이 들어있다. 

brr은 얕은 복사를 이용해 만들었고, arr과 brr 두 개의 변수가 하나의 주소값을 공유하고 있기 때문이다. 

 

 

 

📍 깊은 복사 (Deep Copy)

깊은 복사란 새로운 메모리 공간을 확보해 완전히 복사하는 것을 의미한다.

원본과의 참조가 완전히 끊어지고 값만 복사하는 것이다. 

 

const arr = [];
const brr = arr.slice(); 

brr.push(1);
console.log(arr === brr); // false
console.log(arr); // []
console.log(brr); // [1]

 

얕은 복사와 달리 brr에 1을 push 했음에도 arr은 빈 배열이다. 이는 arr과 brr 배열이 서로 다른 주소값을 가지고 있기 때문이다. 

 

깊은 복사를 하는 방법은 다음과 같다. 

◽️Object.assign() - 1차원 객체

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

const obj = { a: 1 };
const copyObj = Object.assign({}, obj);

copyObj.a = 2;

console.log(obj); // { a: 1 }
console.log(obj === copyObj); // false

 

여기서 주의해야 할 점은 1차원 객체/배열일 경우에만 깊은 복사가 이루어지며 2차원일 경우 깊은 복사가 이루어지지 않는다. 

const obj = {
  a: 1,
  b: { c: 2 },
}

const copyObj = Object.assign({}, obj);

copyObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj === copyObj); // false
console.log(obj.b === copyObj.b); // true
console.log(obj.b.c === copyObj.b.c); // true

 

같은 Object.assign() 를 이용해 객체를 복사하였지만 복사된 하위 객체는 얕은 복사가 된 것이다. 

 

◽️ Spread 연산자 - 1차원 객체 

const obj = {
  a: 1,
  b: { c: 2 },
};

const copyObj = {...obj};

copyObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === copyObj.b.c); // true

 

Object.assign()와 마찬가지로 2차원 객체/배열일 경우에는 얕은 복사가 이루어진다. 

 

그 다음 깊은 복사 방법들은 2차원 이상의 객체도 깊은 복사가 가능하다.

 

◽️ JSON.stringify() && JSON.parse()

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

JSON.parse()는 JSON 문자열을 JS 값이나 객체로 변환한다. 

 

즉 JSON.stringify() 통해 참조(references)를 모두 제거하고, 

다시 JSON.parse()를 통해 원래 객체 형태로 돌려놓는 방식이다. 

 

 이 방식은 간단하지만, 다른 방식에 비해 느리고, function의 경우 undefined로 나온다는 것이 단점이다. 

 

const obj = {
  a: 1,
  b: { c: 2 },
  func: function() {
    return this.a;
  }
};

const copyObj = JSON.parse(JSON.stringify(obj));
console.log(copyObj.func); // undefined

 

◽️ 커스텀 재귀 함수 

const deepCopy = (obj) => {
  if (obj === null || typeof obj !== 'object') return obj;
  
  let copy = {};
  for (let key in obj) {
    copy[key] = deepCopy(obj[key]);
  }
  return copy;
};

const obj = {
  a: 1,
  b: { c: 2 },
  func: function () {
    return this.a;
  }
};

const copyObj = deepCopy(obj);

copyObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === copyObj.b.c); // false

 

직접 함수를 구현하여 다차원 객체를 깊은 복사할 수 있지만 구현이 복잡하다는 것이 단점이다. 

 

◽️ lodash 라이브러리의 cloneDeep() 

const obj = {
  a: 1,
  b: { c: 2 },
  func: function () {
    return this.a;
  }
};

const copyObj = lodash.cloneDeep(obj);

copyObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === copyObj.b.c); // false

 

라이브러리를 이용해 쉽고 안전하게 깊은 복사를 할 수 있다.