[면접 준비 #2] Object Copy (Deep Copy VS Shallow Copy) 깊은 복사와 얕은 복사

Soozynn·2022년 6월 22일
0

면접준비

목록 보기
2/5

면접 질문 준비를 하면서 정리 겸 머릿 속에 저장하고자 얕은 복사와 깊은 복사에 대해 정리해보고자 한다.

Deep Copy & Shallow Copy

먼저, deep copy를 알기 위해 아래 코드를 살펴보자.

let num1 = 1;
const num2 = num1;


num1 = 2;

// num2와 num1의 값은 무엇일까? 
console.log(num2, num1);

// -> num2 = 1; num1 = 2;
console.log(num2 === num1);

처음에 위 코드를 보고 나는 num2가 곧 num1이라고 하였으니 num1의 값이 변경되어도 두 개의 값이 동일하게 나오지 않을까? 라고 생각하였다.

하지만 콘솔에 출력된 값은 달랐다.

여기서, 짚고 넘어가야할 것이 deep copy란 "값 자체가 대입된 다는 것"이다.
num1num2의 값을 출력해보면 알 수 있듯이 num1num2는 다른 존재이다. num1의 값인 1num2의 할당된 것이다.

즉, 위 코드는 아래와 같이 동작한다.

let num1 = 1;
const num2 = 1; // num1의 값은 1이므로 1 값 자체가 할당이 된다.

num1 = 2;

console.log(num1, num2); -> 2, 1 출력
console.log(num1 === num2); -> false 출력

그렇다면 아래 코드는 어떻게 동작할까?
const num1 = { number: 1 };
const num2 = num1;

// 둘은 위와 같이 동일한 값만을 가진 다른 존재일까?

num1.number = 2;

// num2.number 값은 무엇일까?

console.log(num1, num2);
console.log(num1 === num2);

num2.number = 2의 값을 가지게 되고, 둘의 존재는 같다고 출력될 것이다. 왜 그럴까?
이는 내가 처음 생각한 동작방식과 일치하는데, 자바스크립트는 objects(참조 값)만 위와 같이 값 자체가 바로 할당되는 것이 아닌, 메모리 주소 값이 할당이 되기 때문이다.

메모리 상에서 originObj 변수와 copyObj 변수가 같은 참조값을 가지고 있다는 것을, 오른쪽에는 특정 메모리 주소(0x10)에 위치하는 값을 보여주고있다.

const copyObj = originObj; 이런 단순 할당만으로는 위와 같은 그림처럼 originObj가 가지고 있던 참조값 0x10 만이 복사되어 들어가게 된다.

따라서 originObjcopyObj 는 같은 참조값을 가지게 되어서 Dot-Notation이나 Bracket-Notation 을 이용한 속성값 접근을 할 때 결국 같은 곳에 있는 같은 값에 접근하게 된다.

또, 다른 예제를 봐보자.

const foo = {
	name: "바보",
    age: 100,
};

let fooCopy = foo;
fooCopy.name = "나는 바보가 아니다";

console.log(foo, fooCopy);
// 	값이 어떻게 출력될지 위 예제를 바탕으로 한번 더 생각해보자.

이렇게 되면 둘은 같은 값을 뜻하게 되므로 후에 하나의 값이 변경되어지더라도 두 개의 값을 출력해보면 똑같이 변경되어 있는 것을 볼 수 있다.
이러한 방식이 바로 shallow copy이다.


그렇다면 왜 objects만 위와 같이 작동하게 되는 것일까?

자바스크립트는 먼저, 원시 값참조 값 두 개의 값으로 나뉘어진다.
우리가 흔하게 봐왔던 문자열(string)숫자(number)와 같은 값들은 원시 값이다.
하지만 functionarray, object는 모두 objects에 속한다.

원시 값 Primitive Type

  • Number (숫자)
  • String (문자)
  • Boolean (참,거짓)
  • Null
  • Undefined
  • Symbol

참조 값 Reference Type

  • Object
  • Array
  • Function



여기서 또 하나 짚고 넘어가야할 것이 객체의 불변성이다.

코딩을 하다보면, 원본 객체를 유지하면서 동일한 새로운 객체를 만들어야할 상황이 만들어질 때가 있다.
이처럼 어떤 특정 객체를 수정, 조작하고 싶을 때 그 객체 원본을 건드리기보다는 수정을 가한 새로운 객체 사본을 만들어서 원본이 아닌 수정된 사본을 사용하는 것


👉 객체 불변성을 유지하는 것이라고 한다.





다시 한번 정리해보자면
어떠한 객체를 복사하고 싶을 때가 있다고 한다면
해당 객체의 속성 값으로 원시 값이 올 수도 참조 값이 올 수도 있다.

원시 값일 경우 그대로 값을 복사해와서 서로 간의 영향이 없지만, 참조 값일 경우, 해당 값의 주소 값을 복사해오기 때문에 둘 중 하나의 값이라도 수정할 시에 참조 값 자체가 수정이 되므로 이는 의도치 않은 오류를 일으킬 수 있게 된다는 것이다. 속성의 값 중 참조 값을 그대로 복사해오고 싶으면서도 서로 간의 영향이 없는, 즉, 각기 다른 주소 값을 가진 같은 값을 만들고 싶을 경우에 깊은 복사가 필요한 것이다.

👉 기존의 것을 건들이지 않고, 새로운 객체를 만들기 위해서는 아래와 같은 방법이 있다.

얕은 복사를 하기 위해선 아래와 같은 방법이 있다.

(속성 값으로 참조 값이 있을 경우, 같은 참조 값의 주소를 갖게 된다.)

redux 에서 순수함수를 작성할 때 두 개의 방법 중 하나를 택했던 이유 또한 이번에 정리를 하면서 확고히 다질 수 있었다.. 개발을 하면서 늘 느끼지만, 내가 적는 코드 한자한자가 무얼 뜻하고 왜 그러한지에 대해 깊이 있게 뜯어보고 생각해봐야한다. 꼭..




Spread Operator{...} 문법에서

객체 리터럴의 확산

ECMAScript 제안에 대한 Rest/Spread 속성 (ES2018)은 object literals에 스프레드 속성을 추가했습니다. 제공된 개체에서 새 개체로 고유한 열거 가능한 속성을 복사한다.

Object.assign() 보다 짧은 구문을 사용하여 얕은 복제(프로토타입 제외) 또는 개체 병합이 가능하다.

let obj1 = { foo: 'bar', x: 42 };
let obj2 = { foo: 'baz', y: 13 };

let clonedObj = { ...obj1 };
// Object { foo: "bar", x: 42 }

let mergedObj = { ...obj1, ...obj2 };
// Object { foo: "baz", x: 42, y: 13 }


mdn에서 Object.assign(target, ...sources)에 대해 나온 내용을 깊이 있게 살펴보면, 조금 더 이해를 하고 넘어갈 수 있어서 아래 예제를 가져와봤다.

객체 복제

const obj = { a: 1 };
const copy = Object.assign({}, obj); 
// 첫 인자에 목표 객체, 두 번째 인자에서는 복사할 객체를 인자 값으로 넣어준다.
  
console.log(copy); // { a: 1 }

깊은 복사 주의점
Object.assign()속성의 값을 복사하기 때문에, 깊은 복사를 수행하려면 다른 방법을 사용해야한다.

만약 출처 값이 객체에 대한 참조라면 "참조 값의 주소 값"만 복사한다.
이게 무슨 뜻인지 아래 예제를 천천히 살펴보면 이해할 수 있다.

function test() {
'use strict'; // 엄격 모드

let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1); // obj1을 얕은 복사하여 obj2 변수에 할당
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}

obj1.a = 1;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
// 참조 값이 아닌 일반 primitive type에서는 객체 값이 각각 따로 복제되어 서로에게 영향이 없음.

obj2.a = 2;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}

// 하지만 참조 값을 가지는 속성은 어떻게 값이 변화하는지 아래 예제를 통해 알 수 있음.
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}
// 다른 변수이나 복사를 했을 때의 b의 속성 값은 같은 "참조 값"을 복사해왔기에 같은 값을 가리키게 되는 셈.
// 이러한 이슈를 방지하기 위해 깊은 복사를 해야하는 것이다.

// 깊은 복사 -> 여기서는 JSON.stringify와 JSON.parse를 사용하여 깊은 복사를 해주었다.
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
}
// obj1의 속성 값 중 객체인 값을 변화해주어도 obj3는 아무 변화가 없음을 알 수 있다.
// 이는 곧 객체의 주소 값이 아닌 값 자체를 복사해왔음을 알 수 있다.

test();

또 위 예제를 자세히 봤으면 알 수 있듯이 Object.assign(target, 복사할 객체)의 첫 번째 인자 값이 목표 객체 자체도 변경이 된 것을 확인할 수 있다.

객체 병합

const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 목표 객체 자체가 변경됨.

같은 속성을 가진 객체 병합

const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }

같은 키를 가진 속성의 경우 매개변수 순서에서 더 뒤에 위치한 객체의 값으로 덮어쓰는 것도 알 수 있다.

이 외 더 자세한 내용은 mdn 참고하기 !

그렇다면, 깊은 복사를 하기 위해선 어떤 방법이 있을까? 🤔

  • for 문을 이용한 재귀적인 깊은 복사를 구현하거나
function deepCopy(origin, copy = {}) {
  for (const key in origin) {
    if (origin.hasOwnProperty(key)) {
      if (typeof origin[key] === 'object') {
        copy[key] = Array.isArray(origin[key]) ? [] : {}
        deepCopy(origin[key], copy[key])
      } else {
        copy[key] = origin[key]
      }
    }
  }
  return copy
}
  • JSON으로 stringify 시킨 후 다시 parse하는 과정을 거치면
function deepCopy(origin) {
  return JSON.parse(JSON.stringify(origin))
}

비로소 완전한 참조가 끊기는 깊은 복사가 구현된다.
- JSON.stringify
- JSON.parse
(단, 이 방법의 경우 성능상의 문제가 있을 수 있다고 한다.)

  • 또는 lodashcloneDeep(obj)을 이용

추가적으로 보면 좋을 영상 ) 드림코딩

0개의 댓글