이 포스트는 Kent C. Dodds의 Application State Management을 번역한 글입니다.
소프트웨어 개발에서 가장 어려운 부분 중 하나는 상태 관리(managing state)입니다. 만약 사용자가 애플리케이션과 상호작용을 전혀 하지 않았더라면 우리 삶은 훨씬 단순했을 겁니다. 물론 90년대 웹사이트에서나 할 이야기입니다. 우리는 상호작용이 필요한 애플리케이션을 만들어야 하니 어디엔가 상태를 저장해야 합니다.
상태는 정말 복잡하며 애플리케이션마다 사용 사례가 극과 극으로 다릅니다. 그래서 애플리케이션에서 상태를 관리하는 방법도 엄청나게 다양합니다. 각각의 방법은 특정 방법을 해결하는데 초점을 맞추고 있으며 어떤 추상이든 이 문제에 따른 고통을 줄이기 위해서 애플리케이션에 복잡도를 더해야 합니다.
어떤 추상을 사용해야 하는가에 대한 핵심은 그 추상의 비용과 이점을 이해하는 데 있습니다. 이득이 무엇인지 알려면 그 추상이 풀기 위한 문제가 무엇인지, 그 문제를 해결하기 위한 다른 해법은 무엇인지 이해할 필요가 있습니다. 이 글에서는 제가 사용해보고 좋았던 추상도 공유하려고 합니다. 물론 여기서는 책이 아니기 때문에 모든 경우를 다 다루지 않고 기본적인 부분에 대해서만 설명합니다.
이 글에서는 애플리케이션의 규모와 복잡도가 성장할 때 추상을 추가하는 것을 어떻게 고려하게 되는지와 동일한 방법으로 접근합니다. 추상을 너무 이르게 추가하지 않는 것은 중요합니다. 필요보다 먼저 추가한 추상은 그 이득보다 더 많은 비용을 지불하게 될 수 있습니다.
그리고 여기서는 리액트에 특정한 해법을 다루지만 다른 프레임워크에서도 충분히 적용할 수 있는 일반적인 개념이 되었으면 합니다.
컴포넌트의 상태
먼저 컴포넌트의 상태부터 시작합니다. 상호작용이 필요한 애플리케이션이라면 어디선가 setState
를 사용하게 될겁니다. 저는 생각보다 리액트 컴포넌트의 상태 API가 심할 정도로 인정을 덜 받고 덜 사용된다고 느낍니다. 믿을 수 없을 정도로 단순한 API며 이 API를 사용하더라도 애플리케이션에 그다지 복잡함을 더하지도 않습니다.
이 지식을 배우거나 다시 살펴보고 싶다면 다음 7분 분량의 (무료) 비디오를 확인해보세요. "리액트에서 컴포넌트 상태 사용하기". 그리고 공식 문서의 상태 들어 올리기를 확인해보세요.
이후 내용을 더 보기 전에 상태 API를 어디까지 사용할 수 있는지 보는 것을 진지하게 추천합니다. 이 API 자체만으로도 정말 강력하기 때문입니다.
컴포넌트의 상태를 사용하다가 문제가 생기기 시작하는 지점은 "프로퍼티 내리꽂기"에서 문제를 마주하게 될 때입니다. 프로퍼티 내리꽂기를 참고하세요. (역주: 번역)
자바스크립트 모듈 (싱글톤)
만약 싱글톤(singleton) 패턴에 익숙하지 않더라도 걱정하지 마세요. 자바스크립트에서는 상당히 직관적인 편입니다. 이는 단순히 모듈 안에 상태를 보관하는 패턴입니다. 이 패턴을 JS로 간단히 구현하면 다음과 같습니다.
const state = {}
const getState = () => state
const setState = newState => Object.assign(state, newState)
export {getState, setState}
"프로퍼티 내리꽂기 문제"에서 오는 불편함을 느끼기 시작할 때면 컴포넌트 상태 API에서 다음 단계로 넘어가는 것을 고려하게 됩니다. 리액트 애플리케이션이 성장한다는 것은 "컴포넌트 트리"도 함께 성장한다는 뜻입니다. 이 과정에서 애플리케이션에서 널리 공유되는 상태를 트리 위로 끌어 올리게 됩니다. 비대해진 컴포넌트 트리에서는 각 컴포넌트가 필요로 하는 상태와 상태를 갱신하기 위한 핸들러를 내려받는 과정이 정말 번거로워집니다.
그래서 일반적인 JS 모듈을 사용해 필요한 상태를 불러올 수 있다는 점에서 싱글톤 패턴은 꽤 괜찮은 선택지입니다. 그러니 프로퍼티를 내려 꽂는 것보다 컴포넌트 스스로 모듈을 불러와서 상태를 사용할 수 있게 됩니다.
물론 이 방법은 심각한 한계가 있습니다. 이 싱글톤에서 상태를 갱신할 때마다 컴포넌트가 다시 렌더링을 하도록 주의해야 합니다. 이 과정은 구현에 조금 더 노력을 필요로 합니다. (간단히 말해서 이벤트 에미터 (event emitter)를 구현할 필요가 있습니다.) 하지만 엄청 어려운 것은 아니며 이 과정을 도와줄 라이브러리도 존재합니다. 그러니 싱글톤 패턴을 사용하는 방법이 그렇게 나쁜 선택지는 아닙니다. 서버-사이드 렌더링을 필요로 하지 않는 경우라면 말이죠. (물론 대부분은 이 부분도 필요로 할 겁니다...)
컨텍스트 Context
리액트의 새 컨텍스트 API는 정말 멋집니다. 아직 이 기능을 모르신다면 리액트⚛️ 의 새 컨텍스트 API를 확인해보세요.
리액트 컨텍스트를 사용하면 프로퍼티 내리꽂기 문제와 싱글톤을 갱신하는 문제를 간단한 내장 API로 극복할 수 있게 됩니다. 이 API는 간단히 <ContextInstance.Provider />
와 <ContextInstnace.Consumer />
컴포넌트를 사용해서 어디서든지 상태에 접근할 수 있도록 구성할 수 있게 됩니다.
새로운 컨텍스트 API가 매우 단순한 덕분에 정말 많은 상황에서 단순히 싱글톤 패턴을 적용하는 것이 훨씬 간단해졌습니다. 리액트에서 컴포넌트 상태를 setState
로 간단히 해결하는 것처럼 createContext
를 사용해서 애플리케이션 상태를 해결할 수 있게 되었다는 점에서 정말 멋진 기능입니다.
Unstated
제임스 카일은 새 컨텍스트 API와 함께 사용할 수 있는, 새로운 상태 라이브러리를 만들었습니다. 이 새로운 라이브러리는 제가 상태를 공유할 일이 있는 앱이 있을 때 사용할 정도로 최애 라이브러리입니다. 이 라이브러리는 컨텍스트 위에 많은 것을 올리지 않는 조그마한 라이브러리라 좋습니다. 그리고 상태 컨테이너와 표현 컴포넌트를 명확하게 분리하고 있어서 모든 코드가 테스트하기 편리하니 한번 염두해보세요.
redux
Redux가 풀려고 의도했던 문제는 flux를 좀 더 손쉽게 사용할 수 있도록 하는 부분이었습니다. Flux는 상태의 흐름을 예측 가능하도록 만들어서 상태를 디버깅하는 방법을 개선하려고 했습니다. 또한 저장소에서 나온 데이터를 연산하는 과정을 단순화하려고 했습니다. 이 두 문제를 해결하기 위해서 명확한 관심사 분리와 단방향 데이터 흐름을 사용했습니다.
Redux는 이 과정을 정말 단순화해서 "flux 전쟁"을 2015년에 종식했습니다. 기존 Flux라면 필요했던 수많은 추상을 구현하지 않고도 flux의 이점을 누릴 수 있게 되었습니다. Redux 기반 상태 관리는 강점을 갖는 경우도 많고 사용 사례 또한 많습니다. 저는 이 라이브러리가 애플리케이션 상태 관리의 어려움을 해결했다는 점에 너무 기쁩니다. 이 라이브러리는 정말 많은 사람에게 도움이 되었습니다!
다시 말하면, 이 라이브러리를 사용한 앱이라면 엄청난 양의 복잡도를 더하게 되었다는 의미입니다. 이 라이브러리는 우회적인 계층을 애플리케이션에 추가하는 것으로 복잡한 사용자 상호작용을 구현하고 상태를 갱신했습니다. 이 라이브러리를 사용하는 프로젝트에 참여한 적이 있다면 디스패치와 액션, 크리에이터와 리듀서를 다 열어서 액션 타입명을 비교하는 일을 해봤을 겁니다. 그렇다면 복잡도에 대한 이야기도 이해할거라 생각합니다.
많은 애플리케이션이 redux를 사용하고 채택하는 이유는 react-redux의 connect
고차 함수가 프로퍼티 내리꽂기 문제를 우연히 해결했기 때문이라 봅니다. 프로퍼티 내리꽂기 문제는 상대적으로 작은 애플리케이션에서도 겪을 수 있는 고통인데 이 문제를 해결하기 위해 redux를 고려하는 경우가 많습니다. 더 쉬운 대안이 있다는 것을 모른 상태로 말이죠.
프로퍼티 내리꽂기 문제를 해결하기 위해 redux를 선택한다면 해결할 필요 없던 문제를 위해 비용을 지불하게 되는데 그로 인해 실제로 얻는 이득보다 더 많은 비용을 내게 됩니다.
그러니 다른 해결책을 먼저 사용해보세요. 그리고 상태를 필요로 하는 트리를 기준으로 redux에 보관하는 상태의 분량을 제한하세요. (일반적인 redux 사용자라면 최상위에 위치하고 있을 겁니다.)
코리 하우스가 이른 redux 적용에 대해 다음과 같은 트윗을 남겼습니다.
Realization: Putting Redux in our company framework by default was a mistake.
— Cory House 🏠 (@housecor) February 11, 2018
Result:
1 People connect *every* component.
2 People embed Redux in "reusable" components.
3 Everyone uses Redux. Even when they don't need it.
4 People don't know how to build an app with just React.
깨달음: Redux를 회사 프레임워크에 기본으로 넣은 것은 실수였다.
결과:
- 사람들이 모든 컴포넌트를 연결함.
- 사람들이 재사용 가능한 컴포넌트에도 Redux를 포함함.
- 모두가 Redux를 사용함. 심지어 필요가 없는 경우에도.
- 사람들이 React만 갖고 앱을 만들 줄 모름.
— 코리 하우스
결론
이 문제를 해결하기 위해 구현할 수 있는 수많은 추상과 패턴이 있지만 제가 이걸 다 적으려면 올해 내내 적어도 시간이 모자랄 겁니다. 😉
그래서 제가 강조하고 싶은 부분은 상태가 존재한다면 필요한 곳에 최대한 가까이 보관하라는 점입니다. 실무적인 용어로 설명하면 폼 입력창의 오류 상태를 전역 스토어에 보관하지 말라는 뜻입니다. 정말로 필요한 경우가 아니라면 말이죠. (그런 경우는 분명 드물껍니다.) 이 규칙을 따른다면 애플리케이션에서 컴포넌트 상태를 사용할 가능성이 매우 높다 는 뜻이고 이런 경우에 컨텍스트나 싱글톤을 애플리케이션 어딘가에서 아마도 사용하고 싶어질 겁니다. 컴포넌트 트리의 작은 일부 영역이라도 이 접근 방법은 매우 유용합니다.