본문 바로가기

FrontEnd

몽고db 문법으로 불변성을 관리하는 immutability-helper

반응형

깃헙 문서 내용 번역이다 : https://github.com/kolodny/immutability-helper

 

GitHub - kolodny/immutability-helper: mutate a copy of data without changing the original source

mutate a copy of data without changing the original source - GitHub - kolodny/immutability-helper: mutate a copy of data without changing the original source

github.com

원본 데이터를 변경하지 않고 복사본을 변경하여 리턴하는 라이브러리다.
다른 라이브러리들도 많은데 이걸 왜쓰느냐 하면 
몽고디비 문법(like 프리즈마 문법)처럼 오퍼레이션을 객체로 표현하는게 특이해서다.
 

면책 조항

제가 써서 정리한 내용이지 추천한다는 건 아닙니다.

굳이 추천하느냐 라고 묻는다면 저는 추천하는 편입니다.

prisma나 mql 등에서 해당 객체 기반 dsl을 사용하는 것 같습니다.


npm install immutability-helper --save

참고로 라액트 공식 문서에서 링크한 라이브러리다.

// import update from 'react-addons-update';
import update from 'immutability-helper';

const state1 = ['x'];
const state2 = update(state1, {$push: ['y']}); // ['x', 'y']

개요

React를 사용하면 뮤테이션을 포함하여 원하는 모든 스타일의 데이터 관리 방법을 사용할 수 있습니다.
그러나 애플리케이션의 성능이 중요한 부분에서 불변 데이터가 필요하다면,
shouldcomponentupdate() 메서드를 구현하여 앱의 속도를 크게 높일 수 있습니다.
 
JavaScript로 불변 데이터를 처리하는 것은 Clojure와 같이 불변 객체를 다루기 위해 설계된 언어보다 더 어렵습니다.
데이터 표현 방식을 근본적으로 변경하지 않고도 불변 데이터를 훨씬 쉽게 처리할 수 있는 간단한 불변성 도우미 update()를 제공합니다. Facebook의 Immutable.js와 React의 Using Immutable Data Structures섹션을 참고하는 것도 괜찮습니다.
 

핵심 아이디어

다음과 같이 데이터를 변경하는 경우:

myData.x.y.z = 7;
// or...
myData.a.b.push(9);
이전 복사본을 덮어쓴 이후 어떤 데이터가 변경되었는지 확인할 방법이 없습니다.
대신 myData의 새 복사본을 만들고 변경할 부분만 변경해야 합니다.
그런 다음 삼중 등호(===)를 사용하여 myData의 이전 사본을 shouldComponentUpdate()의 새 사본과 비교할 수 있습니다.
 
const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);

 

불행히도 깊은 복사는 비용이 많이 들고 때로는 불가능합니다.
변경해야 하는 개체만 복사하고 변경되지 않은 개체를 재사용하여 이 문제를 완화할 수 있습니다.
불행히도 오늘날의 JavaScript에서는 이것이 번거로울 수 있습니다.

const newData = Object.assign({}, myData, {
  x: Object.assign({}, myData.x, {
    y: Object.assign({}, myData.x.y, {z: 7}),
  }),
  a: Object.assign({}, myData.a, {b: myData.a.b.concat(9)})
});
상당히 성능이 좋지만(log n 개체의 얕은 복사본만 만들고 나머지는 재사용하기 때문에) 쓰기가 매우 어렵습니다.
구현의 반복은 성가실 뿐만 아니라 버그에 대한 넓은 표면적을 제공합니다.

update()

update()는 이 코드를 더 쉽게 작성할 수 있도록(공유 데이터 재사용 및 새로운 데이터만 복사)
이 패턴 주위에 간단한 구문 설탕을 제공합니다.
이 코드는 사용하면 다음과 같이 보입니다.
import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});
구문이 익숙해지는 데 약간의 시간이 걸릴 수 있습니다.(MongoDB's query language에서 영감을 얻었지만)
$ 접두어가 붙은 키를 커맨드라고 합니다. 그들이 "변경하는" 데이터 구조를 타겟이라고 합니다.

사용 가능한 커맨드

 

  • {$push: array} : 타겟의 배열에 있는 모든 항목을 push()합니다.
  • {$unshift: array} : 타겟의 배열에 있는 모든 항목을 unshift()합니다.
  • {$splice: array of arrays}는 배열의 각 항목에서 제공하는 매개변수를 사용하여 타겟에 splice()를 호출합니다.
    • 참고: 배열의 항목은 순차적으로 적용되므로 순서가 중요합니다. 대상의 인덱스는 작업 중에 변경될 수 있습니다.
  • {$set: any} : 타겟을 완전히 바꿉니다.
  • {$toggle: array of string} :  타겟 개체에서 리스트에 포함된 불리언 필드를 토글합니다.
  • {$unset: array of string} : 타겟 개체에서 배열의 키 목록을 제거합니다.
  • {$merge: object} : object의 키를 타겟과 병합합니다.
  • {$apply: function} :  현재 값을 함수에 전달하고 새로운 반환 값으로 업데이트합니다.
  • {$add: array of objects}는 Map 또는 Set에 값을 추가합니다.
    • Set에 추가할 때 추가할 객체의 배열을 전달하고
        • update(mySet, {$add: [{'foo' :bar'}, {'baz' :'boo'}]}
    • Map에 추가할 때 다음과 같이 [key, value] 배열을 전달합니다.
      • update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]}
  • {$remove: array of string} : 맵 또는 셋에서 배열의 키 목록을 제거합니다.

약식 $apply 구문

커맨드 객체 대신 함수를 전달할 수 있으며
$apply 커맨드을 사용한 명령 객체인 것처럼 처리됩니다.
update({a: 1}, {a: function}). 이 예는 update({a: 1}, {a: {$apply: function}})와 동일합니다.

 


한계

⚠️ update 함수는 Object.defineProperty로 정의된 접근자 속성이 아닌 데이터 속성에 대해서만 동작합니다.
Object.defineProperty로 정의된 접근자 속성를 볼 수 없기 때문에
setter 부작용에 따라 애플리케이션 로직을 깨뜨릴 수 있는 섀도잉 데이터 속성을 생성할 수 있습니다.
따라서 update 함수는 데이터 속성만 하위 항목으로 포함하는 일반 데이터 객체에만 사용해야 합니다.
 
 

예제

간단한 푸시

초기 데이터는 그대로임

const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]

중첩된 컬렉션

컬렉션의 인덱스 2,에 존재하는 객체의 키 a에 액세스하고 13과 14를 삽입하는 동안,

인덱스 1에서부터 (17을 제거하기 위해) 한 항목의 스플라이스를 수행합니다.

const collection = [1, 2, {a: [12, 17, 15]}];
const newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]

현재 값을 기반으로 값 업데이트

const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
// 같지만 좀 더 복잡하고 중첩된 객체 문법을 사용함.
const newObj2 = update(obj, {b: {$set: obj.b * 2}});

얕은 값 병합

const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}

 

Computed Property Names

배열은 ES2015 Computed Property Names 기능을 통해 런타임 변수로 인덱싱할 수 있습니다.
개체 속성 이름 식은 최종 속성 이름을 만들기 위해 런타임에 평가될 대괄호 []로 묶일 수 있습니다.
const collection = {children: ['zero', 'one', 'two']};
const index = 1;
const newCollection = update(collection, {children: {[index]: {$set: 1}}});
// => {children: ['zero', 1, 'two']}

배열에서 요소 제거

값과 상관없이 해당 인덱스 위치의 데이터 하나를 삭제합니다.

// Delete at a specific index, no matter what value is in it
update(state, { items: { $splice: [[index, 1]] } });

Autovivification

Autovivification은 필요할 때 새로운 배열과 객체를 자동으로 생성하는 것입니다.
자바 스크립트의 맥락에서 아래와 같은 것을 의미합니다.
const state = {}
state.a.b.c = 1; // state would equal { a: { b: { c: 1 } } }
javascript에는 이 "기능"이 없기 때문에 immutability-helper에도 동일하게 적용됩니다.
이것이 자바스크립트에서 그리고 immutability-helper에서 불가능한 이유는 다음과 같습니다.
var state = {}
state.thing[0] = 'foo' // 어떤 타입이어야 하나요? 객체? 배열?
state.thing2[1] = 'foo2' // 객체일까요?
state.thing3 = ['thing3'] // autovivification 없이도 동작하는 일반 js 문법
state.thing3[1] = 'foo3' // 배열일까요?
state.thing2.slice // undefined 일까요?(객체)
state.thing3.slice // function 일까요? (배열)
깊이 중첩된 객체를 설정해야 하는 경우, 아래와 같이 인위적인 구현을 사용합니다.
var state = {}
var desiredState = {
  foo: [
    {
      bar: ['x', 'y', 'z']
    },
  ],
};

const state2 = update(state, {
  foo: foo =>
    update(foo || [], {
      0: fooZero =>
        update(fooZero || {}, {
          bar: bar => update(bar || [], { $push: ["x", "y", "z"] })
        })
    })
});

console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true
// note that state could have been declared as any of the following and it would still output true:
// var state = { foo: [] }
// var state = { foo: [ {} ] }
// var state = { foo: [ {bar: []} ] }
확장 기능을 사용하여 $auto 및 $autoArray 명령을 추가하도록 선택할 수도 있습니다.
import update, { extend } from 'immutability-helper';

extend('$auto', function(value, object) {
  return object ?
    update(object, value):
    update({}, value);
});
extend('$autoArray', function(value, object) {
  return object ?
    update(object, value):
    update([], value);
});

var state = {}
var desiredState = {
  foo: [
    {
      bar: ['x', 'y', 'z']
    },
  ],
};
var state2 = update(state, {
  foo: {$autoArray: {
    0: {$auto: {
      bar: {$autoArray: {$push: ['x', 'y', 'z']}}
    }}
  }}
});
console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true

나만의 명령 추가하기

이 모듈과 react-addons-update의 주요 차이점은 이를 확장하여 더 많은 기능을 제공할 수 있다는 것입니다.
 
import update, { extend } from 'immutability-helper';

extend('$addtax', function(tax, original) {
  return original + (tax * original);
});
const state = { price: 123 };
const withTax = update(state, {
  price: {$addtax: 0.8},
});
assert(JSON.stringify(withTax) === JSON.stringify({ price: 221.4 }));
위 함수의 original 객체는 원본 객체이므로 먼저 개체를 얕은 복사해야 합니다.
또 다른 옵션은 update를 적용하여 업데이트한 결과를 리턴하는 것입니다. ((original, { foo: {$set: 'bar'} }))
 
전역 업데이트 함수를 엉망으로 만들고 싶지 않다면
복사본을 만들어 해당 복사본으로 작업할 수 있습니다.
import { Context } from 'immutability-helper';

const myContext = new Context();

myContext.extend('$foo', function(value, original) {
  return 'foo!';
});

myContext.update(/* args */);​
반응형