(JS) 깊은 복사와 얕은 복사

Dragon·2024년 1월 21일

위클리 페이퍼

목록 보기
6/18
post-thumbnail

자바스크립트의 자료형

자바 스크립트의 자료형에는 원시형과 참조형의 두가지 종류가 존재한다.

  • 원시형에는 Number, String, Boolean, Symbol, Bigint, Null, Undefined 의 7가지 자료형이 존재하며, 변수에 할당 시에는 메모리의 Stack 영역 변수 자리에 '값' 자체가 저장되기 때문에, 변수에 다른 값을 할당하면 실제 값이 바뀐다.

  • 참조형에는 Object, 하나의 자료형이 존재한다. 변수에 참조형 데이터를 할당하면 메모리의 Stack 영역 변수 위치에는 Heap 영역에 저장되어 있는 객체의 주소만을 가져오게 된다. 즉, 변수에 다른 값을 할당하면 객체와의 주소 연결만 끊어지게 될 뿐, 저장된 객체 자체는 바뀌지 않는다.

깊은 복사와 얕은 복사

let x = 3;
let y = x;
x += 2
console.log(y)

위와 같은 코드에서는 어떤 값이 출력될까? x += 2 와는 상관없이 3 이 출력될 것이다.
x 에 담긴 3 이라는 값은 원시형 값이기 때문에, y = x 라는 복사 과정에서 y3 이라는 값 자체를 데이터에 저장한다. 즉, 원시형의 값이 변수에 복사되어 할당될 때에는 변수 데이터 영역에 '값' 을 새로 만들어 저장한다. 위 예시에서는 x 에도 3, y 에도 3 이라는 데이터가 새로 만들어져 저장되었기 때문에 x 가 변하더라도 y 에 저장된 값은 똑같이 3 이다. 이처럼, 복사 과정에서 똑같은 '값' 자체를 그대로 복사해 저장하는 것을 깊은 복사 라고 한다.

let x = { first: 1 };
let y = x;
x.first = 2;
console.log(y);

그럼 위의 경우도 살펴보자. 과연 어떤 값이 출력될까? x.first = 2 로 인해 변화된 { first: 2 } 가 출력될 것이다.
사실 x 라는 변수에 담긴 데이터는 '객체' 그 자체가 아니라 '객체'에 대한 주소값이다. 객체 자체는 Heap 영역이라는 다른 위치에 저장되고, 변수에는 그 객체를 찾기 위한 주소값 만이 저장된다. 따라서 복사 과정에서는 xy 에 똑같은 객체를 향한 주소가 저장되고, x.first = 2 를 통해 객체가 변화되면 y 의 주소값이 가리키는 객체도 변하게 되는 것이다. 이렇게 복사 과정에서 '값'을 새로 만들어 저장하는 것이 아니라, '값'을 찾는 주소값 만을 복사하는 것을 얕은 복사 라고 한다.

참조형 데이터의 얕은 복사

새로운 A 라는 빈 객체를 만들고, B 객체의 프로퍼티들을 전부 가져와 담는다면, 두 객체는 서로 다른 주소값을 가진다. 이런 경우 일반적으로 for..in 반복문을 통해 복사를 시도한다. 하지만 B 객체의 프로퍼티 중에 객체가 있다면 A 안의 객체와 B 안의 객체는 같은 주소를 가진 같은 객체이므로 A 와 B 간의 연결이 아예 끊어진 것은 아니다. 이런 방식의 복사 또한 얕은 복사라고 부른다.

Object.assign()

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

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

Object.assign(obj1, obj2) 메서드를 사용하면, obj1obj2 를 병합한다. 두 객체 사이에 동일한 key 의 프로퍼티가 있는 경우에는 obj2 의 값을 덮어씌운다. 따라서 위처럼 빈 객체에 원하는 obj 를 병합하면 복사를 할 수 있다. 하지만, 두 객체의 b 프로퍼티는 주소가 동일하므로 얕은 복사이다.

Spead 연산자

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

const copyObj = {...obj};

Spread 연산자(전개 연산자)는 배열에서도 사용되며, 마찬가지로 얕은 복사이다.

참조형 데이터의 깊은 복사

위의 얕은 복사처럼이 아니라, 객체 내부의 객체에 대해서도 다른 주소값을 가진 새로운 객체로 복사해오는 깊은 복사를 하려면 아래 방법을 참고하자.

재귀 함수

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

function deepCopy(obj) {  
  if (obj === null || typeof obj !== "object") {
    return obj;     // 빈 객체거나, 원시형 데이터라면 그냥 내보낸다.
  }    
  
  const copyObj = Array.isArray(obj) ? [] : {};  // 객체인지 배열인지의 여부를 판단
  
  for (let key of Object.keys(obj)) {    // key 값과 value 값을 따로 가져와 저장한다.
    copyObj[key] = deepCopy(obj[key]);  
  }   
  
  return copyObj;
} 

const copyObj = deepCopy(obj);

재귀함수를 이용한 복사 방법이다. 내부에 객체가 있더라도 그 객체를 통째로 가져오는게 아니라, 그 객체의 key와 value를 하나씩 가져와 빈 객체에 저장하므로 주소 값도 달라진다.

JSON.parse(JSON.stringify(obj))

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

const copyObj = JSON.parse(JSON.stringify(obj));

JSON.stringify() 는 객체나 원시형 값을 JSON 문자열로 변환하고, JSON.parse() 는 JSON 문자열을 분석하여 다시 JavaScript 코드로 변형해준다. 그리고 그 과정에서 새로운 원시형 값이나 객체를 생성되므로 원본 객체와의 참조가 모두 끊어지고 깊은 복사가 가능해진다.

Lodash 라이브러리 사용

const deepCopy = require("lodash.clonedeep");

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

const copyObj = deepCopy(obj);

lodash 라이브러리의 clonedeep 기능을 이용하여 편하게 깊은 복사가 가능하다. 다운로드가 필요하지만, 편리한 기능이므로 이용해보는 것도 괜찮다.

profile
2024년부터 시작하는 프론트엔드 개발 공부

0개의 댓글