redux
는 상태관리를 도와주는 도구로, 다수의 컴포넌트 또는 App 전체의 상태를 다룰 수 있도록 도와준다. 그런데 이런 점에서 보면 context
로도 충분하지 않나라는 생각이 들 수도 있다. 그렇다면 context
가 있는데 redux
를 사용하는 이유는 무엇일까?
Context
를 구분해서 사용해야 하고 이는 Provider
도 중첩해야 한다는 것을 말한다.Context
하나만을 사용한다고 한다면 Context
안에 너무나 많은 내용이 담길수도 있다.Context
는 빈도가 낮은 변경이 있을땐 좋지만 상태가 빈번하게 변경되는 상태에는 성능상 좋지 않다.실제로 얼마전 했던 프로젝트에서 겪었던 문제들이었다. 각기 다른 인원수, 캘린더, 가격이라는 상태를 전역적으로 공유해야했고 이를 위해 context
를 사용하고자 했다. 그런데 이를 효율적으로 사용하기 위해 - 컨텍스트 사용으로 인한 리렌더링 문제를 해결하기 위해 - context
를 세개로 나누어야 했다. 근데 세개를 나누는 것에서 그치지 않았다. 컨텍스트의 리렌더링 문제를 약간이나마 해소하기 위해, 내부적으로는 단순히 상태만을 전달하는 context
와 이를 dispatch
하기 위한 컨텍스트로도 나누었기 때문이다. 결과적으로 이를 위해서 6가지 context
를 만들어야 했다.
이 프로젝트는 아주 간단하고 작은 프로젝트였는데도 6개의 context
를 만들어야 했는데 만약 대규모 프로젝트라면 상상할 수 없는 수의 context
를 만들어야 했을 것이다. 또한 위처럼 만들었음에도 컨텍스트 사용으로 인한 리렌더링 문제를 완벽히 해소할 수 없었다. 그리고 provider
가 세개만 되었을 뿐인데 약간 정신없었다.
리덕스는 이러한 context
의 문제를 어느정도 해소시켜 준다.
리덕스는 중앙에 존재하는 단 하나의 store
를 사용한다.
store
를 구독하고 있다.reducer FN
이 담당한다.reducer FN
을 실행한다.reducer FN
은 store
의 상태를 변경한다.component
가 받아서 다시 렌더링한다.// counter.js
// 1. 초기 상태 정의하기
const initialState = { counter: 0 };
// 2. 액션 타입 정의하기
// 액션타입은 관례상 대문자로 작성한다.
// 이렇게 액션타입을 미리 정의해두는 방법을 통해서 ide의 자동완성을 이용하고
// 오타가 나는 등의 오류를 방지할 수 있다.
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 3. 액션 생성 함수 정의하기
export const increase = () => {
return {
type: INCREASE,
};
};
export const decrease = () => {
return {
type: DECREASE,
};
};
const counterReducer = (state = initialState, action) => {
if (action.type === INCREASE) {
return {
counter: state.counter + 1,
};
}
if (action.type === DECREASE) {
return {
counter: state.counter - 1,
};
}
return state;
};
export default counterReducer;
// 리덕스의 상태는 읽기 전용이므로 상태 그 자체를 변경하면 안된다.
// 리듀서에서는 꼭 상태의 불변성을 지켜주어야 한다.
// 이렇게 3가지를 다 만들면 루트리듀서로 export한다.
// store.js
import { createStore } from 'redux';
import counterReducer from './counter';
// 지금은 리듀서가 하나인 상태여서 루트리듀서를 만들 필요가 없다.
// 하지만 리듀서가 두개 이상이라면 combineReducers를 사용해서 리듀서들을 합칠 수 있다.
// const rootReducer = combineReducers({
// counterReducer,
// ...다른 리듀서
// })
const store = createStore(counterReducer);
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './store/store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
// 이런 방식으로 만들어진 store를 적용한다.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease } from './store/counter';
// 이 increase, decrease는 counter에서 만들었던 액션생성함수이다.
// export const increase = () => {
// return {
// type: INCREASE,
// };
// };
// 이런식으로 만들어져 있다.
function App() {
const number = useSelector((state) => state.counter);
// useSelector을 통해서 store에 저장해둔 상태를 불러올 수 있다.
const dispatch = useDispatch();
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
// 이렇게 만들어둔 액션생성함수를 사용한다.
return (
<div className="App">
<button onClick={onIncrease}>plus</button>
<button onClick={onDecrease}>minus</button>
<div>changed by redux</div>
<div>{number}</div>
</div>
);
}
export default App;
사실 리덕스를 공부는 진짜 조금 했었고 리덕스 toolkit을 공부했다. 그래서 구현을 할때에도 거의 toolkit을 이용해서 구현했었다. 그래서 액션생성자함수라던지 toolkit에서 자동으로 만들어준다는 액션객체라던지를 이해할 수 없었는데 조금 이해할 수 있었다.
그리고 리덕스의 코드가 길다라는 부분을 알 수 있었다.
또한, 리덕스의 리듀서들은 모두 순수함수들이여야 하므로 값이 바뀌는게 당연한 비동기 fetch로직등에 미들웨어를 사용해야 한다던지 또는 다른 라이브러리를 사용해야 한다던지를 이해할 수 있었다.