자바스크립트

[번역] 불변(Immutable) 데이터 구조와 자바스크립트

원문 : http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript

얼마전에 나는 현재의 내 블로그를 재작성할 것이라 말하면서, 내가 배운 것들에 대해 좀더 깊이 들어가 보기로 약속했다. 오늘 나는 자바스크립트에서의 불변 데이터 구조, 특히 immutable.js 와 seamless-immutable 라이브러리에 대해 이야기할 것이다. 다른 라이브러리들도 있지만, 개념적으로는 완전히 영구적인 데이터 구조이거나 네이티브 자바스크립트 객체를 복사한 것 중의 하나일 것이고, 당신이 어떤 라이브러리를 선택하든 간에*[1]* 이들 두 라이브러리를 비교해 보는것으로 각자의 장단점을 잘 알게 될 것이다. 또한 transit-js 에 대해서도 조금 언급할 예정인데, 이는 직렬화를 할 때에 매우 유용할 것이다.

[1] mori는 영구적 데이터 구조에 대한 또다른 구현이며(ClosureScript에서 나왔다), React’s immutablility helpers는 네이티브 자바스크립트 객체를 얇게 복사하는 또다른 라이브러리이다.

이 글의 아주 조금은 Redux에 한정된 내용일 것이다. 전반적으로는 불변 데이터 구조를 사용하는 것에 대해 이야기를 하지만, 그것을 Redux에 사용할 수 있는 방법도 조금 제공할 것이다. Redux에서는 단일 앱 상태 객체를 갖고 그것을 불변적으로(immutably) 갱신하는데, 이렇게 할 수 있는 다양한 방식이 있고 각각의 장단점이 있다. 이에 대해서는 아래에서 살펴보겠다.

Redux에 대해서 한가지 고려해야 할 것은 단일 앱 상태 요소(single app state atom)를 만들기 위해 reducer를 어떻게 결합할 것인가이다. Redux가 기본으로 제공하는 메소드인 combineReducers는 당신이 여러 값들을 하나의 자바스크립트 객체로 결합한다고 가정한다. 만약 당신이 그 값들을 하나의 Immutable.js 객체로 결합하려 한다면, 자신만의 combineReducers를 작성해야 할 것이다. 만약 당신이 앱 상태를 직렬화해야 하고, 그 결과가 온전히 Immutable.js 객체로 만들어지기 원한다면 이 과정이 필요할 것이다.

이 글의 대부분은 자바스크립트에서 불변 객체를 어떻게 사용하는가에 대해 다룰 것이다. 이는 가끔 조금 불편하게 느껴지는데, 왜냐하면 당신은 기본 문법을 극복하려 하고 있고, 그 때문에 타입을 교묘하게 다루고 있다는 기분이 들 수 있기 때문이다. 하지만 당신의 앱에 따라, 그리고 당신이 어떤 식으로 구성하는가에 따라 거기에서 빠져나올 수 있을 것이다.

최근에 자바스크립트 네이티브에 불변 데이터 구조를 추가하자는 제안이 나왔지만, 진행이 될지는 아직 확실하지가 않다. 이 제안은 분명히 현재 자바스크립트에서 불변 데이터 구조를 사용할 때 생기는 대부분의 문제들을 해결할 것이다.

Immutable.js

Immutable.js는 페이스북에서 만들었으며 불변 데이터 구조의 가장 유명한 구현중의 하나이다. 이는 굉장히 멋지다; 완전히 영구적인 데이터 구조를 직접 구현하기 위해 tries와 같은 고급 개념들을 사용해서 구조를 공유하도록 만들었다. 모든 업데이트는 새로운 값을 반환하지만, 내부적으로는 구조가 공유되어 메모리 사용량을 (또한 GC의 스레싱(thrashing)도) 현저하게 줄인다. 말인즉슨, 만약 당신이 1000개의 요소를 벡터에 추가한다 해도 실제로 1001개의 요소를 가지는 새로운 벡터를 반환하지 않는다는 뜻이다. 거의 확실하게, 내부적으로는 몇개의 작은 객체들만 메모리에 할당될 것이다.

획기적인 기초를 쌓은 Okasaki의 멋진 공헌에 힘입어 구조를 공유하는 데이터 구조를 적극적인 활용함으로써, 불변적 값들은 너무 느려서 실제 앱에서 사용할 수 없다는 미신은 대부분 사라졌다. 실제로 얼마나 많은 앱들이 이를 통해 더 빨라질 수 있는지를 알면 놀랄 것이다. (다른 누군가에 의해 변형되는 것을 피하기 위해) 빈번하게 데이터 구조를 복사하고 읽어들이는 앱들은 불변 데이터 객체에서 쉽게 이점을 얻을 수 있다. (단순히 하나의 큰 배열을 한 번 복사하는 것만으로 변형가능한 데이터의 성능 이점은 상쇄될 것이다.)

또다른 예제는 ClosureScript가 불변 데이터 구조를 UI에 사용함으로써 큰 성능 향상 효과를 얻을 수 있음을 발견한 내용에서 확인할 수 있다. 만약 당신이 UI를 변형하려 한다면 아마 일반적으로는 DOM을 필요 이상 건드리게 될 것이다(왜냐하면 값을 갱신해야 하는지 아닌지 알 수가 없기 때문이다). React는 DOM 변형을 최소화해주지만, 당신은 그 이를 위해 여전히 virtual DOM을 만들어내야 한다. 만약 컴포넌트들이 불변적이라면, 당신은 virtual DOM을 만들어낼 필요조차 없다. 단순히 === 등가 비교만으로도 갱신이 필요한지 아닌지 알 수 있을 것이다.

사실이라고 하기엔 너무 마냥 좋은 것 같은가? 아마도 당신은 이런 이점들에도 불구하고 왜 우리가 불변 데이터 구조를 항상 사용하지 않는지 궁금할 것이다. 글쎄, Elm이나 ClojureScript 같은 몇몇 언어들은 실제로 그렇게 사용하고 있다. 자바스크립트에서는 언어가 기본적으로 제공하고 있지 않기 때문에 좀더 어려우며, 그렇기 때문에 우리는 장점과 단점을 비교해볼 필요가 있다.

공간과 GC 효율성

왜 구조의 공유가 불변 데이터 구조를 더 효율적으로 만드는지는 이미 설명했다. 물론 특정 인덱스의 배열을 직접 변형하는 것보다 나은 방법은 없겠지만, 불변성의 오버헤드는 그리 크지 않다. 만약 당신이 변형을 피하고 싶다면 불변성은 객체를 직접 복사하는 것보다 더 나을 것이다.

Redux에서는 불변성이 강제된다. 당신이 새로운 값을 반환하지 않는다면 화면에서 아무것도 갱신되지 않을 것이다. 이것으로 인한 큰 이점들이 있는데, 만약 당신이 객체를 복사하는 것을 피하고 싶다면 Immutable.js를 살펴봐야 할 것이다.

참조(Reference) & 값(Value) 등가 비교

당신이 내부적으로 객체의 참조를 저장하고 있다고 가정하고 그 객체를 obj1이라 하자. 나중에 obj2가 들어온다. 만약 당신이 절대 객체를 변형하지 않으며, obj1 === obj2가 true 라면, 당신은 아무것도 변경된 것이 없다는 것을 확실하게 알 수 있다. React와 같은 많은 아키텍처에서 이는 강력한 최적화를 할 수 있게 해 준다.

이것을 “참조 등가 비교”라고 부르는데, 단순히 포인터를 비교하는 방법이다. 하지만 또다른 “값 등가 비교”라는 개념이 있으며, 이는 두 개의 객체가 실제로 동일한지를 obj1.equals(obj2)를 이용해서 검사하는 방법이다. 만약 모두가 불변적이라면 객체를 단지 값인 것 처럼 다룰 수 있다.

ClosureScript에서는 모든 것이 값이며, 심지어 (===와 같은) 기본 등가 비교 연산자조차 값 비교를 수행한다. 만약 당신이 실제로 인스턴스를 비교하고 싶다면 identical?을 사용할 수 있다. 불변 데이터 구조에 대해 값을 등가 비교하는 것의 이점은, 전체를 재귀적으로 검사하는 것보다 훨씬더 빠르게 검사할 수 있다는 점이다(만약 구조를 공유한다면 그 부분을 체크하지 않고 건너뛸 수 있다).

그러면 실제로 이것을 어떻게 사용할까? 이것으로 어떻게 React를 손쉽게 최적화 하는지에 대해서는 이미 설명했다. 단지 sholdComponentUpdate를 구현해서 상태가 동일한지 확인하고 동일하다면 렌더링을 건너뛰면 된다.

또한 나는 Immutable.js에서 === 를 사용하는 것이 값 등가 비교를 수행하지는 않지만 (당연하게도, 자바스크립트의 문법을 직접 오버라이드할 수는 없다) Immutable.js가 객체의 동일성을 위해 값 등가 비교를 이용한다는 것을 발견했다. Immutable.js는 객체가 동일한지를 검사하는 어느 곳에서든 값 등가 비교를 사용한다.

예를 들자면 Map 객체의 키들은 값 등가 비교를 사용한다. 말인즉슨, Map에 객체를 저장한 다음 나중에 똑같은 형태를 가진 객체를 제공하면 같은 값을 가져올 수 있다는 것이다.

let map = Immutable.Map();
map = map.set(Immutable.Map({ x: 1, y: 2}), "value");
map.get(Immutable.Map({ x: 1, y: 2 })); // -> "value"

이것은 정말 멋진 많은 의미를 갖는다. 예를 들어 서버로부터 데이터를 내려받기 위해 필요한 필드를 지정하는 쿼리 객체를 받는 함수가 있다고 해 보자.

function runQuery(query) {
  // pseudo-code: somehow pass the query to the server and
  // get some results
  return fetchFromServer(serialize(query));
}

runQuery(Immutable.Map({
  select: 'users',
  filter: { name: 'James' }
}));

만약 내가 쿼리를 캐싱하는 것을 구현하고 싶다면, 이렇게만 하면 될 것이다.

let queryCache = Immutable.Map();
function runQuery(query) {
  let cached = queryCache.get(query);
  if(cached) {
    return cached;
  } else {
    let results = fetchFromServer(serialize(query));
    queryCache = queryCache.set(query, results);
    return results;
  }
}

쿼리 객체를 값으로써 다룰 수 있고, 그 객체를 키로 해서 결과를 저장할 수 있다. 나중에 무언가가 동일한 쿼리를 실행한다면, 심지어 쿼리 객체가 동일한 인스턴스가 아니더라도 캐시된 결과를 돌려받게 될 것이다.

값 등가 비교가 단순화시킬 수 있는 수많은 패턴들이 있다. 사실 나는 포스트를 가져올 때 정확히 같은 기법을 사용하고 있다.

자바스크립트와의 상호 연동

Immutable.js 데이터 구조의 중요한 단점은 바로 위에서 설명한 모든 기능들을 구현할 수 있는 이유이다 : 일반 자바스크립트 데이터 구조가 아니라는 점이다. Immutable.js 객체는 자바스크립트 객체와 완전히 다르다.

즉 map.property대신에 map.get("property")를 해야 하고, array[0] 대신에 array.get(0)을 해야 한다는 의미이다. Immutable.js가 자바스크립트와 호환이 되는 수많은 API의 목록을 제공하고는 있지만, 심지어 그것들 조차 다르다(push는 기존의 인스턴스를 변형시키는 대신 반드시 새로운 배열을 반환해야 한다). 마치 엄격하게 변형을 위해(mutation-heavy) 만들어진 자바스크립트 문법과 싸우고 있는 것 처럼 느껴질 수도 있다.

이것이 문제가 되는 이유는, 만약 하드코어 하게 사용하지 않는다거나 프로젝트를 처음부터 시작하는 것이 아니라면 Immutable 객체를 모든 곳에서 쓸 수 없기 때문이다. 실제로 작은 함수들의 로컬 객체들에서는 불변성이 반드시 필요하지는 않을 것이다. 만약에 당신이 모든 객체/배열 등을 불변적으로 생성한다고 하더라도, 일반적인 자바스크립트 객체/배열 등을 사용하는 외부 라이브러리들과 함께 동작하도록 해야 할 것이다.

그 결과 당신은 자바스크립트 객체나 Immutable 객체 중 어떤 것을 이용하고 있는지 절대로 알 수 없게 된다. 이는 함수들을 이해하는 것을 어렵게 만든다. 당신이 어느곳에 불변 객체를 사용할 지를 명확하게 정할 수 있다고 하더라도, 여전히 당신은 시스템을 통해서 사용여부가 명확하지 않은 곳으로 객체들을 넘겨주게 될 것이다.

실제로, 가끔 Immutable Map 내부에 일반 자바스크립트 객체를 넣고 싶을 때가 있다. 그렇게 하지 말라. 불변한 상태와 가변 상태를 같은 객체에 섞는 것은 혼란을 줄 것이다.

나는 이것에 대해 두 가지 해결책을 알고 있다.

  1. TypeScript나 Flow와 같은 타입 시스템을 사용하라. 이것은 불변 데이터 구조가 시스템의 어느 곳에서 흐르고 있는지를 기억해야만 하는 정신적인 부하를 제거해준다. 다만, 이는 꽤나 다른 코딩 스타일을 요구하기 때문에 많은 프로젝트들에서 굳이 이 과정을 도입하려 하지는 않을 것이다.
  2. 데이터 구조에 대한 상세 내용을 숨겨라. 만약 당신이 Immutable.js를 시스템의 특정한 부분에서 사용하고 있다면, 외부에서 직접 데이터 구조에 접근할 수 있도록 만들지 말라. Redux에서의 단일 요소 앱 상태가 좋은 예이다. 만약 앱의 상태가 Immutable.js 객체라면, React 컴포넌트가 Immutable.js의 API를 직접적으로 사용하도록 강요하지 않도록 하라.여기에는 두 가지 방법이 있다. 첫번째는 typed-immutable과 같은 것을 사용해서 실제로 객체의 타입 을 지정하는 것이다. Record을 생성하면 Immutable.js 객체를 감싸주는 가벼운 랩퍼(Wrapper)를 얻을 수 있고, 이 객체는 타입이 지정된 필드에 대해 겟터(getter)를 정의하여 map.property 인터페이스를 사용할 수 있게 해 준다. 그 객체로부터 값을 읽어들이는 모든 것들은 그 객체를 일반 자바스크립트 객체처럼 다룰 수 있다. 그 객체를 변형(mutate)할 수는 없긴 하겠지만, 그건 실제로 당신이 강제하고자 하는 것이다. 두번째 방법은 객체를 쿼리할 수 있는 방법을 제공하고 값을 읽으려고 하는 모든 것들에게 그 쿼리를 수행하도록 강제하는 것이다. 이것을 일반적으로 적용할 수는 없겠지만, Redux의 경우에는 매우 잘 동작하는데, 이는 Redux가 단일 앱 상태 객체를 갖고 있으며 어떻게든 그 데이터 구조를 숨기길 원하기 때문이다. 모든 React 컴포넌트들이 그 데이터 구조에 의존하게 된다면 당신은 결코 실제 앱 상태의 구조를 변경할 수 없을 것이다. (하지만 실제로는 자주 변경할 수 밖에 없다)

    깊은 객체 쿼리를 위해 쿼리가 꼭 복잡한 엔진일 필요는 없고, 그냥 단순한 함수면 된다. 아직 내 블로그에 적용하지는 않았지만, 만약에 getPost(state, id)나 getEdittorSettings(state)와 같은 함수들이 한 묶음 있다고 가정해보라. 이들은 모두 state를 받아서 내가 “쿼리”하는 것을 단지 함수를 이용해 반환한다. 나는 그것이 상태 내부의 어디에 위치하는지 더이상 신경쓰지 않는다. 단 하나의 문제점은 여전히 immutable 객체를 반환할 수도 있다는 점인데, 그렇기 때문에 아마도 그 객체를 먼저 자바스크립트 객체로 강제 변환시키든가 위에서 말한 Record 타입을 이용해야 할 것이다.

모든 걸 종합하자면 : 자바스크립트 상호 연동은 실제 이슈이다. 절대 불변 객체로부터 자바스크립트 객체를 참조하지 말라. 상호 연동의 문제는 typed-immutable에 의해 제공되는 Record 타입에 의해 완화될 수 있는데, 이는 변형을 하거나 유효하지 않은 필드를 읽을 때 에러를 발생시키는 등의 또다른 흥미로운 이점을 갖고 있다. 마지막으로, 만약 Redux를 사용한다면 모든 것이 앱 상태 구조에 의존하도록 강제하지 말라. 왜냐하면 이후에 변경하게 될 것이기 때문이다. 데이터 구현을 추상화하면 불변성의 상호 연동과 관련된 문제들을 해결할 수 있을 것이다.

seamless-immutable

불변성을 강제할 수 있는 또다른 방법이 있다. seamless-immutable 프로젝트는 일반 자바스크립트 객체를 사용하는 훨씬 가벼운 해결책이다. 이것은 새로운 데이터 구조를 만들어내지 않으며, 그러므로 데이터 구조를 공유하지 않고, 객체를 수정할 때 그것을 복사해서 사용한다는 의미가 된다(하지만, 얇은 복사만 한다). 위에서 설명한 성능이나 값 등가 비교의 이점은 얻을 수 없다.

하지만 그 대신 훌륭한 자바스크립트 상호 연동이 훌륭하다. 모든 데이터 구조는 말 그대로 온전한 자바스크립트 데이터 구조이다. 차이점은 seamless-immutable은 Object.freeze를 호출하기 때문에 객체를 변형할 수 없다는 점이다 (ES6 에서 기본설정인 strict mode 에서는 변형 시에 에러를 발생시킬 것이다). 덧붙여서 이것은 각각의 인스턴스에 갱신을 지원하기 위해 (제공된 프라퍼티를 합쳐서 새로운 객체를 반환하는)merge와 같은 몇가지 메소드들을 추가한다.

불변 데이터 구조를 갱신할 때, 깊게 중첩된 객체들을 갱신하기 쉽게 만들어 주는 Immutable.js의 setIn 이나 mergeIn과 같은 몇가지 메소드들은 포함되어 있지 않다. 하지만 이들은 쉽게 구현될 수 있으며, 나는 이들에 대해 프로젝트에 공헌할 계획을 갖고 있다.

불변 객체와 가변 객체를 섞는 것은 불가능하다. seamless-immutable은 모든 인스턴스를 감쌀 때 객체들을 깊게 불변 객체로 변경하며, 어떤 값이 추가되든 자동적으로 감싸진다. 실제로는 Immutable.js도 매우 비슷한 방식으로 동작하는데, obj.merge와 같은 몇가지 메소드들 뿐만 아니라 Immutable.fromJS도 깊은 변환을 한다. 하지만 obj.set은 자동으로 강제 변환을 하지 않는데, 그렇기 때문에 원하는 어떤 데이터 타입이든 저장할 수 있게 된다. seamless-immutable에서는 이것이 불가능하며, 그렇기 때문에 실수로 가변 자바스크립트 객체를 저장하는 일은 발생할 수 없다.

개인적인 의견으로는, 각각의 라이브러리들이 지금 하고 있는 방식 대로 계속 진행하길 바란다. 이들은 다른 목표를 가지고 있다. 예를 들어 seamless-immutable은 자동적인 강제 변환을 하기 때문에, 알지 못하는 타입에 대해서는 저장할 수 없을 것이고, 그렇기 때문에 기본적으로 내장된 타입을 제외한 타입들과는 제대로 동작하지 않을 것이다. (실제로 지금 현재 Map이나 Set 타입도 지원하지 않고 있다)

seamless-immutable은 많은 장점을 가진 작은 라이브러리이지만, 그와 동시에 값 등가 비교와 같은 불변 데이터 구조의 중요한 장점들을 잃어버리기도 했다. 만약 자바스크립트 상호 연동이 중요한 고려대상이라면 이것은 환상적인 해결책일 것이다. 이것은 기존에 존재하는 코드를 마이그레이션 할 때 특히 유용한데, 당신은 객체들을 건드리는 모든 코드를 전부 재작성할 필요 없이 객체를 차근차근 불변한 상태로 변경할 수 있을 것이다.

빠뜨린 부분: transit-js를 이용한 직렬화

한가지 마지막으로 고려해야 할 부분이 있다. 바로 직렬화이다. 만약 커스텀 데이터 타입을 사용한다면 JSON.stringify는 더이상 선택사항이 아니다. 하지만 JSON.stringify가 썩 훌륭하지는 않는데, 심지어 ES6의 Map이나 Set 인스턴스조차 직렬화하지 못한다.

transit-js는 David-Nolen이 만든 훌륭한 라이브러리이며, 확장 가능한 데이터 전송 포맷을 정의한다. 기본적으로는 Map이나 Set 인스턴스들을 던져넣을 수 없지만, 중대한 차이점은 커스텀 타입들을 transit이 이해할 수 있는 무언가로 번역할 수 있다는 점이다. 실제로 전체 Immutable.js 타입들에 대한 직렬화와 역직렬화를 위한 전체 코드는 150 라인이 채 되지 않는다.

또한 Transit은 타입을 인코드 하는 방식을 보면 더욱 똑똑하다. 예를 들어 Transit은 맵의 키가 복잡한 타입일 것이라는 것을 알기 때문에, 어떻게 Map 타입을 직렬화 할 것인지 알려주기가 쉽다. Immutable.js를 지원하는 (위에서 언급했던) transit-immutable-js 라이브러리를 사용하면, 이런 식으로 할 수 있다.

let { toJSON, fromJSON } = require('transit-immutable-js');

let map = Immutable.Map();
map = map.set(Immutable.Map({ x: 1, y: 2 }), "value");

let newMap = fromJSON(toJSON(map));
newMap.get(Immutable.Map({ x: 1, y: 2 })); // -> "value"

값 등가 비교를 할 때 transit의 간편하고 쉬운 맵 직렬화를 사용하면, 어떤 시스템에서든 단순하고 일관된 패턴의 방식을 제공할 수 있다. 실제로 내 블로그에서 서버렌더링을 할 때 쿼리를 캐쉬한 다음에 클라이언트로 그 캐쉬를 전송하는데, 그 캐쉬는 여전히 손상되지 않고 유지된다. 이 케이스는 내가 transit으로 전환했던 실제 핵심 이유였다.

ES6 타입을 직렬화 하는 것이 물론 어렵지는 않겠지만, 만약 키값이 복잡한 구조로 되어 있다면 나는 값 등가 비교를 하지 않고 어떻게 역직렬화된 인스턴스를 사용할 수 있을 지 모르겠다.

또한 만약 일반 자바스크립트 객체와 Immutable.js 객체가 섞여서 존재한다면, transit을 이용한 직렬화는 모든 타입들을 손상되지 않게 유지시켜준다. 나는 이들을 섞는 것을 추천하지 않지만, transit은 각각의 객체를 적절한 타입으로 역직렬화 시킬 것이다. 반면 순수 JSON 객체를 사용한다면 역직렬화 할 때 모든 객체를 Immutable.js 타입으로 변환하게 될 것이다. (Immutable.fromJS(JSON.parse(str))를 한다고 생각해보라)

당신은 tranit을 확장해서 Date 인스턴스나 커스텀 타입등과 같은 어떤 것이든 직렬화할 수 있다. transit-format에서 타입을 어떻게 인코드 하는지 확인해보라.

만약 seamless-immutable을 사용한다면, 당신은 이미 스스로를 내장된 자바스크립트 (고로 JSON 변환 가능한) 타입만 사용하도록 제한하고 있는 것이기 때문에 JSON.stringify를 사용할 수 있다. 이는 단순하긴 하지만 확장성을 잃게 된다. 모든 것은 결국 트레이드오프다.

결론

불변성은 많은 이점을 제공해 주지만, Immutable.js가 제공하는 완전히 영구적인 데이터 구조를 사용할지 아닐지의 여부는 앱에 따라 다르다. 내 생각에 대부분의 앱들은 비교적 규모가 작기 때문에 객체를 복사하는 것에 별다른 문제가 없을 것이다.

하지만 단순함을 얻는 대신 기능들을 잃게 될 것이다; 제한된 API 뿐만 아니라 값 등가 비교도 할 수 없다. 덧붙여서 나중에 구조의 공유를 통한 성능의 향상을 위해 Immutable.js로 전환하려고 할 때 어려울 수도 있다.

일반적으로 나는 데이터의 상세 구조를 외부 세계로부터 숨기는 것을 추천한다. 특히 Immutable.js를 사용한다면 말이다. obj.property와 arr[0]와 같은 자바스크립트 객체나 배열의 기본 프로토콜을 따르려고 노력하라. 불변 객체들을 빠르게 이런 인터페이스들로 감싸는 것이 가능해야 하는데, 좀더 연구가 필요할 것 같다.

이는 특히 Redux의 경우 사실인데, 당신이 앱 상태의 구조를 나중에 변경하게 될 것이기 때문이다. 심지어 당신의 앱 상태가 일반 자바스크립트 객체인 경우에도 같은 문제가 있다. 앱 상태와 관련된 것들을 변경할 때 외부에서 그 상태를 사용하는 것들이 깨져서는 안된다. 대신에 앱의 상태를 쿼리하는 방법을 제공하라. 최소한 데이터 접근을 함수를 통해 할 수 있도록 추상화라도 시켜라.  Relay나 Falcor와 같은 더 복잡한 솔루션도 이를 해결할 수 있는데, 왜냐하면 쿼리 언어가 데이터에 접근하는 기본 방법이 되기 때문이다.

불변성과 관련된 내가 알고있는 현존하는 라이브러리에 대한 gist를 만들었다.