Javascript | 얕은 복사와 깊은 복사(Part.1)

shin6403·2021년 1월 10일
7
post-thumbnail

# Intro

우선 이 주제를 이해하려면 자바스크립트의 데이터 타입(기본형,참조형)에 대해서 이해하여야 한다.
필자는 '코어자바스크립트' 책을 보면서 많은 정보를 얻었지만, 위와 같은 이해가 부족하다면 아래 링크를 읽어보길 바란다.

자바스크립트 기본형, 참조형 설명

간단하게 설명하자면 자바스크립트에서 데이터 타입은 위와 같이 되어있다.
여기서 우리가 중점적으로 봐야할 부분은 **참조형(reference type)**이다.
객체는 기본적으로 참조형(reference type)이다.

# 얕은 복사? 깊은 복사? 🧐

1. 얕은 복사 📄

  • **얕은 복사(Shallow Copy)**는 바로 아래 단계의 값만 복사하는 방법
  • 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사한다는 의미이다.

이게 무슨 소리일까? 이해하기 쉽게 하나하나 코드를 보면서 이해 해보도록 하자.

//기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)
const copyObj =(target)=>{
  const result = {};
  for(let prop in target){
  result[prop] = target[prop]
   }
  return result;
}

위 함수는 얕은 복사만 수행한다. 위에서 설명한대로 프로퍼티만 복사할 때 그 주솟값만 복사한다는 의미이다.
그렇게되면 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 된다.
즉, 사본을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀐다.

//중첩된 객체에 대한 얕은 복사
const user ={
  name:'sewon',
  urls:{
      portfolio:'https://github.com/shinsewon',
      instagram:'https://www.instagram.com/s_sewon'
   }
};

const user2 = copyObj(user);

user2.name = 'yejin';
console.log(user.name === user2.name); //false

user.urls.portfolio = 'https://velog.io/@shin6403'
console.log(user.urls.portfolio === user2.urls.portfolio); //true

user2.urls.instagram = ''
console.log(user.urls.instagram === user2.urls.instagram); //true

위 코드에서 사본인 user2의 name 프로퍼티를 바꿔도 user의 name 프로퍼티는 바뀌지 않았다.
반면 그 아래 portfolio,instagram 부분은 원본과 사본 중 어느 한쪽을 바꾸더라도 다른 한쪽의 값도 함께 바뀐 것을 확인할 수 있다.

즉, user 객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한단계 더 들어간 urls의 내부 프로퍼티들은 기존 데이터를 그대로 참조하는 것이다.

이런 현상이 발생하지않게 하려면 user.irl 프로퍼티에 대해서도 불변 객체로 만들 필요가 있다.

2. 깊은 복사 📑

  • **깊은 복사(Deep Copy)**는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법
//기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)
const copyObj =(target)=>{
  const result = {};
  for(let prop in target){
    result[prop] = target[prop]
   }
  return result;
}

위 함수는 얕은 복사만 수행한다. 위에서 설명한대로 프로퍼티만 복사할 때 그 주솟값만 복사한다는 의미이다.
그렇게되면 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 된다.
즉, 사본을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀐다.

//중첩된 객체에 대한 깊은 복사
const user ={
   name:'sewon',
   urls:{
     portfolio:'https://github.com/shinsewon',
     instagram:'https://www.instagram.com/s_sewon'
  }
};

const user2 = copyObj(user);
user2.urls = copyObj(user.urls);

user.urls.portfolio = 'https://velog.io/@shin6403'
console.log(user.urls.portfolio === user2.urls.portfolio); //false

user2.urls.instagram = ''
console.log(user.urls.instagram === user2.urls.instagram); //false

위 코드에서 urls 프로퍼티에 copyObj 함수를 실행한 결과를 할당했다.
이제 urls 프로퍼티의 내부까지 복사해서 새로운 테이터가 만들어졌으므로 밑부분에 portfolio,instagram 부분의 값이 서로 다르다는 결과를 얻을수 있다.

어떤 객체를 복사할 때 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들고자 할 때, 객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사되지만, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야한다.

3. 얕은 복사 -> 깊은 복사 리팩토링 📝

위와 같은 개념을 바탕으로 copyObj 함수를 깊은 복사 방식으로 고친 코드 예시 두가지를 적어놓았고 다음과 같다.

예시 (1)

//객체의 깊은 복사를 수행하는 범용 함수
const copyObj =(target)=>{
  const result = {};
  if(typeof target === 'object' && target !== null){ //(1)
  for(let prop in target){
    result[prop] = copyObj(target[prop])
    }
  } else{
  result = target //(2)
 }
  return result
};

// (1)번째 줄에서 target !== null 조건을 덧붙인 이유는 typeof 명령어가 null에 대해서도 'object'를 반환하기 때문이다. (자바스크립트 자체 버그이다.)

(1)번째 줄에서 target이 객체인 경우 내부 프로퍼티들을 순회하며 copyObj함수를 재귀적으로 호출하고,
객체가 아닌 경우에는 (2)번째 줄에서 처럼 target을 그대로 지정하게끔 했다.

이 함수를 사용해 객체를 복사한 다음에는 원본과 사본이 서로 완전히 다른 객체를 참조하게 되어 어느 쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않는다.

예시 (2)

객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸는 것이다.
이 방법은 단순함에도 불구하고 잘 동작한다.
httpRequest로 받은 데이터를 저장한 객체를 복사할 때 순수한 정보만 다룰때 활용하기 좋은 방법이다.

//JSON을 활용한 간단한 깊은 복사
const copyObjVioJSON = (target)=>{
  return JSON.parse(JSON.stringify(target)) 
}

const obj ={
	a:1,
  	b:{
   	  c: null,
  	  d:[1,2],
     	  func1:function(){console.log(3);}      
        },
  	func2:function(){console.log(4);}
    }

const obj2 = copyObjVioJSON(obj)

obj2.a=3;
obj2.b.c=4;
obj.b.d[1]=3;

console.log(obj) //{a:1, b:{c:null, d:[1,3], func1:f()},func2:f()}
console.log(obj2) //{a:3, b:{c:4, d:[1,2], func1:f()},func2:f()}

3. ES6 'Object.prototype.assign'과 'spread operator'

Object.prototype.assign은 ES6 문법에 새로 생긴 문법이다. spread operator은 정확히 언제나왔는지 모르겠다.
Object.prototype.assign,spread operator은 얕은 복사일까 깊은 복사일까? .

아래 코드를 봐보도록 하자.

const obj1 = {name: 'sewon'}
const obj2 = Object.assign({}, obj1)
const obj3 = {...obj1}

obj1.foo = 'yejin'
console.log(obj2) // { name: 'sewon' }
console.log(obj3) // { name: 'sewon' }

console.log(obj1 === obj2) // false
console.log(obj1 === obj3) // false
console.log(obj2 === obj3) // false

이 두가지 외에도 객체를 스트링으로 바꿔서 할당 후 다시 객체로 바꾸는 방법도 ES5 이하에선 쓰였지만 이제는 추천하지 않는다.
두가지를 이용해서 객체를 복사하면 원본 객체인 obj1를 수정해도 obj2, obj3의 값은 변하지 않는다.
===로 비교해봐도 세가지 객체 모두 다른 메모리 참조값을 가지고 있는걸 확인할 수 있다.
많은 사람들이 이러한 현상으로 깊은 복사(Deep copy)라고 오해하는 경우가 많지만 정답은 얕은 복사이다.

다른 코드를 봐보도록 하자.

const obj4 = {
  a: 1,
  innerObj: {
    b: 3
  }
}
const obj5 = {...obj4}

obj4.a = 6
obj4.innerObj.b = 6
console.log(obj5) // { "a": 1, "innerObj": { "b": 6 } }

console.log(obj5 === obj4) // false
console.log(obj4.innerObj === obj5.innerObj) // true

obj4는 내부에 innerObj라는 객체가 있고, 이를 깊은 복사를 이용해도 innerObj 의 경우 같은 참조를 가리킨다.
결국엔 depth가 있는 객체도 참조타입이기 때문이다. 하위 객체는 완전히 복사된게 아니다.
실제 개발을 하다보면 depth가 깊게 들어간 객체를 은근 자주 만날 수 있다.
이를 해결하는 방법은 이론상으론 쉽다.

깊이가 얼마나 깊든 재귀적으로 깊은 복사를 하면 된다.

간단하지만 작업이지만 자체 구현은 귀찮은 작업이기도 하다.
이럴때 사용하는 오픈소스가 있는데 lodash이다.

lodash는 자바스크립트 고차함수 집합 및 함수형 라이브러리이다.

깊이가 깊은 객체를 완전 복사하려면 lodash_.cloneDeep을 사용하길 추천한다.
lodash엔 이미 _.clone이 있는데, 이를 재귀적으로 수행해주는 함수다.
깊은 복사 외에도 유용한 함수가 많고, Javascript 개발자라면 이미 많이 사용하고 있다.
lodash는 아래 링크를 참고하길 바란다.

https://lodash.com/docs/4.17.11#cloneDeep

결론: 완전히 객체를 복사할려면 lodash 같은 라이브러리의 cloneDeep 등의 함수를 이용하자. 또는 재귀적인 clone 함수를 직접 구현하자.

다음편에선 그럼 lodash에 대한 자료를 가지고 오기로 한다!

profile
생각하는대로 살지 않으면, 사는대로 생각하게 된다.

1개의 댓글

comment-user-thumbnail
2021년 1월 24일

도움 많이 되었어요~~ 감사합니다^^

답글 달기