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에 대해서 정리해보았습니다.