ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • How to make the Object as immutable in JS
    FP 2020. 7. 9. 01:00
    반응형

    출처: https://medium.com/@neetishop/immutable-objects-with-property-descriptors-in-javascript-31693faaf03


    불변

    FP에서 객체의 상태를 불변으로 유지하는건 굉장히 중요하다.
    불변을 통해 부수효과를 방지하기 때문이다.

    Why JS?

    간단하다. 굉장히 범용적으로 사용되는 언어이고
    무엇보다 내가 좋아하기 때문이다.

    -끝-













    농담이다. 🙃


    JS에서 객체의 상태 관리

    ES5까지는 변수를 상수처럼 고정 할 수 있는 방법이 없었지만
    ES6에서 const 라는 멋들어진 키워드가 소개되면서 우리는 변수를 상수처럼
    런타임시에 변경할 수 없도록 지정할 수 있게 되었다.

    하지만 그것도 잠시...

    객체의 속성은 여전히 런타임에도 맘데로 지멋데로 제멋데로 바꿀 수 있다.

    속성의 변신은 무죄?

    function Dog(name) {
      this.name = name;
    }
    
    Dog.prototype.toString = function() {
      return `Dog(name: '${this.name}')`;
    }
    
    const dog = new Dog('hello');
    dog.name = 'world';
    dog.toString();    // Dog(name: 'world') ...!!!!!!

    이런 거지같은 상황을 타개할 방도가 정녕 없단말인가?


    신에게는 아직 클로저가 있사오니...

    그렇다. 우리는 클로저(Closure)라는 굉장한 무기가 있었다.
    바로 클로저를 통해 객체지향의 private 한 느낌으로 변수를 관리해서 속성의 변신을 유죄 로 만들어 버릴 수 있다.

    function Dog(name) {
      const _name = name;
    
      return {
        name: _name,
        toString: function() {
          return `Dog(name: '${_name}')`;
        }
      };
    }
    
    const dog = new Dog('hello');
    dog.name = 'world';
    dog.toString();    // Dog(name: 'hello')

    너가 아무리 우리 객체한테 찍접거려도
    우리 객체는 쳐다도 안본단다. 😝

    즉, 클로저를 통해 객체 내부의 변수로의 접근을 차단해버릴 수 있다.

    그리고 한가지 또!
    만약 객체 내부의 값을 바꿔야 한다면, 새로운 객체를 만들어 반환하여 불변으로 유지할 수 있다.

    function Developer(coffee) {
      const code = coffee ? 'more code!' : 'no more code...';
    
      return {
        drink: function(coffee) {
          return new Developer(coffee);
        },
        toString: function() {
          return `I can ${code}`;
        }
      };
    }
    
    const huna = new Developer('coffee');
    const tiredHuna = huna.drink(null);
    
    huna.toString();    // I can more code!
    tiredHuna.toString();    // I can no more code...

    굉장하지 않는가?

    출처: https://www.reddit.com/r/ProgrammerHumor/comments/8n0b10/i_prefer_monster_tbh/


    또 다른 방법

    우리에겐 사실 클로저 말고 다른 무기가 하나 더 있었다.
    다만, 당신이 API 문서를 대충 봐서 놓친 그 무기 말이다.

    바로 Object.freeze() 이다.

    function Dog(name) {
      this.name = name;
    }
    
    Dog.prototype.toString = function() {
      return `Dog(name: '${this.name}')`;
    }
    
    const dog = Object.freeze(new Dog('hello'));
    dog.name = 'world';
    dog.toString();    // Dog(name: 'hello') 

    오... 클로저랑 동일하게 동작한다.

    근데 좀 그런 문제가 하나 있는데, 요건 사실 얕은 동결(shallow freeze) 이라고 불린다.
    그게 뭐냐면 역시 코드로 한번 보자.

    function Master(name) {
      this.name = name || 'No master';
    }
    
    Master.prototype.toString = function() {
      return `Master(name: '${this.name}')`;
    }
    
    function Dog(name, master) {
      this.name = name;
      this.master = master instanceof Master ? master : new Master();
    }
    
    Dog.prototype.toString = function() {
      return `Dog(name: '${this.name}', master: ${this.master.toString()})`;
    }
    
    const dog = Object.freeze(new Dog('js'));
    dog.toString();    // Dog(name: 'js', master: Master(name: 'No master'))
    
    dog.master.name = 'huna';
    dog.toString();    // Dog(name: 'js', master: Master(name: 'huna'))... WTF!!!

    아직 실망하긴 이르다.
    왜냐하면 우린 JS 용사들이기 때문이다.
    재귀를 통해 객체의 속성들을 전부 얼려버릴 수 있지 않나요?
    코드로 한번 구현해보자.

    const isObject = val => val && typeof val === 'object';
    
    function deepFreeze(obj) {
      if (isObject(obj) && !Object.isFrozen(obj)) {
        Object.keys(obj).forEach(name => deepFreeze(obj[name]));
        Object.freeze(obj);
      }
      return obj;
    }
    
    // 위의 Dog 객체를 사용하자
    const dog = deepFreeze(new Dog('FrozenJS'));
    dog.master.name = 'huna';
    dog.toString();    // Dog(name: 'FrozenJS', master: Master(name: 'No master'))

    굉장하다. 재귀 만세.

    함수를 쓰다가 화살표 함수를 쓰다가 왔다갔다 하는건 우린 ES6의 축복을 받았기 때문에 맘데로 해본 것 뿐이다.


    여기가 끝인줄 알았다면 오산이다.
    아직 FP스럽게 객체를 동결하는 무시무시한 무기가 또 남아있다.


    The 렌즈

    렌즈는 뭘까? 카메라에 달려있는 그거?
    뭔가를 들여다 보는 걸까?

    그렇다. 역시 당신은 JS 용사다.
    렌즈를 통해 원하는 속성에만 집중 할 것이다.

    아래 예제에서는 유명한 함수형 라이브러리인 ramda 를 사용해서 렌즈 기법을 살펴볼 것이다.

    보통 ramda는 R 이라는 전역 객체를 통해 모든 함수들에 접근이 가능하다.

    사용법은 먼저

    1. 우리가 접근하려는 속성을 렌즈로 만들고,
    2. 렌즈를 통해 들여다보거나
    3. 렌즈를 통해 수정할 수 있다.

    이를 통해 우리는 불변 객체를 사용할 수 있다.

    const dog = new Dog('js');
    dog.master = new Master('huna');
    
    const nameLens = R.lensProp('name');
    const newDog = R.set(nameLens, 'FP', dog);
    
    dog.toString();    // Dog { name: 'js', master: Master { name: 'huna' } }
    newDog.toString();    // Dog { name: 'FP', master: Master { name: 'huna' } }

    또한 굳이 귀찮게 deepFreeze 같은 함수 안만들고도
    객체의 속성의 객체의 속성들 같은 중첩된 객체들까지 모조리 불변으로 만들어 버릴 수 있다.

    const masterLens = R.lensPath(['master', 'name']);
    const cryingDog = R.set(masterLens, 'others', dog);
    
    dog.toString();    // Dog(name: 'js', master: Master(name: 'huna'))
    cryingDog.toString();    // Dog(name: 'js', master: Master(name: 'others'))

    굉장하다... deepFreeze 꺼졍 두번 꺼졍

    또한 렌즈는 setter 로써의 역할뿐만 아니라 getter 의 역할도 아주 기똥차게 해낸다.

    R.view(nameLens, dog);    // 'js'
    R.view(masterLens, dog);    // 'huna'


    어떤가. 프로젝트에서 렌즈를 써보고 싶어서 손가락이 근질근질 하지 않는가?
    그럼 이만!


    출처: 함수형 자바스크립트

    반응형

    'FP' 카테고리의 다른 글

    Function  (0) 2020.07.12
    FP vs OOP  (0) 2020.07.08
    Initial commit - FP  (0) 2020.07.07

    댓글

Designed by Tistory.