
React로 프론트엔드 개발을 하면서 가장 기본적이고도 중요한 것은 단연 상태 관리라고 생각합니다.
React에서는 상태를 관리할 수 있는 방법이 많이 있습니다.
단순한 상태를 나타내는useState와 복잡한 상태를 나타내는 useReducer가 있습니다.
이번 포스팅에서 다룰 내용은 중앙에서 상태를 관리하는 Context API와 Redux에 대해서 다루고자 합니다.
Context API는 React에서 제공하는 내장 API입니다.
상태를 관리하는 context를 만들고 상태를 사용하는 범위에 Provider를 씌워줍니다. Provider에 내부에서는 context에 존재하는 상태에 자유롭게 접근이 가능하고 변경이 가능합니다.
useContext를 활용하여 상태를 가져올 수 있습니다. 위에서 언급했듯이 간단한 상태는 useState처럼, 복잡한 상태는 useReducer처럼 가져올 수 있습니다.
// dispatch를 통해 복잡한 상태를 관리 가능
export default function ThemeToggleButton() {
const { state, dispatch } = useContext(ThemeContext); // 상태 접근
return (
<button onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
현재 테마: {state.theme}
</button>
);
}
Context API를 사용하는 가장 큰 이유는 props drilling을 해결하기 위해서입니다.
props drilling프로젝트 규모가 커질수록 컴포넌트 계층이 깊어지면서,
props를 여러 컴포넌트를 거쳐 하위 컴포넌트로 전달해야 하는 상황이 발생합니다.이러한 방식은 불필요한 렌더링을 유발하고 코드의 유지보수를 어렵게 만듭니다.
리렌더링의 조건 중에 하나는
props의 변경입니다. 리렌더링을 원하지 않는 중간노드에서 어쩔 수 없이 리렌더링이 일어날 수 있고,useMemo또는React.memo로 최적화를 하더라도 코드의 가독성이 떨어질 수 있습니다.
이건 사람마다 다르고 프로젝트에 따라 다릅니다. 하지만, 저의 경우는 props drilling이 3단계 이상으로, 여러 경우 일어나면 사용을 고려합니다.
Context API는 완벽하지 않습니다. 그저 props drilling을 해결하기 위해 만들어졌을 뿐입니다. Context API는 여러 단점이 있습니다.
context 내부의 상태가 변경되면, 그 상태가 존재할 수 있는 Provider내부의 컴포넌트가 전부 리렌더링됩니다. props drilling의 단점 중에 하나인 불필요한 리렌더링이 다시 발생할 수 있는 것입니다. 그러면 최대한 불필요한 리렌더링이 일어나지 않게 context도 많이 만들어서 각각 관리하면 되지 않을까요?
context가 많아지고 이에 해당하는 Provider가 많아질 수록 관리가 어려워지고 가독성이 떨어집니다.
<AuthContext.Provider value={authState}>
<ThemeContext.Provider value={themeState}>
<CartContext.Provider value={cartState}>
<UserContext.Provider value={userState}>
<App />
</UserContext.Provider>
</CartContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
예시코드의 경우 4개의 context일 뿐인데 매우 복잡합니다.
위의 경우와 같이 Context API는 규모가 커지게되면 관리하기 불편하고 가독성이 매우 떨어집니다!
상태관리 라이브러리를 사용하게 되면 이러한 문제를 해결할 수 있습니다. 이번 포스팅에서는 상태관리 라이브러리 중 하나인 Redux에 대해 알아보겠습니다.
Redux나 zustand와 같은 상태관리 라이브러리는 Flux패턴을 기반으로 만들어졌습니다. Flux패턴은 MVC패턴의 양방향 데이터 흐름의 단점을 개선하기 위해서 만들어졌습니다. 그리고 이러한 구조는 React에 잘 맞았습니다.
양방향 데이터 흐름의 단점
- 규모가 커질수록 데이터 흐름이 복잡해짐
- 한 컴포넌트의 상태 변경이 예측하기 어려운 방식으로 다른 컴포넌트에 영향을 줄 수 있음
Flux는 단방향 데이터 흐름
Flux는 Action → Dispatcher → Store → View 순서로 동작하며, 데이터가 한 방향으로만 흐르게 됩니다.
action생성dispatcher가 action을 store로 전달store에서 상태를 변경view에서 리렌더링Redux는 Flux패턴을 기반으로 만들어진 상태관리 라이브러리입니다. 그러면 어떤 것이 변경되었는지 알아보겠습니다.
action객체를 생성dispatch가 action을 reducer로 전달Redux는 현재 상태(state)와 action을 reducer로 보냅니다.action객체는 type과 payload(변경할 데이터)를 포함합니다.reducer가 action을 보고 새로운 상태를 생성reducer는 순수함수로 작성되어야 합니다.reducer는 이전 상태와 action객체를 받아서 새로운 상태 객체 반환store가 새로운 상태를 업데이트reducer가 반환한 새로운 상태를 store에 저장view에서 리렌더링useSelector : store에서 상태를 가져오는 훅useDispatch : action을 발생시키는 훅Flux패턴의 경우는 여러개의 store로 구성이 되어있지만, Redux는 하나의 중앙 store를 사용합니다.
store의 상태는 불변성을 유지합니다. 이는 상태를 직접 수정하는 것이 불가능하며, 새로운 상태를 반환하는 방식으로 상태를 변경합니다.
Flux패턴은 dispatcher가 action을 store로 전달하지만, Redux의 경우는 dispatch함수 실행을 통해 Redux가 직접 action을 store로 전달합니다.
Flux패턴은 store에서 상태를 직접 변경시키지만, Redux의 경우는 상태를 읽기만 가능하고 reducer를 통해서 새로운 상태를 반환하여 업데이트합니다.
Redux를 사용할 때는 Action, Reducer, Dispatch, Store 등을 별도로 정의해야 하고, 상태 하나 추가할 때도 많은 파일을 작성해야 하며, 코드가 길어집니다.
상태를 변경시키지 않고 기존 상태와 action을 통해서 새로운 상태를 반환해야하므로 실수가 자주 일어납니다.
RTK를 사용하게 되면 Redux의 문제점을 해결할 수 있습니다.
createSlice를 사용하면 Action과 Reducer를 한 곳에서 관리할 수 있어서 보일러 플레이트 코드가 대폭 감소합니다.
createSlice에서는 Immer를 내장하고 있어 직접 상태를 변경해도 자동으로 불변성을 유지해 줍니다.
Immer불변성을 유지하도록 해주는 JavaScript 라이브러리입니다.
위와 같은 이유로 RTK를 사용하면 조금 더 편리하게 Redux의 기능을 사용할 수 있습니다.
React에서 로컬상태를 관리하는 방법 중 Context API와 Redux에 대해서 정리해보았습니다.