ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] 객체의 복사 (깊은 복사, 얕은 복사)
    Front-End(Web)/Javascript 2021. 3. 12. 17:43
    반응형

    🤔 서론

    알고리즘을 풀거나, 웹페이지에서 배열상태를 최신화할때 경험했던 일이다. "나는 사본을 수정했는데, 왜 원본도 같이 바뀌지???"

    자바스크립트에서 배열이나 객체를 복사하는데 다양한 방법이 있으며, 이들은 조금씩 다른 원리를 가지고 있던 것이다!


    📒 복사

    복사는 원본가 모두 같은 내용으로 사본을 만드는 작업이다. 원본과 사본이 각각 존재하며, 이들의 관계에 따라 복사종류를 구분할 수 있다.

     

    - 얕은 복사

    사본을 만들어내지 않고 원본을 참조하도록 소위 "복사한 척" 을 하는 것이다.

     

    1. 참조 할당

    const arr1 = [1,2];
    const arr2 = arr1;
    
    arr2[0] = 3;
    
    console.log(arr1[0], arr2[0]);		// 3 3

    가장 기본적인 형태이다. arr2 변수에 arr1 객체를 할당한 모습이다.

    두 다른 변수가 같은 객체를 참조하기 때문에, 사본의 수정이 원본까지 영향을 미친다. 사본 복사/수정에서 가장 많이 범한 실수였다.

     

    2. Object.assign()

    const obj1 = {
      age: 20,
      name: {
        first: "taeng",
        last: "kim",
      },
    }
    
    const obj2 = Object.assign({}, obj1);
    
    obj1.age = 200;
    obj1.name.last = "park";
    
    console.log(obj2.age);		// 20 (no change)
    console.log(obj2.name.last);	// "park" (changed!!!)

    Object.assign() 은 객체를 연결하여 새로운 객체를 만드는 메서드이다. 이처럼, 빈 객체와 원본객체를 입력하면 사본이 반환될 것이다.

    문제는, obj.name.last 에서 확인할 수 있다. 사본(obj2)는 1차 키값은 영향을 받지 않았지만, 2차 키값이 수정되면서 완벽한 복사가 아님을 알 수 있다.

     

    3. Array.slice()

    const arr1 = [1,2,3];
    const arr2 = arr1.slice();
    
    arr2[2] = 4;
    
    console.log(arr1);	// [1,2,3]
    console.log(arr2);	// [1,2,4]

    slice()는 통상 인덱스를 입력하여 배열을 잘라내는 메서드이다. 위처럼, 인덱스가 없으면 배열 전체를 복사한다.

     

    const arr1 = [1,2,[3,4]];
    const arr2 = arr1.slice();
    
    arr2[2].push(5);
    
    console.log(arr2);	// [1,2,[3,4,5]]
    console.log(arr1);	// [1,2,[3,4,5]]

    slice() 도 결국 얕은 복사였고, Object.assign() 과 동일한 문제점을 보인다. 2차 Depth부터는 완전히 복사되지 않고 원본을 참조한다.

     

    4. Spread Operator(전개 연산자)

     

    전개 연산자는 해당 인자가 이터러블(순회 가능한 객체인지?)을 확인한 뒤, 맞다면 반복문으로 모든 요소를 하나씩 옮겨담는다.

    const arr1 = [1,2,[3,4]];
    const arr2 = [...arr1];
    
    arr2[2].push(5);
    
    console.log(arr2);	// [1,2,[3,4,5]]
    console.log(arr1);	// [1,2,[3,4,5]]

    하지만, 이 역시 slice() 와 동일하다. 이터러블 순회복사도 1-depth 이기 때문에, 중첩구조는 복사하지 못했다.

     

     

    결론적으로, 우리가 대부분 사용하던 객체 복사는 얕은 복사이며, 이는 이터러블을 순회하며 1-depth로 요소들을 옮겨담는 로직이었던 것이다.

    * 이터러블 참고 포스팅 : abangpa1ace.tistory.com/34?category=910462

     

    - 깊은 복사

    그렇다면, 깊은 복사는 객체 안의 객체 참조값도 완전히 복사되어야할 것이다. 즉, 객체가 참조하는 원본이 완벽하게 복사되어야할 것이다.

     

    1. JSON 메서드 사용 

    const obj1 = {
      age: 20,
      name: {
        first: "taeng",
        last: "kim",
      },
    }
    
    const obj2 = JSON.parse(JSON.stringify(obj1));
    
    obj1.age = 200;
    obj1.name.last = "park";
    
    console.log(obj2.age);		// 20 (good~)
    console.log(obj2.name.last);	// "kim" (very good!)

    첫 번째는 JSON의 stringify(), parse() 메서드를 활용하는 것이다.

    stringify() 로 객체를 JSON 형태의 문자열(원시 데이터)로 만들어 복사한 뒤, parse() 로 다시 객체화하는 방법이다.

     

    보시다시피 내부 중첩구조까지 깔끔하게 복사된 것을 알 수 있다. 이는, 이터러블 순회복사와는 다른 JSON 복사 로직 때문이다.

    순회가 아니라 객체를 문자열로 변경한 뒤, 이것을 다시 해석해 원본 객체로 변경하면서 기존이 아닌 새로운 참조값이 생기는 것이다.

     

    JSON 메서드 복사는 깊은 복사의 장점은 있지만, 성능적으로 느리고 객체의 함수나 undefined 값은 복사가 되지 않는 문제가 있다.

    const testData = {a: (a, b) => a + b, c: undefined};
    const copyData = JSON.parse(JSON.stringify(testData));
    console.log(copyData);  // {}

     

    2. Lodash clonedeep 함수

     

    Node.js 에서 제공되는 미들웨어 중, Lodash 의 clonedeep 메서드를 사용하면 깊은 복사가 가능하다.

    const clonedeep = require("lodash.clonedeep")
    
    const original = {
      a: 1,
      b: {
        c: 2,
      },
      d: () => {
        console.log("hi")
      },
    }
    
    const deepCopied = clonedeep(original)
    
    original.a = 1000
    original.b.c = 2000
    
    console.log(deepCopied.a) // 1
    console.log(deepCopied.b.c) // 2
    console.log(deepCopied.d()) // 'hi'

     

    이를, 직접 함수로 구현해봤다. 

    function cloneDeepMade(obj) {
      // 1) 미입력, 객체외 경우 예외처리
      if (obj === null || typeof obj !== "object") {
        return obj
      }
      
      // 2) 결과값 자료형 정의 (객체 or 배열)
      const result = Array.isArray(obj) ? [] : {}
    
      // 3) 재귀함수 형태로 객체순회
      for (let key of Object.keys(obj)) {
        result[key] = cloneDeepMade(obj[key])
      }
    
      return result
    }
    

     

    입력인자(obj)가 빈 값이거나 객체가 아닌 경우 예외처리를 진행한다. 그리고, result 반환값의 자료형을 [](배열), {}(객체) 중 고른다.

    마지막으로, obj의 key값을 순회하며 이를 result에 복사한다. 결과값은, cloneDeepMade() 함수를 재귀로 활용한다.


    지금까지, slice() 나 전개 연산자의 경우 원본에 영향을 미치지 않아 완벽한 복사라고 막연하게 생각하고 있었다.

    이번에 알고리즘을 풀면서 배열을 복사하다가 우연히 깊은 복사란 개념을 접했고, 곧 위 방법들 역시 2-depth 이상에선 위험성이 있음을 깨달았다.

    진정한 깊은 복사인 JSON 객체변환도 알게 되었으며, 특히 명확한 단점이 존재하기에 2-depth 이상의 경우에서만 사용해야함을 느꼈다!

     

    [출처]

    - FALSY LAB. 블로그 : falsy.me/javascript-6-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B4%85%EB%8B%88%EB%8B%A4/

    - 박준우 님의 블로그 : junwoo45.github.io/2019-09-23-deep_clone/

    - Moon 님의 블로그 : medium.com/watcha/%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%8B%AC%EB%8F%84%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%95%BC%EA%B8%B0-2f7d797e008a

    반응형
Designed by Tistory.