본문 바로가기
개발/자바스크립트

[자바스크립트] 객체를 복사하는 다양한 방법에 대하여

by 핸디(Handy) 2021. 2. 22.

자바스크립트의 객체의 복사는 크게 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)로 나눠진다.

다음 예를 보자

let stock = { name : "apple" };

let myStock = stock;

console.log(stock.name) // apple
console.log(myStock.name) // apple

console.log(stock.name == myStock.name) // true
console.log(stock.name === myStock.name) //true

우리가 흔히 알고 있듯이, 객체의 변수가 다른 변수에 할당되어 Call by reference (참조) 가 일어났고

한 데이터를 변경하면 같은 참조를 가진 값도 변한다.

let stock = { name : "apple" };

let myStock = stock;

console.log(stock.name) // apple
console.log(myStock.name) // apple

stock.name = "Amazon"
console.log(stock.name) // Amazon
console.log(myStock.name) // Amazon

데이터가 하나 추가로 생성되어 할당되는 것이 아닌, 데이터 메모리 주소에 대한 참조를 전달하게되어 한 메모리를 공유한다. 이게 바로 Shallow Copy (얕은 복사)다.

하지만 때때로 우리는 이러한 참조를 끊고 온전히 다른 참조를 가진 변수가 필요할때가 있다.

다음 코드를 보자. stock 객체의 형태를 가져가서 원하는 stock name으로 변경했다.

let stock = { name : "apple" };

let myBestStock = stock;
let myWorstStock = stock;

myBestStock.name = "google";
myWorstStock.name = "Tesla";

console.log(myBestStock.name) // expect : google -> real : google
console.log(myWorstStock.name) // expect : Tesla -> real : google

근데 결과는..? 둘 다 google이다. Best 이자 Worst가 모두 Google 이 되어버리는 모순적인 상황이 일어났다.

이제 해결해보자.


Shallow Copy에 해서 모순적인 상황이 일어났으니 Deep Copy를 하면 상황이 해결되지 않겠는가?

맞다. Deep Copy는 Call by reference 가 아닌 Call by value (값) 이다. value(값)을 복사한다.

자바스크립트에서 제공하는 방법은 크게 2가지가 있다.

// 첫번째. Object.assign() 이용
let stock = { name : "apple" };

let myBestStock = Object.assign({},stock);
let myWorstStock = Object.assign({},stock);

myBestStock.name = "google";
myWorstStock.name = "Tesla";

console.log(myBestStock.name) // expect : google -> real : google
console.log(myWorstStock.name) // expect : Tesla -> real : Tesla

Object.assign를 이용하여 빈 객체{} 와 원하는 객체를 병합시켜버리는 것이다.

Object.assign의 상세한 내용은 MDN가서 확인해보자.

 <추가자료>
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

그리고 글을 읽다 보면 다음과 같은 문구가 나온다.

Warning( 경고 ) 라는데 위에서 잘 동작해서 값 바뀌는 거 확인했잖아. 뭐가 문제인데?? 일단 되니깐 넘어가자!!

 

// 두번째. ...연산자(spread) 이용
let stock = { name : "apple" };

let myBestStock = {...stock};
let myWorstStock = {...stock};

myBestStock.name = "google";
myWorstStock.name = "Tesla";

console.log(myBestStock.name) // expect : google -> real : google
console.log(myWorstStock.name) // expect : Tesla -> real : Tesla

ECMA6(2015)에 새롭게 나온 ... 연산자를 사용하는 방법이다. 모르면 아래 글을 읽고 와보자. 간략하게 정리해놨다.

2020/10/08 - [개발/자바스크립트] - [자바스크립트] JS다운 코드 스타일 #2. Spread 연산자


휴 그럼 이제 Deep Copy 도 끝난 건가.. 후훗 다음 작업을 해보자.... 연산자가 치기 쉬우니깐 이걸 사용하자

// 두번째. ...연산자(spread) 이용
let stock = { name : "apple", info : {ticker : "AAPL", price : 100} };

let myBestStock = {...stock};
let myWorstStock = {...stock};

myBestStock.name = "google";
myBestStock.info.ticker= "GOOGL";
myBestStock.info.price= "200";

myWorstStock.name = "Tesla";
myWorstStock.info.ticker= "AMZN";
myWorstStock.info.price= "50";


console.log(myBestStock.name, myBestStock.info.ticker, myBestStock.info.price)  
// google,AMZN, 50
console.log(myWorstStock.name, myWorstStock.info.ticker, myWorstStock.info.price) 
// Tesla ,AMZN, 50

이제 stock의 이름(name)뿐만 아니라 티커(ticker)와 가격(price)에 대한 정보를 추가해보자.

역시 우리는 Best. 구글은 가격이 2배가 됐고 Worst는 1/2이 되어버렸다. 근데 값을 찍어보니? 

name은 아름답게 나오는데 info의 property는 둘 다 AMZN, 50으로 나온다. 또 이상해져 버렸다. 

아차차. 위에서 설명한 Deep Copy 방법은 객체의 1-Depth에서만 동작하는 페이크가 있었던 것이다.

이제 아까 봤던 Warning 이 눈에 들어온다.  

we need to use alternatives..

그렇다. 우리는 alternatives가 필요하다.

첫 번째 alternative.

위에서 설명한 Deep Copy 방법이 1-Depth에서 동작한다고 했다. 그렇다 1-Depth에서는 제대로 동작하면 재귀를 써서 매 상황을 1-Depth에서 동작하자고 만들어주면 되는 거 아닌가?

만들어보기 전에 찾아보자

stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge/34749873

 

How to deep merge instead of shallow merge?

Both Object.assign and Object spread only do a shallow merge. An example of the problem: // No object nesting const x = { a: 1 } const y = { b: 1 } const z = { ...x, ...y } // { a: 1, b: 1 } The

stackoverflow.com

역시 있다.  코드를 보기 전에 따봉 1등 한 답변을 보자. 어썸한 코드가 기다리고 있을 것이다.

그렇다. ES6/7 에는 없다

두 번째 따봉 답변은 감사하게도 구현을 해주셨다.

// 객체인지 배열인지 판별
function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]); // 재귀적인 실행
      } else {
        Object.assign(target, { [key]: source[key] }); // 우리가 했던 첫번째로 복사
      }
    }
  }

  return mergeDeep(target, ...sources); // 2번째 방법도 들어가네?
}

동작을 확인해보자.

// 두번째. ...연산자(spread) 이용
let stock = { name : "apple", info : {ticker : "AAPL", price : 100} };

let myBestStock = mergeDeep({},stock)
let myWorstStock = mergeDeep({},stock)

myBestStock.name = "google";
myBestStock.info.ticker= "GOOGL";
myBestStock.info.price= "200";

myWorstStock.name = "Tesla";
myWorstStock.info.ticker= "AMZN";
myWorstStock.info.price= "50";

console.log(myBestStock.name, myBestStock.info.ticker, myBestStock.info.price);
// google, GOOGL, 200
console.log(myWorstStock.name, myWorstStock.info.ticker, myWorstStock.info.price);
// Tesla, AMZN, 50

역시 어썸하다..

형님들이 만들어 주신 DeepMerge에 대해 코드를 하나씩 따라가 보면서 확인해보라고 권해주고 싶다. 


두 번째 방법 alternative

JSON.stringfy와 JSON.parse를 이용한 방법이다.

let stock = { name : "apple", info : {ticker : "AAPL", price : 100} };

let myBestStock = JSON.parse(JSON.stringify(stock))
let myWorstStock = JSON.parse(JSON.stringify(stock))

myBestStock.name = "google";
myBestStock.info.ticker= "GOOGL";
myBestStock.info.price= "200";

myWorstStock.name = "Tesla";
myWorstStock.info.ticker= "AMZN";
myWorstStock.info.price= "50";

console.log(myBestStock.name, myBestStock.info.ticker, myBestStock.info.price);
// google, GOOGL, 200
console.log(myWorstStock.name, myWorstStock.info.ticker, myWorstStock.info.price);
// Tesla, AMZN, 50

역시나 제대로 동작한다.  내가 가끔씩 쓰는 방법이다.

생각해보자. 객체가 있는데 그걸 string으로 변경했다. ( 여기서 기존의 참조들이 전부 다 끊어진다 ). 그다음에 다시 pasring 한다. 딱 봐도 시간 잡아먹게 생겼다. 게다가 JSON 함수 자체가 원체 리소스를 많아 잡아먹는 함수라고 한다. 따라서 최적화가 필요할 때에 사용하면 철퇴를 맞을 수도 있다.


세 번째 방법으로는 loadsh의 cloneDeep를 이용하는 방법이다. -> lodash.com/docs/4.17.15#merge

 

Lodash Documentation

_(value) source Creates a lodash object which wraps value to enable implicit method chain sequences. Methods that operate on and return arrays, collections, and functions can be chained together. Methods that retrieve a single value or may return a primiti

lodash.com

이건 공식 Doc으로 대체한다. 그냥 가져다 쓰면 된다.

 


객체를 복사하는 방법 2가지 ( Object.assign, ...operator )를 통해 Shallow Copy,  Deep Copy에 대해 배웠고

해당 방법의 한계점( 1-Depth에서만 동작)을 예시를 통해 확인했다.

그에 대한 해결책 3가지 ( 재귀적 copy, JSON 함수, loadsh.cloneDeep )을 다시 확인해봄으로써

원하는 형태 Copy하는 방법에 대해 배우게 됐다.

별개로 내가 예전에 작성한 배열을 비교하는 3가지 방법에 대한 글을 읽어보길 권한다. 객체의 복사와 상관없는 내용처럼 보이지만 내부적으로 시사하는 바는 같다. 그리고 형님들의 설루션을 보고 인사이트를 얻어갔으면 좋겠다.

2020/09/27 - [개발/자바스크립트] - [자바스크립트] 배열 비교하는 3가지 방법 + 형님의 솔루션

댓글