ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] 객체와 불변성(Immutability)
    javascript 2022. 10. 26. 09:55

    poiemaweb에서 javascript 기본 문법에 대해 복습하던 중 헷갈리거나 제대로 알지 못했던 부분들이 있어 기록으로 남기고자 한다.

     

     

    웹 프로그래밍 튜토리얼 | PoiemaWeb

    Front-end Development Tutorial

    poiemaweb.com

     


    Immutability(불변성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다.

    Immutability는 우리가 SPA를 사용할 때에도 강조했다시피 중요한 원리이다.

     

    객체는 참조(reference) 형태로 전달하고 전달 받는다. 객체가 참조를 통해 공유되어 있다면 그 상태가 언제든지 변경될 수 있기 때문에 문제가 될 가능성도 커지게 된다.

    이는 객체의 참조를 가지고 있는 어떤 장소에서 객체를 변경하면 참조를 공유하는 모든 장소에서 그 영향을 받기 때문인데 이것이 의도한 동작이 아니라면 참조를 가지고 있는 다른 장소에 변경 사실을 통지하고 대처하는 추가 대응이 필요하다.

     

    불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화할 수 있고 성능 개선에도 도움이 된다.

     

    1. 변경 가능한 값

    user.name의 값을 변경했지만 변수 myName의 값은 변경되지 않았다.

    이는 변수 myName에 user.name을 할당했을 때 user.name의 참조를 할당하는 것이 아니라 immutable한 값(string) ‘Lee’가 메모리에 새로 생성되고 myName은 이것을 참조하기 때문이다.

     

    따라서 user.name의 값이 변경된다 하더라도 변수 myName이 참조하고 있는 ‘Lee’는 변함이 없다.

    var user = {
      name: 'Lee',
      address: {
        city: 'Seoul'
      }
    };
    
    var myName = user.name; // 변수 myName은 string 타입이다.
    
    user.name = 'Kim';
    console.log(myName); // Lee
    
    myName = user.name;  // 재할당
    console.log(myName); // Kim

     

    아래의 경우 객체 user2의 name 프로퍼티에 새로운 값을 할당하면 객체는 변경 불가능한 값이 아니므로 객체 user2는 변경된다.

    그런데 변경하지도 않은 객체 user1도 동시에 변경된다.

    var user1 = {
      name: 'Lee',
      address: {
        city: 'Seoul'
      }
    };
    
    var user2 = user1; // 변수 user2는 객체 타입이다.
    
    user2.name = 'Kim';
    
    console.log(user1.name); // Kim
    console.log(user2.name); // Kim

    이는 user1과 user2가 같은 어드레스를 참조하고 있기 때문이다.

    이렇듯 객체에서는 참조의 의미가 매우 중요하다.

     


     

    2. 불변 데이터 패턴(immutable data pattern)

    위 경우들과 같이 의도하지 않은 객체의 변경이 발생하는 원인의 대다수는 레퍼런스를 참조한 다른 객체에서 객체를 변경하기 때문이다.

    이 문제의 해결 방법은 객체의 변경이 필요한 경우에는 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경한다.

     

    (1) Object.assign

    Object.assign은 타킷 객체로 소스 객체의 프로퍼티를 복사한다.

     

    이때 소스 객체의 프로퍼티와 동일한 프로퍼티를 가진 타켓 객체의 프로퍼티들은 소스 객체의 프로퍼티로 덮어쓰기된다. ES6에서 추가된 메소드이며 Internet Explorer는 지원하지 않는다.

    // Syntax
    Object.assign(target, ...sources)

    기존 객체를 변경하지 않고 객체를 복사하고 싶으target을 빈 객체로 설정하면 된다.

    // Copy
    const obj = { a: 1 };
    const copy = Object.assign({}, obj);
    console.log(copy); // { a: 1 }
    console.log(obj == copy); // false
    
    // Merge
    const o1 = { a: 1 };
    const o2 = { b: 2 };
    const o3 = { c: 3 };
    
    const merge1 = Object.assign(o1, o2, o3);
    
    console.log(merge1); // { a: 1, b: 2, c: 3 }
    console.log(o1);     // { a: 1, b: 2, c: 3 }, 타겟 객체가 변경된다!
    
    // Merge
    const o4 = { a: 1 };
    const o5 = { b: 2 };
    const o6 = { c: 3 };
    
    const merge2 = Object.assign({}, o4, o5, o6);
    
    console.log(merge2); // { a: 1, b: 2, c: 3 }
    console.log(o4);     // { a: 1 }

     

    그러나 Object.assign은 완전한 deep copy를 지원하지 않고 객체 내부의 객체(Nested Object)는 Shallow copy된다.

    const user1 = {
      name: 'Lee',
      address: {
        city: 'Seoul'
      }
    };
    
    // 새로운 빈 객체에 user1을 copy한다.
    const user2 = Object.assign({}, user1);
    // user1과 user2는 참조값이 다르다.
    console.log(user1 === user2); // false
    
    user2.name = 'Kim';
    console.log(user1.name); // Lee
    console.log(user2.name); // Kim
    
    // 객체 내부의 객체(Nested Object)는 Shallow copy된다.
    console.log(user1.address === user2.address); // true
    
    user1.address.city = 'Busan';
    console.log(user1.address.city); // Busan
    console.log(user2.address.city); // Busan

     

    user1 객체는 const로 선언되어 재할당은 할 수 없지만 객체의 프로퍼티는 보호되지 않는다. 다시 말하자면 객체의 내용은 변경할 수 있다.

     

    (2) Object.freeze

    Object.freeze()를 사용하여 불변(immutable) 객체로 만들수 있지만 객체 내부의 객체(Nested Object)는 변경가능하다.

     

    내부 객체까지 변경 불가능하게 만들려면 Deep freeze를 하여야 한다.

    function deepFreeze(obj) {
      const props = Object.getOwnPropertyNames(obj);
    
      props.forEach((name) => {
        const prop = obj[name];
        if(typeof prop === 'object' && prop !== null) {
          deepFreeze(prop);
        }
      });
      return Object.freeze(obj);
    }
    
    const user = {
      name: 'Lee',
      address: {
        city: 'Seoul'
      }
    };
    
    deepFreeze(user);
    
    user.name = 'Kim';           // 무시된다
    user.address.city = 'Busan'; // 무시된다
    
    console.log(user); // { name: 'Lee', address: { city: 'Seoul' } }

     

    (3) Immutable.js

    Object.assign과 Object.freeze을 사용하여 불변 객체를 만드는 방법은 번거러울 뿐더러 성능상 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋다.

     

    또 다른 대안으로 Facebook이 제공하는 Immutable.js를 사용하는 방법이 있다.

    Immutable.js는 List, Stack, Map, OrderedMap, Set, OrderedSet, Record와 같은 영구 불변 (Permit Immutable) 데이터 구조를 제공한다.

     

    npm을 사용하여 Immutable.js를 설치한다.

    $ npm install immutable

     

    Immutable.js의 Map 모듈을 import하여 사용한다.

    const { Map } = require('immutable')
    const map1 = Map({ a: 1, b: 2, c: 3 })
    const map2 = map1.set('b', 50)
    map1.get('b') // 2
    map2.get('b') // 50

    map1.set(‘b’, 50)의 실행에도 불구하고 map1은 불변하였다. map1.set()은 결과를 반영한 새로운 객체를 반환한다.

    'javascript' 카테고리의 다른 글

    [Javascript] insertAdjacentHTML  (0) 2023.01.30
    [Javascript] JSDoc  (0) 2022.10.26
    [Javascript] 객체  (0) 2022.10.25
    [Javascript] 단축평가  (0) 2022.10.25
    [Javascript] 데이터 타입과 변수  (0) 2022.10.24
Designed by Tistory.