[JS] 깊은 복사 & 얕은 복사

위영민(Victor)·2021년 8월 11일
2

개요

이전 JS, 원시타입 & 객체 포스팅에서 언급했듯이, JS는 유독 다양한 여러 Object 를 지원하기도하고, 조금이라도 복잡한 데이터 구조가 형성 된다하면, 배열이나 객체에 데이터를 저장해놓는 작업이 많다.
복사라는 개념이 이런 관점에서 단순한 원시타입에 대해서는 일반적으로 생각할 수 있는 복사가 될 수 있지만, 객체수준부터는 복사에 대한 생각을 조금 깊게 해볼 필요가 있다.

본론

그전에 확인 할 것

값 복사

  • 변수는 모두 메모리에 저장된다.
  • 값 복사는 각각 독립된 메모리주소를 가지게 된다.
  • 그래서, 복사를 한 후, 조작이 있더라도 각 데이터에 정보가 훼손되지 않는다.

참조(메모리 주소) 복사

  • 값은 하나인데 변수는 여러 개일 수 있다.
  • 이것을 참조라고 한다.
  • JS에서는 Number, String, Boolean 정도의 원시타입을 제외한 모든 객체는 메모리 주소에 대한 복사가 일어난다.
  • 이것은 즉, 동일한 하나의 값에 대해, 여러 이름(식별자)으로 복사 개념이 일어나지만, 실질적으로는 하나의 값에대해 동일한 메모리의 주소값을 가지게 된다.
  • 그로인한, 이후 데이터 조작에 대해, 생각치 못한 이슈들이 발생하게 된다.

📍 문제 상황 연출

{
  /**
   * 값 복사
   * - 분리된 메모리 공간에, 데이터를 각각 저장(공유 X) 💡
   */
  let num1 = 1;
  let num2 = num1;

  num2 = 2;
  console.log(`num1 : ${num1}, num2 : ${num2}`);
  // num1 : 1, num2 : 2
  // 🔍 원본 데이터 값 보존

  /**
   * 객체 복사
   * - 동일한 메모리 공간 내, 주소값 데이터에 대해, 동일한 주소값 데이터를 각각 저장(공유 O) 💡
   */
  let person = {
    name: "min",
    job: "developer",
    salary: 5000,
  };
  let clonePerson = person;
  clonePerson.salary = 6000;

  console.log(`Salary - Person : ${person.salary}, ClonePerson : ${clonePerson.salary}`);
  // Salary - Person : 6000, ClonePerson : 6000
  // 🔍 원본 데이터 값 변경됨
}

📍 참조타입 복사 - Shallow Copy(얕은 복사)

참조타입 복사는 크게 Shallow Copy(얕은 복사)Deep Copy(깊은 복사)로 나뉘는데, 이 중 Shallow Copy 에 대해 먼저 알아본다.

위에 상황에서 원시타입이 아닌 일반 객체에 대한 단순 대입복사로 인한 문제를 해결하기 위해, 그리고 그 객체의 내부 데이터가 모두 원시타입 값으로 구성되어 있는 경우, Shallow Copy 로 충분히 해결 가능하다.

JS에서 Shallow Copy(얕은 복사)를 하는 방법은 몇 가지 방법들이 있지만, ES6+ 이후 기준에서 흔히, 그리고 비교적 간단 방법은 다음과 같다.

  • Object.assgin() 사용
/**
   * Shallow Copy - Object.assign() 사용
   */
  let myCat = {
    name: "Cash",
    age: 5,
    weight: "3kg",
  };
  let myCat2 = Object.assign({}, myCat);
  myCat2.name = "Rolly";
  myCat2.age = 2;

  console.log(myCat); // { name: 'Cash', age: 5, weight: '3kg' }
  console.log(myCat2); // { name: 'Rolly', age: 2, weight: '3kg' }
  • Spread Operator 사용
 /**
   * Shallow Copy - Spread Operator 사용
   */
  let myCaptin = {
    name: "Iron Man",
    rank: 1,
    gender: "M",
  };
  let myCaptin2 = { ...myCaptin };
  myCaptin2.name = "Captin America";
  myCaptin2.rank = 2;

  console.log(myCaptin); // { name: 'Iron Man', rank: 1, gender: 'M' }
  console.log(myCaptin2); // { name: 'Captin America', rank: 2, gender: 'M' }

💡 [참고]
객체가 아닌, 배열(Array)에 대해서도, Spread Operator 나 Array Object 내장 메소드인 Array.prototyp.slice( ) , Array.prototype.map( ) 같은 메소드들도, 기존 배열 데이터에 대해 조작후 새로운 배열자체를 반환하는 개념이라 객체에서 개념과 똑같은 Shallow Copy 가 가능하다.

두 방식을 보면, 맨 처음 객체를 대입복사 했을 경우와 비교했을 때, 값 복사처럼 각 변수에 대한 데이터가 보존되는 것처럼 보인다.

방금 말에서 ~되는 것처럼 보인다.라고 한 것에는 이유가 있다.
다음 상황을 보자.

 /**
   * Shallow Copy 에 함정
   */
  let myCar = {
    name: "부릉이",
    price: 7000,
    specialMode: {
      mode: "Fly",
    },
  };
  let myCar2 = { ...myCar };
  myCar2.name = "따릉이";
  myCar2.price = 4000;
  myCar2.specialMode.mode = "Dive";

  console.log(myCar); // { name: '부릉이', price: 7000, specialMode: { mode: 'Dive' } }
  console.log(myCar2); // { name: '따릉이', price: 4000, specialMode: { mode: 'Dive' } } 🔍

// 두 car에 대해 프로퍼티 비교
  console.log(myCar.name === myCar2.name); // false
  console.log(myCar.specialMode === myCar2.specialMode); // true 🔍
  console.log(myCar.specialMode.mode === myCar2.specialMode.mode); // true 🔍

다 좋은데, 마지막, specialMode(객체) 프로퍼티 내부에, mode 프로퍼티가 Shallow Copy 를 하고 myCar2 에 specialMode 를 변경했음에도 불구하고, myCar 와 myCar2 두가지 car 모두 mode 가 "Dive" 모드로 변경된 것을 볼 수 있다.

❓ 어떻게 된거지 ?

이게 바로 Shallow Copy 사용시 주의점이다.

MDN에 Spread Operator 문서를 보면 이런 설명이 있다.

이 문서는 Array 복사에서 Spread Operator 사용시 설명이라 그렇지, 아래에 보면, Object.assign( ) 도 동일한 개념이라고 말하고 있다.

중요한 건, 깊이(Depth) 가 1 이라는 말이 무엇이냐면, 쉽게 말해, 바로 위 예제에서, myCar 객체에 내부 프로퍼티에 대해, Depth 라는 표현을 빗대어 설명하자면

더 쉽게 이해하려면, 가장 바깥 껍데기만 복사되는 복사가 Shallow Copy(얕은 복사)라고 생각할만 하다.

이렇게 볼 수 있지만, 좀 더 상세하게 이해하자면

  • Shallow Copy는 새로운 객체에 원본 객체의 프로퍼티의 값을 정확히 복사한다.

  • 단, 먄약 프로퍼티의 값이 원시타입이 아닌, 또 "객체(Object)"라면 객체의 주소를 복사한다. 🔍

  • 즉, 복사된 객체는 원본 객체와 동일한 프로퍼티와 값들을 "새롭게" 가지지만, 주소가 복사된 프로퍼티는 새로운 형태가 아닌 "같은 것(객체의 메모리 주소)"을 공유하는 하게 된다..

그래서 결과적으로, 맨 처음 일반 객체의 대입복사처럼, 객체 내부에 또 다른 "객체함수 프로퍼티에 대해서는, 같은 데이터에 대해 메모리 주소를 공유하기 때문에, 완벽한(Deep 한) 복사가 이뤄지지 않는 것이다.

📍 참조타입 복사 - Deep Copy(깊은 복사)

Deep Copy(깊은 복사)는 원본 객체를 완전히 복사하는 것이다.
방금 전에 살펴본 Shallow Copy에 치명적인 함정(Depth에 따른 완벽한 복사가 이뤄지지 않는 것)이 있는 것이 아닌, 말 그대로 새로운 메모리 공간을 확해 생성하게 되는 것이다.

내부적인 예로 들자면 방금 Shallow Copy 문제의 상황과 비교했을 때, Deep Copy 는 다음의 내부구조를 가진다고 예를 들 수 있다.

객체나 메소드처럼 프로퍼티에 추가적인 Depth 가 있다해도, 완벽히 독립된 새로운 메모리 공간 확보

Deep Copy(깊은 복사)를 구현하는 방법에는 크게 다음 방법들이 알려져 있다.

1️⃣. JSON.parse( ) 와 JSON.stringify( ) 함수 사용

  • JSON.stringify 함수를 이용해서, Object 전체를 문자열로 변환 후
  • 다시 JSON.parse 함수를 이용해서 문자열을 Object 형태로 반환
  • 그러면, 문자열로 변환하는 순간 참조 값이 끊기기 때문에 "새로운" Object로 만들어 사용할 수 있다. 🔍
  • 하지만, JSON 함수는 엄청난 리소스를 잡아먹는 함수이다. 성능이 좋지 않은 부분을 고려해야 한다.
 /**
   * Deep Copy(깊은 복사) - JSON 함수를 이용
   */
  let myFruit = {
    name: "Apple",
    price: 1000,
    characteristic: {
      favor: "Sweet",
    },
  };
  let myFruit2 = JSON.parse(JSON.stringify(myFruit));
  myFruit2.characteristic.favor = "Very Sweet";

  console.log(myFruit); // { name: 'Apple', price: 1000, characteristic: { favor: 'Sweet' } }
  console.log(myFruit2); // { name: 'Apple', price: 1000, characteristic: { favor: 'Very Sweet' } } 🔍

마지막에 characteristic(객체 프로퍼티) 프로퍼티가 Depth 가 1이 아님에도 불구하고, myFruit 과 myFruit2 가 완벽히 분리되어 사용되는 것을 볼 수 있다.

위에 언급한 JSON 함수를 이용했을 때 문제점은 다음과 같다.

  • JSON 데이터에는 함수 데이터 타입이 없기 때문에 함수 속성들은 누락된다.
/**
   * Deep Copy - JSON 함수 사용시 function 누락 현상
   */
  let myObj = {
    name: "obj",
    sayHi: function () {
      console.log("Hello Javascript");
    },
  };
  let copyMyObj = JSON.parse(JSON.stringify(myObj));
  console.log(myObj.sayHi); // [Function: sayHi]
  console.log(copyMyObj.sayHi); // undefined 🔍
  • 이 밖에도 Object Tree 내에 순환 참조가 있는 경우, stringify 메소드에서 TypeError: Converting circular structure to JSON 이라는 오류가 발생한다고 한다.

2️⃣. Lodash 의 cloneDeep 함수를 사용

  • Lodash 는 JS 고차함수 및 함수형 라이브러리다.
  • 그 중 _.cloneDeep(value) 함수를 사용하는 것이다.
/**
   * Deep Copy(깊은 복사) - lodash 에 cloneDeep 함수를 이용
   * - lodash 라이브러 import 필요
   */

  const _ = require("lodash");
	
  let myFruit = {
    name: "Apple",
    price: 1000,
    characteristic: {
      favor: "Sweet",
    },
    sayHi: function () {
      console.log("과일이 인사를 하네요.");
    },
  };

  let myFruit2 = _.cloneDeep(myFruit);
  console.log(myFruit);
  // {
  // 	name: 'Apple',
  // 	price: 1000,
  // 	characteristic: { favor: 'Sweet' },
  // 	sayHi: [Function: sayHi]
  // }

  console.log(myFruit2);
  // {
  // 	name: 'Apple',
  // 	price: 1000,
  // 	characteristic: { favor: 'Sweet' },
  // 	sayHi: [Function: sayHi]
  // }

3️⃣. 직접 구현(재귀)

  • 재귀적으로 Object Tree를 따라서 말단 노드까지 모두 복사를 해주는 함수가 필요하다.
  • 여러 많은 포스팅을 보다보면 대부분 다음과 같은 방식으로 구현한다.
  /**
   * Deep Copy - 재귀를 이용한 직접 구현
   */

  function clone(source) {
    var target = {};
    for (let i in source) {
      if (source[i] != null && typeof source[i] === "object") {
        target[i] = clone(source[i]); // resursion
      } else {
        target[i] = source[i];
      }
    }
    return target;
  }
  let myFruit = {
    name: "Apple",
    price: 1000,
    characteristic: {
      favor: "Sweet",
    },
    sayHi: function () {
      console.log("과일이 인사를 하네요.");
    },
  };
  const myFruit2 = clone(myFruit);

  console.log(myFruit); 
  // {
  // 	name: 'Apple',
  // 	price: 1000,
  // 	characteristic: { favor: 'Sweet' },
  // 	sayHi: [Function: sayHi]
  // }

  console.log(myFruit2); 
  // {
  // 	name: 'Apple',
  // 	price: 1000,
  // 	characteristic: { favor: 'Sweet' },
  // 	sayHi: [Function: sayHi]
  // }

결론

[TMI]
결국, 지금까지 JS에서 "복사"에 대해 잘못 알고 있었던(나같이 😁) 대부분의 사람들이 Depth가 1인 데이터에 대해 Shallow Copy 만 해서, "음 ~ 이상없네"를 생각하고 개발을 해왔다면, 정작 중요한 건 Deep Copy(깊은 복사)이다.

그건 알아야 할 것 같다.
객체 복사를 함에 있어, 무조건 "Shallow Copy 나쁜거다" 는 잘못된 생각이다. 상황에 따라 Shallow Copy 정도만 진행해서 객체를 복사해도 충분한 상황이 있고, Deep Copy를 통해 주의깊게 데이터의 복사가 이뤄져야 하는 상황도 있을 것이다.
이를 잘 판단하며 사용하는 것이 중요하겠다.

이번 주제를 조사하다, 어떤 분의 포스팅에서 복사가 중요한 이유를 소신껏 적어주셨는데 다음과 같다.

예측할 수 없는 버그를 최소화하기 위해서

동의한다. JS 자체가 도입부에 말했던 동적 타이핑언어이고, 런타임 언어이기 떄문에, 도중에 내가 생각치도 못한 곳에서의 데이터가 조작이 일어날 수 있고, 이를 또 디버그 하기에도 프로그램이 커지면 커질 수록 발견하기도 힘들 것이니, 애초에 개발할 때 이번에 정리한 Shallow Copy 나 Deep Copy 를 잘 고려해서 개발을 하면 좋을 것 같다.

다음은, 이터러블(Iterable)객체에 대해 알아본다.


참고

profile
블로그 이사했습니다 :) 👉 https://www.youngminss-log.com/blog

0개의 댓글