[JS] 객체의 얕은 복사 vs 깊은 복사

colki·2021년 4월 25일
0
post-custom-banner

호랑이 책을 한 번 완독했지만, 진도에 맞춰서 부분적으로 읽다보니 빠뜨린 부분이 많다.
그래서 얕은 복사, 깊은 복사 파트가 내 지식에 아예 없었는데
extend메서드를 구현해보면서 사용했던 Object.assign이 얕은 복사라 깊은 복사에 대해서 알아보라는 얘기를 듣고 공부하고 정리해야지 마음 먹은걸, 오늘에서야 실천하게 됐다. 🙄

이전 포스팅에서도 객체의 복사방법에 대해서 다뤘었는데, 그런데 말입니다.

어떤 상황에서 객체를 왜 복사할까?

함수형 프로그래밍에서는 값을 변형하는 과정에서 원래의 값, 초기화값을 바꾸지 않는다.
외부의 상태를 변경시키지않고 인자로 받은 값도 변경하지 않는다.

함수형 프로그래밍에서도 유념해야 할 부분이기도 하지만, 값으로 전달 받은 원본객체는 변경하지 말아야 하는 경우가 많다.

const user = {
  name: 'colki',
  age: 20
};

const changeName = function(user, newName) {
  const newUser = user;
  newUser.name = newName;
  return newUser;
};

const user2  = changeName(user, 'Hany');

if (user === user2) {
  console.log('유저 정보가 일치합니다');
} else {
  console.log('유저 정보가 일치하지 않습니다');
}

console.log(`user.name: ${user.name}`);
console.log(`user2.name: ${user2.name}`);

원본은 변경하지 않고, changeName()을 사용해서 새로운 객체를 생성하고 싶다. 하지만 위 코드에서 콘솔에는 다음과 같은 값이 출력된다.

"유저 정보가 일치합니다"
"user2.name: Hany"
"user2.name: Hany"

객체가 같은 주소값을 바라보고 있고 변수 자체가 동일하게 생성되었다.
바뀌기 전후 차이를 보여줘야 하거나, 서로 다른 객체가 되게 해야 한다면 코드를 고쳐야 한다.

const user = {
  name: 'colki',
  age: 20
};

const changeName = function(user, newName) {
  return {
    name: newName,
    age: user.age
  };
};

const user2  = changeName(user, 'Hany');

if (user === user2) {
  console.log('유저 정보가 일치합니다');
} else {
  console.log('유저 정보가 일치하지 않습니다'); 
}

console.log(`user.name: ${user.name}`);
console.log(`user2.name: ${user2.name}`);

이번에는 changeName함수가 새로운 객체{ : }를 리턴하는 형태이기
때문에 서로 다른 객체를 만들 수 있었다.

"유저 정보가 일치하지 않습니다"
"user.name: colki"
"user2.name: Hany"

하지만 changeName함수안에서 user.age로 기존 객체의 프로퍼티를 직접
참조하고 있으므로 효율적인 방법은 아니다. 만약 복사해야 하는 프로퍼티가 몇십개라면 그 이상이라면 이건 말도 안되는 짓이다.

그래서 이럴 때 객체의 프로퍼티 개수에 상관없이 복사하는 함수를 만들어서
재사용하는 편이 좋겠다.

객체를 복사하는 방법으로는 크게 2가지로 나눌 수 있는데 바로 얕은 복사와 깊은 복사이다.

얕은 복사_Shallow Copy


Object.assign

const person = {
  name: 'hany',
  age: 20,
};

const person2 = Object.assign({}, person);

메서드를 이용하면 간단하게 객체를 복사할 수 있다.
그러면 서로 다른 객체는 프로퍼티를 변경하더라도 영향을 주지 않는게 확실할까?

(1)원본 객체의 프로퍼티 변경시 -----------
person.name = 'colki';

console.log(person); // {name: "colki", age: 20}
console.log(person2); // {name: "hany", age: 20}

(2) 사본 객체의 프로퍼티 변경시 -----------
person2.age = 18;

console.log(person); // {name: "hany", age: 20}
console.log(person2); // {name: "hany", age: 18}

각각 속성을 변경했을 때 서로의 값에 영향을 주지 않는 것을 확인했다. 그런데 value가 만약 숫자, 문자열 등의 원시형 데이터가 아닌 참조형 데이터라면 어떨까?

const person = {
  favorite: {food: 'bread'},
  study: function () {console.log('객체 공부')}
};

const person2 = Object.assign({}, person);

(1)원본 객체의 프로퍼티 주소값 변경시 -----------
person.favorite = 'too many';

console.log(person); // {favorite: 'too many', study: ƒ}
console.log(person2); // {favorite: 'bread', study: ƒ}

(2) 사본 객체의 프로퍼티 주소값 변경시 -----------

person2.study = function () { console.log('배고프다') };

console.log(person);
// { 
//  favorite: {food: 'bread'}, 
//  study: {console.log('객체 공부')}
// }

console.log(person2); 
// { 
//  favorite: {food: 'bread'}, 
//  study: {console.log('배고프다')}
// } 

참조형 데이터가 value인것도 아무 문제가 없다. 서로 다른 객체인 게 분명한 것 같다.
그런데 프로퍼티의 프로퍼티 . 주소값의 주소값을 참조하는 경우에는 얘기가 달라진다.

const person = {
  favorite: {food: 'bread'},
  study: function () {console.log('객체 공부')}
};

const person2 = Object.assign({}, person);

(1)원본 객체의 프로퍼티의 프로퍼티 변경시 -----------
person.favorite.food = 'ice-cream';

console.log(person);
// {favorite: {food: "ice-cream"}, study: ƒ}

console.log(person2); 
// {favorite: {food: "ice-cream"}, study: ƒ}

(2) 사본 객체의 프로퍼티의 프로퍼티 변경시 -----------
person2.favorite.food = 'pasta';

console.log(person);
// {favorite: {food: "pasta"}, study: ƒ}

console.log(person2); 
// {favorite: {food: "pasta"}, study: ƒ}

직접 속한 프로퍼티에 대해서는 완전히 새로운 데이터가 만들어진 반면에 한 단계
더 들어간 내부 프로퍼티(프로퍼티의 프로퍼티...) 들은 그대로 참조하게 된다.
원본과 사본 어느 쪽을 바꾸더라도 다른 한쪽의 값이 바뀌어 버린다.

이렇게 객체 프로퍼티 내부의 프로퍼티들은 복사하지 못하는 복사를 얕은 복사라고 한다.

다음 함수도 Object.assign과 마찬가지로 얕은 복사를 수행하는 코드이다.

function copyObject(targetObj) {
  const copiedObj = {};
  
  for (var key in targetObj) {
    copiedObj[key] = targetObj[key];
  }
  
  return copiedObj;
}


const obj = { a: 1, b: {c:2} };
const obj2 = copyObject(obj);

obj2.b.c = 10;
console.log(obj);
// { a: 1, b: {c: 10} }

console.log(obj2);
// { a: 1, b: {c: 10} } 

이런 현상이 생기지 않도록 객체 내부의 프로퍼티의 프로퍼티까지도 복사하는
깊은 복사가 필요한 것이다.

깊은복사_Deep Copy


객체의 값들이 참조형 데이터인데 그 내부적인 프로퍼티들 또 이 또 존재한다면 내부의 내부 프로퍼티까지 재귀적으로 완전히 복사해야만 독립적인 새로운 객체를 생성할 수 있다.

객체 내부를 순회하며 만약 요소가 객체일 경우 다시 그 내부를 순회하며
복사하게끔 만들어 줘야 원본과 사본이 서로 다른 객체를 참조하게 된다.

앞서 얕은 복사를 수행했던 코드를 수정하면 깊은 복사를 만들어낼 수 있다.

① 깊은복사 재귀함수 이용


function copyObject(targetObj) {
  let copiedObj = {};
  
  if (typeof targetObj === 'object' && targetObj !== null) {
    for(var key in targetObj) {
      copiedObj[key] = copyObject(targetObj[key]);
    } 
  } else {
    copiedObj = targetObj;
  }
        
  return copiedObj;
}

const obj = { a: 1, b: {c: 2} };
const obj2 = copyObject(obj);

obj2.b.c = 10;

console.log(obj); // { a: 1, b: {c: 2} }
console.log(obj2); // { a: 1, b: {c: 10} }

② 깊은복사 JSON 이용 😮
객체를 문자열로 전환했다가 다시 객체로 전환하는 방법.
간단하고 심플하게 구현할 수 있지만,
하지만 메서드나 __proto__ , getter/ setter 등 JSON으로 변경할 수 없는 프로퍼티들은 무시하는 단점이 있다.

그래서 데이터를 저장한 객체 등 순수한 정보를 다룰 때 용이하게 쓸 수 있다.


function cloneObj(targetObj) {
  return JSON.parse(JSON.stringify(targetObj));
}

const obj = { a: 1, b: {c: 2} };
const obj2 = cloneObj(obj);

obj2.b.c = 10;

console.log(obj); // { a: 1, b: {c: 2} }
console.log(obj2); // { a: 1, b: {c: 10} }

Reference 코어 자바스크립트, ColkiVelog_ fill메서드

profile
매일 성장하는 프론트엔드 개발자
post-custom-banner

0개의 댓글