반응형
깃헙 문서 내용 번역이다 : https://github.com/kolodny/immutability-helper
원본 데이터를 변경하지 않고 복사본을 변경하여 리턴하는 라이브러리다.
다른 라이브러리들도 많은데 이걸 왜쓰느냐 하면
몽고디비 문법(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']]}
- Set에 추가할 때 추가할 객체의 배열을 전달하고
- {$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 */);
반응형
'FrontEnd' 카테고리의 다른 글
새로운 리액트 공식문서로 배우는 Context API 1편 : 프롭 드릴링 해결하기 (0) | 2022.07.05 |
---|---|
새로운 리액트 공식문서로 배우는 Reducer (0) | 2022.07.05 |
[짤막글] styled-components는 어떻게 스타일을 적용하는가 (0) | 2022.07.03 |
디자인 시스템 컴포넌트를 만들 때 고려할 사항들 (0) | 2022.07.02 |
CSS Variables를 이용하여 컬러 팔레트 구성하기 (0) | 2022.07.02 |