리액트에서 가장 중요한 라이브러리가 바로 상태 관리 라이브러리
이다.
리액트(React)는 주로 단방향 데이터 바인딩을 사용합니다. 이는 부모 컴포넌트에서 자식 컴포넌트로 state
를 props
형태로 전달할 수 있지만, 반대로 자식 컴포넌트에서 부모 컴포넌트로 props
를 직접 전달할 수는 없다. 자식 컴포넌트가 부모 컴포넌트의 state
를 변경하려면, 부모 컴포넌트의 setState
함수를 props
로 전달받아 사용해야한다.
이 방식은 컴포넌트 간 데이터 흐름을 명확하게 유지하는 장점이 있지만, 복잡한 애플리케이션에서는 props를 여러 단계에 걸쳐 전달하는 문제가 발생할 수 있다. 이를 Props Drilling이라고 하며, 프로젝트의 규모가 커질수록 props의 깊이가 증가하여 불필요한 리렌더링을 초래할 수 있다.
컴포넌트의 재사용성과 의존성 분리 측면에서 props
를 사용하여 state
를 관리하는 것은 유용합니다. 그러나 프로젝트 전반에서 사용되는 데이터는 전역 상태로 관리하고, 이를 보완하는 부가적인 state만 props로 처리하는 것이 더 효율적이다.
--> 코드의 유지보수성을 높이고 불필요한 리렌더링을 줄이는 데 도움이 됩니다.
대표적인 라이브러리로는 Redux
, Tanstack-query
, Recoil
등이 있습니다. 각 라이브러리의 장단점을 살펴보고, 프로젝트의 요구 사항에 맞게 적절한 라이브러리를 선택하는 것이 좋습니다.
Redux는 데이터가 단방향으로 흐르는 구조
이 구조는 애플리케이션의 상태 관리와 데이터 흐름을 예측 가능하고 명확하게 만든다. Redux의 주요 구성 요소는action
,reducer
,selector
,store
로 이루어집니다. 이러한 구성 요소들은 유지보수에 유리한 장점을 제공하지만, 상태의 개수가 적더라도 많은 보일러플레이트 코드가 필요하다는 단점이 있다.
Action
: 상태 변화를 요청하는 객체로, type 속성과 필요한 추가 데이터를 포함합니다.Reducer
: action을 처리하여 새로운 상태를 반환하는 순수 함수입니다.Store
: 애플리케이션의 상태를 저장하는 중앙 집중식 저장소입니다. 모든 상태 변경은 store를 통해 이루어집니다.Selector
: store에서 필요한 상태를 추출하는 함수입니다.Redux는 모든 상태를 중앙 집중식으로 관리
하나의 store
에 모든 상태를 저장하고, 애플리케이션의 외부 요소로 간주됩니다. 따라서 리액트 내부에서 직접 접근할 수 없으며, 상태 변경은 반드시 action
객체를 통해 이루어집니다.
Redux 자체는 비동기 데이터를 처리하는 기능을 제공하지 않음
비동기 작업을 처리하려면 redux-thunk
나 redux-saga
와 같은 별도의 미들웨어를 추가로 사용해야한다. 이러한 미들웨어는 비동기 로직을 관리하고, 이를 Redux의 단방향 데이터 흐름에 통합할 수 있게 해준다.
예측 가능성
: 상태 변경이 단방향으로 일어나고, 모든 상태 변화가 store에 집중되기 때문에 예측 가능한 결과를 얻을 수 있다.
디버깅 용이성
: Redux는 하나의 store와 객체 트리를 사용하여 상태를 관리하기 때문에 디버깅이 용이합니다. 상태의 변화는 action 객체를 통해서만 이루어지며, 이로 인해 상태 변화를 쉽게 추적할 수 있다.
확장성
: 보일러플레이트 코드가 많다는 단점에도 불구하고, Redux의 구조는 애플리케이션 확장과 유지보수에 유리하다.
보일러플레이트 코드
: 상태의 개수가 적더라도 action, reducer, store 등을 설정하는 많은 양의 보일러플레이트 코드가 필요하다.
비동기 데이터 처리
: 비동기 작업을 처리하기 위해 추가적인 라이브러리나 미들웨어가 필요하다.
Redux는 엄격한 데이터 관리 방식을 채택하여 보일러플레이트 코드가 많아지는 단점이 있지만, 이는 확장성과 디버깅의 용이성 측면에서 큰 강점으로 작용합한다. 모든 상태 변경이 예측 가능하고 추적 가능하기 때문에 복잡한 애플리케이션에서도 안정적으로 상태를 관리할 수 있다.
MobX는 사용이 간편하고 코드 양이 적다는 장점을 가지고 있지만, Redux에 비해 사용률이 낮다.
불변성에 대한 자유로움
: MobX는 Redux와 달리 불변성(immutability)에 엄격하지 않습니다. 상태를 직접 변경할 수 있으며, 상태 변경 시 자동으로 반응형 업데이트가 일어난다. 이는 개발자가 불변성에 신경 쓰지 않아도 되므로 구현이 용이하다.
다수의 Store
: MobX는 여러 개의 store를 사용할 수 있습니다. 이는 상태를 논리적으로 분리하여 관리할 수 있는 유연성을 제공한다. 하지만 상태 변경 시 여러 store에 영향을 미칠 수 있어 복잡한 프로젝트에서는 관리가 어려울 수 있다.
액션 없이 상태 변경
: MobX는 Redux와 달리 action 객체 없이도 상태를 업데이트할 수 있다. 이는 구현을 간단하게 하지만, 테스트와 유지보수 측면에서는 문제를 일으킬 수 있다.
테스트와 유지보수
MobX의 자유로운 상태 변경 방식은 초기 개발 단계에서는 편리하지만, 프로젝트가 커지고 복잡해질수록 상태 관리가 어려워진다.
확장성
대규모 프로젝트에서는 Redux의 구조화된 접근 방식이 더 적합합니다. MobX는 작은 규모의 프로젝트에서는 효율적이지만, 대규모 프로젝트에서는 유지보수와 확장성이 문제가 될 수 있다.
커뮤니티와 지원
MobX는 상대적으로 덜 사용되므로 지원과 자료가 제한적일 수 있다.
컴포넌트 트리 전체에서 데이터를 전역적으로 공유할 수 있도록 한다. 리액트가 제차적으로 가지고 있다.
정적인 데이터 위주로 처리하거나 업데이트가 자주 발생하지 않을 때 사용하기 적합하다.
정적인 데이터 처리
: Context API는 주로 테마, 사용자 인증 정보, 언어 설정 등과 같이 변경되지 않거나 드물게 변경되는 정적인 데이터를 처리하는 데 적합하다.
전역 상태 관리
: 여러 컴포넌트에서 공통적으로 사용하는 상태를 전역적으로 관리할 수 있다.
복잡한 업데이트 처리의 비효율성
: Context API는 복잡한 업데이트를 처리할 때 비효율적일 수 있다.
Context의 Provider로 감싸진 컴포넌트들이 상태의 변화를 감지할 때, 실제로 업데이트가 필요하지 않은 컴포넌트들까지도 리렌더링됩니다. 이는 성능 저하로 이어질 수 있다.
Recoil: Context API의 보완
이러한 한계를 보완하기 위해 나온 것이 Recoil 라이브러리입니다. Recoil은 리액트의 상태 관리를 더 효율적으로 처리할 수 있도록 도와줍니다.
Atom과 Selector: Recoil은 상태를 atom이라는 단위로 관리하며, selector를 통해 파생 상태를 생성하고 관리합니다. 이는 상태의 변경이 필요한 최소한의 컴포넌트만 리렌더링되도록 하여 성능을 최적화합니다.
더 나은 상태 추적: Recoil은 복잡한 상태 변화와 의존성을 효율적으로 관리할 수 있도록 도와줍니다. 상태가 변경될 때마다 의존성을 추적하여 필요한 부분만 업데이트합니다.
Atom과 Selector라는 두 가지 주요 개념을 통해 상태를 관리한다.
Atom은 상태의 단위로, 고유한 키값을 통해 구분
특정 Atom을 구독하고 있는 컴포넌트들만 상태 변경 시 선택적으로 리렌더링된다.
Selector는 Atom의 상태 변화를 다루는 순수 함수
비동기 처리 및 데이터 캐싱 기능을 제공하여 비동기 데이터를 다루기에 용이하다.
Recoil은 단순한 get/set 인터페이스를 통해 상태를 공유할 수 있어 보일러플레이트 코드가 없다.
Recoil은 React의 동시성 모드와 양립할 가능성이 있어, 더 나은 성능과 사용자 경험을 제공한다.
상태를 분산적으로 둘 수 있어 코드 스플리팅이 가능하며, 로딩 시간을 개선할 수 있다.
다른 라이브러리와 병합하여 사용할 수 있다.
Tanstack-Query
: API로부터 데이터를 불러오고 저장, 자동화하는 데 사용
Recoil
: 상태 관리 용도로 사용하여 미니 리덕스처럼 동작
Apollo-client
: GraphQL API와의 통신을 담당
Recoil
: 상태 관리 용도로 사용
코드에서 당장 필요한 부분만 로딩하고, 나중에 필요할 때 추가로 로드함으로써 초기 로딩 시간을 개선하는 방법
사용자 경험을 개선하고, 애플리케이션의 성능을 최적화할 수 있다.