The Rise of Immer in React (번역)

(원문: https://www.netlify.com/blog/2018/09/12/the-rise-of-immer-in-react/)

불변성을 다루는 방법에 변화가 일고 있습니다. 최소한, React에서 불변성을 다루는 방법은 확실히 바뀌고 있습니다.

역사

JavaScript 세계에서는 사실 불변성에 대한 필요성이 명확하지 않습니다. 전통적으로, 불변성의 가장 큰 이점은 두려움 없이 동시성을 구현할 수 있다는 것입니다. 하지만 JavaScript는 single-threaded 언어이므로 그 이점이 크지 않습니다.

React에서 불변성의 역사를 알아보기 위해 2013년 12월로 거슬러 올라가봅시다. 이 때 David Nolen은 Om을 발표했습니다. Om은 ClosureScript 사용자들이 React를 사용할 수 있도록 감싼 wrapper였습니다. Om에는 이상한 점이 하나 있었는데, 바로 React보다 더 빠르다는 것이었습니다.

어떻게 wrapper가 원래 것보다 빠를 수 있을까요? David가 그에 대해 잘 설명한 talk이 있으니 한 번 보시기 바랍니다. 여하간 가장 큰 이유는 불변 데이터입니다. React라는 라이브러리가 하는 대부분의 작업은 reconcilication입니다. 그런데 객체와 배열에 대해 얕은 비교를 수행하거나 memoize 함수를 사용하면 많은 작업을 건너뛸 수 있습니다. React Fiber의 데이터 구조는 내부적으로 여기저기에 memoization을 활용해서 반복작업을 회피하고 있습니다.

Lee Byron은 여기에서 더 나아가 2015년에 Immutable.js를 발표했습니다. (그 밖에 ReactConf 발표, Q&A를 참고하세요.) 이는 JavaScript에서의 불변성을 위한 라이브러리입니다. Note, in particular, his point that mutable objects complect time and value, and the benefits of low-level structural sharing.*

Flux 철학과 잘 부합했던 Immutable.js는 Redux 커뮤니티에 빠르게 도입되었습니다. (그 외에 67종류의 대체재를 포함해서 말이죠.) 그리고 Netlify 역시 Immutable.js를 도입했습니다! 이제 불변성과 관련된 문제는 모두 해결되었습니다! 맞나요?

맞나요?…

Immer를 만든 사람, 문화, 그리고 커뮤니티

2018년 초, Michel Weststrate는 Immer를 오픈소스화 했습니다. 여기 그 내용을 다시 적는 대신에 그의 소개글프로젝트 도움말을 읽어보실 것을 강력히 권장합니다. 또한 React Finland에서의 발표(및 슬라이드)도 확인하시는 것을 추천합니다.

이제까지 많은 사람들이 Immer에 찬사를 보내고 있습니다.

회사로부터:

단점이 있기는 하지만, Immer는 여러 요구사항을 충족시키면서도 가볍고, 단순하고, 성능이 좋습니다. 이제까지 우리의 개발자들은 Immer를 즐기며 사용하고 있으며, 사용할 때 막히는 부분이 없고 학습 비용이 거의 들지 않습니다. — Workday Engineering

강사로부터:

저는 이제 (Immutable.js 대신) Immer를 선호합니다. — Cory House

오픈소스 메인테이너로부터:

이제부터 Immer를 사용할 것입니다. — Mark Erikson
react-copy-write 라이브러리는 내부적으로 @mweststrate가 제작한 Immer를 사용하고 있습니다. 이 라이브러리를 사용하면, 객체를 불변적으로 변경하기 위해 초안(draft)을 변경하게 됩니다. 그리고 이 때 구조적-공유(structural-sharing)가 일어나기 때문에, react-copy-write를 사용해도 필요할 때에만 re-rendering이 일어나게 할 수 있습니다. — Brandon Dail

그리고 React 팀으로부터:

“만약 당신이 MobX를 좋아한다면, @mweststrate의 Immer를 확인해보시길 강력히 추천합니다. MobX는 우리가 React를 발전시켜나가려는 방향과 멀어져있는 반면, Immer는 완전히 부합하고 있습니다.” — Sebastian Markbåge

React, 그리고 Immer를 사용해야 하는 이유

그러니까, Immer에는 마법같은 무언가가 있습니다. 그리고 Immer의 철학과 React의 설계 원칙이 어떻게 잘 부합한다는 것인지 생각해볼 가치가 있습니다.

React의 철학을 소개할 때 제가 즐겨 참조하는 두 문서가 있습니다. 하나는 공식 문서에 나오는 설계 원칙이고 또 하나는 의사코드를 통해 React에 숨어있는 개념을 소개하는 react-basic 문서입니다. 저는 여기서 Immer과 관련이 있는 세 가지 개념을 소개할 것입니다:

임시적 가변성 (Temporal Mutability)

React의 데이터 모델에서 state updater 함수를 사용할 때에는 기본적으로 불변성을 지켜야 합니다. 간단한 클릭 카운터 예제를 통해 확인해보겠습니다. 우리는 React에서 이렇게 하면 안된다는 것을 알고 있습니다:

// MobX?
clickHandler = () => this.state.count++

뭐 React를 재작성해서 이런 방식을 지원하게 만들수는 있겠지요. (혹은 바깥에서 MobX를 사용하는 방법도 있습니다.) 하여간, 위처럼 하는 대신 우리는 이렇게 합니다:

// React
clickHandler = () => this.setState(state => ({
count: state.count + 1
}))

이는 Immer의 Producer 사용법과 완벽히 부합합니다:

// Immer
const nextState = produce(currentState, draft => {
draft.count = draft.count + 1
})

상호운용성 (Interoperability)

Immutable.js를 사용할 때 겪는 가장 큰 문제는 상호운용이 어렵다는 것입니다. Netlify 내부에서 2년동안 Immutable을 잘 써오긴 했지만, “우리가 일반적인 JavaScript를 사용하고 있는 것이 아니”라는 사실을 계속 되뇌어야 했습니다. 특히 Immutable.js의 Map을 분해대입 할 때 말이죠:

// Immutable.js
const map1 = Immutable.Map({ foo: 1, bar: 2 })
const { foo, bar } = map1
console.log(foo) // undefined
console.log(bar) // undefined

이는 문제가 있는 추상화입니다. 우리가 다루는 변수가 Immutable.js로 감싸져있는지를 계속 생각해야만 하기 때문입니다.

하지만 Immer를 사용하면, 여러분이 사용하는 객체와 배열은 진짜 JavaScript의 객체와 배열이 맞습니다. 즉, 원래 할 수 있는 모든 작업을 Immer를 쓰면서도 할 수 있습니다:

// Immer
const map1 = { foo: 1, bar: 2 }
const map2 = produce(map1, draft => {
draft.foo += 10
})
const { foo, bar } = map2
console.log(foo) // 11
console.log(map1.bar === bar) // true

이런 상호운용성은 사실 React가 성공할 수 있었던 요인이기도 합니다 — React는 상호운용성에 초점을 두고 만들어졌고, 그래서 점진적인 도입이 가능했습니다. (React를 도입하기 위해서 모든 것을 재작성하는 대신 말이죠.) 또한 React는 기존 JavaScript 생태계에 있던 다른 라이브러리들과도 같이 사용할 수 있었습니다. 2010년 경에 proxy가 도입되던 때에도 Brendan Eich는 같은 목표를 추구했습니다. Immer는 proxy를 사용해서 자연스럽게 언어의 기능을 확장시킨 좋은 사례입니다!

Debugging

Immer가 내장하고 있는 Patch 기능은 세밀한 디버깅 및 원인 추적을 가능하게 합니다. 아마 이 기능을 활용해서 개발자 도구를 만들 수 있을 것 같습니다.

이런 점은 React가 디버그 용이성에 초점을 맞춘 것과 비슷합니다. 예를 들면 UI 변경이 잘못되었을 때 문제를 일으킨 부분을 바로 추적할 수 있다던가, React DevTools 같은 훌륭한 개발자도구를 그 위에 만들 수 있었죠.

더해서, Patch 기능을 통해 Redux 방식의 undo/redo 구현을 쉽게 할 수 있습니다. 이 글에 포함시키기에는 양이 많아서 자료를 링크해놓았으니 확인해주세요.

강력함을 유지하는 기술

Churn**이 JavaScript 생태계의 어쩔 수 없는 밈이 돼버린 지금, 지속적으로 성장하기 위해서는 강력함을 유지할 수 있는 기술이 무엇인지 가려내는 능력이 반드시 필요합니다. 우리 회사의 CEO인 Mathias Biilmann은 이 능력을 갖추기 위해 필요한 세 가지 요소에 대해 글을 쓴 바 있습니다:

  • 당신이 몸담고 있는 분야의 역사를 배워라.
  • 사람, 문화, 그리고 커뮤니티는 중요하다.
  • 항상 “왜” 를 이해하려고 노력해라.

고백하자면, 이 글을 읽으실 독자 여러분이 이러한 멘탈 모델을 따라올 수 있도록 글을 썼습니다. 저는 이 방법이 매우 훌륭하다고 생각합니다. 특정 기술을 평가할 때, 또는 어떤 오픈소스 프로젝트가 커뮤니티의 환영을 받는지를 이해할 때 말이죠.

Immer의 폭발적인 성장은 주목할만 합니다만, 그 이유가 무엇인지를 이해하는 것이 더 중요합니다 — Immer는 과거로부터의 여러 교훈 위에 만들어졌고, React 커뮤니티에서 이미 많은 지지를 받고 있습니다. 그리고 이 성과는 그 기초에 있는 철학이 있었기에 가능했습니다.

Dan Abramov 역시 이와 같은 기술의 진화 방식, 그리고 패러다임의 전환이 일어나는 방식에 대해 최근 지적한 바 있습니다:

성공 레시피: 디버그하기 쉬운 것을 골라잡고, 이것을 사용하기 더 쉽게 만들어라. Immer를 만들어준 @mweststrate에게 감사를! — Dan Abramov

저는 이것이 심오한 통찰이라고 생각합니다 — 이전의 기술들이 불변성에서 오는 이점을 가져다주면서도, 문제가 있는 API를 남겨두었기 때문에 Immer가 성공할 수 있었던 것입니다. Immer는 같은 이점을 주면서도 API를 개선하는데 초점을 맞추었습니다. 마치 React가 그랬던 것처럼 말이죠.

Immer를 당장 도입하고 싶을 때 가장 좋은 방법은 Redux나 React의 setState에 있는 긴 코드를 Immer를 가지고 줄여보는 것입니다. 시간이 좀 더 지난 뒤에, 아래와 같이 Immer를 기반으로 구현된 라이브러리들을 찾아보세요. 이런 라이브러리를 통해 빠르면서도 코드를 길게 작성할 필요가 없는 앱을 만들 수 있을 것입니다!

역주

*: 이 문장은 무슨 의미인지 잘 모르겠습니다. 혹시 아시는 분은 댓글 남겨주시면 감사하겠습니다.

**: 사전적 의미는 ‘우유를 휘저어서 버터를 만드는 통’. JavaScript 생태계에서 특정 기술이 유행했다가 순식간에 구식이 되어버리는 어지러운 현상을 이르는 말.