원문 : Application State Management with React
리액트가 어플리케이션의 상태 관리를 하는데 필요한 모든 것.
상태관리는 어플리케이션에서 가장 어려운 일중 하나입니다. 이는 수 많은 상태 관리 라이브러리가 존재하고, 매일 생겨나는 이유입니다. (심지어 일부는 다른 라이브러리 위에 구축됩니다. npm에는 수백 가지의 "쉬운 redux" 추상화가 있습니다.) 상태 관리가 어려운 이유 중 하나는 우리가 상태를 관리하기 위한 솔루션을 과도하게 엔지니어링하기 때문입니다.
제가 React를 사용하는 한, 개인적으로 구현하고자 했던 상태 관리 솔루션이 하나 있습니다. React Hooks(및 React Context의 대대적인 개선)가 출시되면서 이 솔루션이 대폭 단순화되었습니다.
우리는 종종 React Component를 레고 블록에 비유하곤 하는데, 사람들은 이 말을 들으면 상태 관리 측면을 배제한다고 생각합니다. 상태 관리 문제에 대한 저의 개인적인 솔루션의 "비밀"은 어플리케이션의 상태가 어플리케이션 트리 구조에 어떻게 매핑되는지 생각해 보는 것입니다.
Redux가 성공한 이유 중 하나는 react-redux가 prop drilling 문제를 해결했기 때문입니다. 컴포넌트를 마법같은 connect
함수에 전달하기만 하면, 트리의 서로 다른 부분이 데이터를 공유할 수 있다는 사실은 아주 멋진 일이었습니다. reducers/action creators/etc를 사용하는 것도 훌륭하지만, 나는 redux의 편재성은 개발자가 프롭 드릴링에 고통 받는 문제를 해결했기 때문이라고 확신합니다.
편재성(ubiquity) : 자원이 특정 지역에 매장되어 있는 특징을 나타내는데, 여기서는 redux가 성공한 이유, 많은 사람들이 redux를 쓰는 상황을 빗댄 것 같다.
나는 개발자들이 글로벌, 지역 구분 없이 모든 상태를 redux에 집어 넣는 것을 자주 보았습니다. 이는 많은 문제를 야기하는데, 적어도 상태 상호 작용을 유지할 때 reducer, action creator/types, dispatch 호출과 상호작용합니다. 이 때문에 무슨 일이 일어나는지, 나머지 코드베이스에 어떤 영향을 미치는지 알기 위해서 궁극적으로는 머릿속에서 많은 파일을 열고 코드를 추적해야합니다.
정리하자면, redux는 글로벌 상태에 대해서는 괜찮지만, 간단한 상태(예를 들면 modal의 open, form input의 value 값)의 경우에는 큰 문제가 발생합니다. 설상가상으로 reduex는 확장성이 좋지 않습니다. 당신의 어플리케이션이 크면 클수록, 이 문제는 더 어려워집니다. 물론, 다른 reducer로 다른 부분을 관리할 수도 있습니다. 하지만 이러한 모든 action creator와 reducer를 거치는 간접적인 방식은 최적의 방법이 아닙니다.
단일 객체가 모든 어플리케이션 상태를 가진다면, Redux를 사용하지 않더라도 또 다른 문제가 발생할 수 있습니다. React의 <Context.Provider>
가 새로운 값을 얻으면, 해당 값을 사용하는 모든 컴포넌트가 업데이트되고, 렌더링 되어야 합니다. 심지어 해당 데이터의 일부만 사용하는 함수 컴포넌트인 경우에도 마찬가지입니다. 이는 잠재적인 성능 이슈를 야기할 것입니다. (React-Redux v6는 redux가 React-hooks와 사용하면 잘 동작하지 않는 다는것을 깨닫기 전까지, 이러한 접근 방식을 사용하려 했습니다. 이후 v7에서는 다른 접근 방식을 사용하게 됩니다.) 하지만 제 요점은, 상태가 더 논리적으로 분리되어 있고, 리액트 tree에 그 상태가 더 중요한 곳에 위치하고 있다면, 이 문제는 발생하지 않는다는 것입니다.
React를 사용해서 어플리케이션을 빌드하는 경우, 이미 상태 관리 라이브러리가 설치되어 있습니다. 여러분은 npm(또는 yarn)으로 설치할 필요 없습니다. 사용자에게 추가적인 byte 비용이 들지 않고, npm의 모든 React 패키지와 통합되어 React 팀에 의해 잘 문서화되어 있습니다. 이미 React 그 자체입니다.
React is a state management library
React 어플리케이션을 빌드 할 때, <App />
에서 시작해서 <input />
, <div />
, <button />
으로 끝나는 컴포넌트 트리를 만들기 위해 컴포넌트 구성 요소를 조립합니다. 여러분은 어플리케이션이 렌더하는 low-level의 복합 컴포넌트를 모두 관리하지 않습니다. 대신 각각의 컴포넌트가 이를 관리하는게 UI를 구축하는 매우 효율적인 방법입니다. 여러분은 상태를 관리하며 이런 방식을 사용할 수 있고, 하고 있을 가능성이 매우 높습니다.
여기서 말한 모든 것들이 class component에서도 동일하게 작동합니다. hooks는 단지 일을 좀 더 쉽게 만듭니다 (잠시후 살펴볼 Context)
"단일 컴포넌트에서 관리되는 단일 상태 요소를 갖는 것은 쉽습니다. 하지만 컴포넌트 간에 상태를 공유해야할 때는 어떻게 해야 하나요? 예를 들어, 아래와 같은 상황에서요"
"count
는 <Counter />
내부에서 관리됩니다. 이제, <CountDisplay />
에서 해당 카운트에 접근하고, <Counter />
에서 그 값을 업데이트 하기 위한 상태 관리 라이브러리가 필요합니다!"
해답은 React가 처음 나왔을 때만큼 오래전부터 존재했고, 관련 문서가 존재합니다. Lifting State Up
"Lifting State Up"은 리액트의 상태 관리 문제에 대한 합법적이고 확실한 해결책입니다. 현재 상황에 적용해 보겠습니다.
우리는 그저 상태를 누가 책임질 것인지 변경했을 뿐이고, 이는 아주 간단합니다. 그리고 우리는 앱의 맨 위로 상태를 계속 끌어 올릴 수 있습니다.
"좋아. 하지만, prop drilling 문제는 어떻게 할건데?"
좋은 질문입니다. 이에 대한 첫 번째 대답은 컴포넌트를 구성하는 방식을 바꾸는 것입니다.
위 예시 대신, 아래처럼 바꿀 수 있습니다.
이는 매우 인위적이라서 명확하지 않습니다. Michael Jackson의 영상이 제가 하고자 하는 말을 이해하는데 도움을 줄 겁니다.
하지만 이러한 구성이 잘 동작하지 않을 것이기 때문에, 여러분의 다음 단계는 React의 ContextAPI 입니다. 이는 오랫동안 "해답"이었지만, 오랫동안 "비공식적인" 해답이었습니다. react-redux
가 제가 언급한 메커니즘으로 문제를 해결했기 때문에, 많은 사람들이 React 공식 문서에서 있는 경고를 걱정하지 않아도 되는 redux를 사용했습니다. 하지만 지금은 context
는 React API에서 공식적으로 지원되는 부분이므로, 문제 없이 직접 사용할 수 있습니다.
NOTE : 이 특정 코드 예시는 매우 인위적이며 이러한 특정 시나리오를 해결하기 위해 CONTEXT에 도달하는 것은 권하지 않습니다! Prop Drilling이 왜 문제가 되지 않고, 종종 더 바람직한지 이해하려면 Prop Drilling를 읽어보세요. context를 너무 빨리 사용하지 마세요!
그리고 이 접근법의 멋진 점은, 우리의 useCount
hook에 상태를 업데이트하는 일반적인 방법에 대한 모든 로직을 넣을 수 있다는 것입니다. (커스터마이징 훅을 만들 수 있다는 얘기입니다.)
그리고 이것을 useState
대신 useReducer
로 쉽게 변경할 수 있습니다.
이는 엄청난 유연성을 제공하고 복잡성을 줄여줍니다. 여기 이러한 방식으로 작업할 때 기억해야할 중요한 사항이 몇 가지 있습니다.
두 번째 요점에 대해 자세히 알아보겠습니다.
각 페이지는 하위 컴포넌트가 필요한 데이터를 가지고 있는 자신만의 provider를 가질 수 있습니다. 코드 분할은 이 작업에도 "그냥 작동"합니다. 각 provider로부터 데이터를 가져오는 방법은 각 provider가 사용하는 hooks와 어플리케이션이 데이터를 회수하는 방식에 달려있습니다. 하지만 여러분은 이러한 작업이 provider에서 어떻게 동작하는지 알기 위해, 어디서부터 시작해야하는지 알고 있습니다.
이러한 colocation이 왜 유익한지 알기 위해, "State Colocation will make your React app faster"와 "Colocation"를 확인하세요. 그리고 context를 더 이해하기 위해 How to use React Context effectively
를 읽어보세요
마지막으로 추가하고 싶은게 있습니다. 상태에는 다양한 범주가 있지만, 모든 상태의 유형은 두 가지 버킷 중 하나에 속할 수 있습니다.
우리는 이 둘을 결합할 때 실수를 합니다. server cache는 UI 상태와 본질적으로 다른 문제가 있으므로 다르게 관리해야합니다. 만약 여러분이 가지고 있는게 실제로 상태가 아니라 상태의 캐시라는 사실을 받아들인다면, 여러분은 이것에 대해 올바르게 생각하고, 올바르게 관리할 수 있습니다.
여러분은 여기저기서 올바른 useContext
를 사용해서 자신의 useReducer
, useState
를 직접 관리할 수 있습니다.
하지만 캐싱은 컴퓨터 공학에서 가장 어려운 문제중 하나입니다. 따라서 이 문제에 대해서는 거인과 어깨를 나란히 하는 것이 현명할 것입니다.
(여기서 거인은 라이브러리나.. 기타 도움을 주는 도구를 말하는 것 같습니다.)
이것이 제가 이런 종류의 상태를 위해 react-query를 사용하고 추천하는 이유입니다. 물론 제가 위에서 상태 관리 라이브러리가 필요없다고 말하긴 했지만, 저는 react-query가 상태 관리 라이브러리가 아니라 캐시라고 생각합니다. 한 번 살펴보세요! 굉장히 좋은 것입니다. Tanner Linsley는 smart cookie(결정을 잘 하는 똑똑한 사람??)입니다.
위의 조언을 따른다면 성능은 별 문제가 되지 않습니다. 특히 the recommendations around colocation 이 문서의 내용을 따른다면. 하지만 성능에 문제가 될 수 있는 use case가 있습니다. 만약 상태 관련 성능 문제가 있는 경우, 가장 먼저 확인해야할 것은 상태가 변경됨으로써 리렌더링 되는 컴포넌트의 수와 이 컴포넌트들이 해당 상태 변경으로 정말 리렌더링 되어야하는지 여부입니다. 만약 그렇다면 성능 문제는 상태 관리 메커니즘이 아닌 렌더링 속도에 있으니, 렌더링 속도를 높여야합니다.
그러나 DOM 업데이트 없이 리렌더링 되거나, side-effect이 필요한 경우라면, 해당 컴포넌트는 불필요하게 리렌더링 됩니다. 이것은 React에서 자주 발생하며 일반적으로 그 자체는 문제가 되지 않습니다. (불필요한 리렌더링이 발생하더라도, 어플리케이션을 먼저 만드는데 집중해야합니다.) 하지만 병목 현상이 발생하는 경우, React context 통한 상태를 사용해서 성능 문제를 해결하는 몇 가지 방법이 있습니다.
상태를 하나의 store에 두기 보다는 논리적으로 다른 부분으로 분리합니다. 이로인해 단일 업데이트가 모든 컴포넌트에 대한 업데이트를 야기하지 않습니다.
jotai를 가져오세요
라이브러리에 대한 또 다른 권장사항이 있습니다. 사실 React의 내장 상태 관리 추상화(abstractions)가 적합하지 않은 use case가 몇 가지 존재합니다. 사용 할 수 있는 모든 추상화 중에서 jotai가 가장 유망합니다. 이런 사례가 무엇인지 궁금하다면 jotai가 잘 해결하는 유형의 문제는 이 영상에 잘 설명되어 있습니다. Recoil과 jotai는 매우 유사합니다(동일한 유형의 문제를 해결). 하지만 제한적으로 이 둘은 경험해보니 개인적으로 jotai가 좋은 것 같습니다.
어쨌든 대부분의 앱에는 recoil이나 jotai 같은 원자 상태 관리 도구가 필요하지 않습니다.
다시 한 번 말하지만 이는 class 컴포넌트로 할 수 있는 일입니다(hooks를 사용할 필요가 없습니다). hooks를 사용하면 더 편리하지만, 리액트 15로 이 철학을 문제없이 구현할 수 있습니다. 상태를 가능한 지역적으로 관리하고, context는 prop drilling이 문제가 될 때만 사용하세요. 이 방식은 상태 상호 작용을 더 쉽게 유지할 수 있습니다.