[Java Script] 얕은 복사와 깊은 복사

June hyoung Park·2020년 9월 18일
3

JavaScript

목록 보기
12/18
post-thumbnail

자바스크립트에서의 복사는 깊은 복사(deep copy) 와 얕은 복사(shallow copy) 가 존재한다.
원시 타입(primitive type)의 데이터가 복사 될때는 새로운 메모리 공간을 생성하여 메모리에 독립적인 값을 저장하는 반면 object(객체,배열, 함수)와 같은 참조 타입(reference type) 데이터는 애초에 저장 시 데이터에 대한 주소 (힙(Heap) 메모리의 주소값)가 저장되기 때문에 복사 시 값 자체가 아닌, 해당 값을 가리키는 주소가 복사된다.

즉 이를 요약하자면, 원시타입(String, Number, Boolean)은 복사(깊은복사)가 되고, 원시타입을 제외한 객체들(참조타입)은 참조(얕은복사)가 된다. 아래의 간단한 예시를 통해 그 차이를 알아보자.

깊은 복사

let a = 1;
let b = a;

console.log(a); // 1
console.log(b); // 1

a = 2;
console.log(a) // 2
console.log(b) // 1

먼저 변수 a에 1을 할당한뒤, 변수 b에 a를 할당(복사)했다. 당연히 이 시점에서 a와 b를 출력한다면 둘다 '1'이 나오게된다. 그후 a에 2를 재할당 했지만, 여전히 b는 '1'을 출력하는것을 볼 수 있다.

이는 원시타입의 특성을 알면 이해할 수 있는 상황이다. 원시타입은 복사 시 값 자체를 담은 독립적인 메모리를 생성하기 때문에 a가 재할당 되더라도 b에는 아무런 영향을 미치지 않는다.

얕은 복사 (참조)

let a = { name: 'Park'};
let b = a; // 참조

console.log(a); //{name: "Park"}
console.log(b); //{name: "Park"}

a.name = 'Kim';

console.log(a); //{name: "Kim"}
console.log(b); //{name: "Kim"}

변수 a에 객체를 하나 만들고 b에 a를 할당해 주었다. 그후 a의 name이란 키의 값을 'Kim'으로 변경했더니, a객체를 복사한 b의 객체에서도 변형이 일어났다. 이는 참조 타입의 변수는 실제 데이터가 저장된 주소를 참조하기에 복사가 일어났다 하더라도, a와 b 둘다 가리키고 있는 방향(주소)이 동일하기에 이런 일이 일어나는 것이다.

즉, 데이터가 그대로 하나 더 생성된 것이 아닌 해당 데이터의 메모리 주소를 전달하게 돼서, 결국 한 데이터를 공유하게 되는 것이다.

참조 타입의 깊은 복사

자바스크립트를 배운지 얼마 안됬을때, 객체나 배열은 전개연산자나 Object.assign() 메서드를 통해 (깊은)복사 할 수 있다고 배웠었지만, 위 방법들은 일정 깊이 이상의 데이터들은 (객체가 프로퍼티로 객체를 가지고있는 등) 복사 되지않는다는 것을 최근에 알게되었다.

let notDeep  =  {
  a : 1,
  b : 2,
  c : {name : "park"}
}


let b = {...notDeep}

notDeep.a = 100
notDeep['c'].name = "Kim"


console.log(notDeep.a) // 100
console.log(b.a) // 1

console.log(notDeep.c) // { name:"Kim" }
console.log(b.c) // { name:"Kim" }

먼저 흔하게 사용하는 전개 연산자를 통한 예제를 만들어 보았다. notDeep 변수에 객체를 할당한뒤 변수 b에 전개 연산자를 통해 notDeep객체를 복사했다.

물론 notDeep 객체의 a란 키에 100을 재할당 후 b의 a를 출력했을때, 100이 아닌, 1로 출력되기에 정상적으로 깊은 복사가 이루어진 것 처럼 보이지만, notDeep 내부의 {name : "park"}이란 객체의 'name'의 값을 "Kim"으로 변경했더니 b의 name까지 변경된 것을 알 수 있다. 즉, 완벽한 복사가 이루어졌다고 하기 힘들다. ((Object.assign()도 동일)

그렇다면 완벽한 복사는 어떻게 해야할까 🤔

1. JSON객체의 메소드를 이용

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

let notDeep  =  {
  a : 1,
  b : 2,
  c : {name : "park"}
}

const copied = copyObj(notDeep);

notDeep.a = 1000;
notDeep.c.name = "Kim";

console.log(notDeep.c) // { name:"Kim" }
console.log(copied.c);	// { name:"park" }

JSON.stringify 메서드는 자바스크립트 객체를 JSON문자열로 변환 시킨다. 반대로 JSON.parse는 JSON문자열을 자바스크립트 객체로 변환시킨다. 그렇기에 객체를 JSON문자열로 변환했다가 다시 객체로 변환하기에, 객체에 대한 참조가 사라진것이다. 그러나 이 방법에는 2가지 문제점이 있는데, 타 방법들에 비해 성능이 떨어진다는점과 JSON.stringify 메서드가 함수를 undefined로 처리한다는 점이다.

2. Lodash의 deepclone 함수

const _ = require("lodash");

let notDeep = {
  a: 1,
  b: 2,
  c: { name: "park" },
  d: () => {
    return "function is work!";
  },
};

const copy = _.cloneDeep(notDeep);

notDeep["c"].name = "Kim";

console.log(notDeep); // { a: 1, b: 2, c: { name: 'Kim' }, d: [Function: d] }
console.log(copy); // { a: 1, b: 2, c: { name: 'park' }, d: [Function: d] }
console.log(copy.d()); // function is work!

lodash 라이브러리의 cloneDeep() 메서드를 이용하는 방법이 가장 간편하다고 한다. 위 예제에서 알 수 있듯이 JSON변환을 이용한 방식에서 일어났던 함수 undefined 처리 문제가 해결된것을 알 수 있다.

이 외에도 재귀적으로 객체 트리를 따라서 내부 요소를 모두 복사를 하는 방법도 있지만, 상당히 번거롭다고 느껴서 따로 기재하진 않았다.

profile
Take me home~~~~

1개의 댓글

comment-user-thumbnail
2022년 4월 13일

면접 문제였는데 깔끔히 의문이 해결되었네요 감사합니다!!

답글 달기