이전 포스트의 카운터 를 Redux 로 바꿔보겠다.
리액트에서는 redux-toolkit 을 권장하므로 아래의 명령어들로 먼저 설치한다.
npm install @reduxjs/toolkit react-redux
이전 포스트에서 Redux 를 설명할 때 아래의 문구가 있었다.
Redux는 전체 애플리케이션에서 사용해야 하는 상태의 중앙 저장소 역할을 하며, 상태가 예측 가능한 방식으로만 업데이트되도록 하는 규칙을 제공합니다.
여기서 중앙 저장소 역할을 한다고 했는데, 말 그대로 중앙 저장소를 만들기 위해
새로운 폴더를 하나 만든다.

이전 포스트에서 Redux 를 사용하기전에 왜 useReducer 를 먼저 공부했냐면 Redux 를 이해하는데 굉장히 도움이 되기때문이다.
useReducer 를 사용할 때 state 사용설명서인 Reducer 를 정의했다.
여기서도 똑같이 Reducer 를 정의한다!
그래서 Redux-toolkit 패키지의 createReducer 를 사용하면 되지만,
더 강력한 기능을 가진 createSlice 를 대신 사용한다.
아래는 createSlice 로 reducer 를 정의한 store/index.ts 전체 코드이다.

그리고 아래는 useReducer 로 만들었던 카운터 이다.

그리고 우선 다른 코드를 볼 필요 없이 createSlice 만 보자.
const initialState = 0;
const countSlice = createSlice({
name : 'count', // 슬라이스 이름
initialState : initialState,
reducers : { // 여기에 reducer
PLUS(state) { // PLUS 함수가 나중에 Redux 에 의해 호출되며, state 라는 매개변수를 받는다.
state++; // 이렇게 작성하면 안된다며;
},
MORE_PLUS(state, action) {
state = state + action.payload;
},
MINUS(state) {
state--;
},
INIT(state ) {
state = 0
},
}
})
useReducer 때와 마찬가지로 initialState 를 먼저 만들고, 아래에 reducer 를 정의한다.
그리고 if 문이나 switch 문 없이 클래스의 매서드를 만드는 것처럼 reducer 들을 정의한다.
이렇게 만들면 우리가 PLUS 를 원할 때 혹은 MINUS 를 원할 때 원하는 함수만 호출해주면 된다.
즉, 아래처럼 dispatch 를 호출한다.
dispatch( { type : "PLUS" } ) ❌
dispatch( counterActions.PLUS() ) ⭕
아직 counterActions 가 뭔지는 모르지만 더 이상 어떤 액션인지 전달할 필요가 없다.
그렇기에 PLUS reducer 에도 action 이라는 매개변수가 없이 오로지 state 만 전달된다.
MORE_PLUS(state, action) {
state = state + action.payload;
},
물론 위와 같이 action 이 전달되는 reducer 도 있지만 이것은 payload 를 사용할 때 이다.
그럼 payload 는 어떻게 전달할까?
dispatch( { type : "PLUS", payload : 5 } ) ❌
dispatch( counterActions.PLUS(5) ) ⭕
그냥 PLUS(5) 라고 매개변수에 전달해주면 redux 에서 자체적으로 { type : "PLUS", payload : 5 } 과 같은 action 객체를 만들어서 사용한다.
PLUS(state) {
state++;
},
분명 useReducer 를 사용할 때 state++ 와 같이 상태를 직접 변경하는 것은 금기였다.
하지만
redux-toolkit은 내부에서immer라는 또 다른 패키지를 사용하여 해당 코드처럼 상태가 변경될 것 같은 코드가 발견되면 자동으로 원래 상태를 복제하여 새로운 상태 객체를 만들어 해당 코드를 실행하여,
상태의 불변성을 유지해주기 때문에 저렇게 사용해도 큰 문제가 없다.
여러모로 개발자 사용친화적인 패키지이다.
다른 코드를 보기 전에 갑자기 먼저 실행하게 됐는데, 만약 위 처럼 코드를 작성하여 실행하면 에러가 발생한다.

처음 연습하는 사람들은 볼 수 있을 에러인데,
이 에러의 원인은 아래의 코드이다.
const initialState = 0;
이것은 바로 위에서 공부한 Immer 패키지가 원인인데
Immer 패키지는 state 참조 값의 내부적인 변화를 감지하여 변화가 발생하지 않도록 유지해주는데
위의 initialState 는 원시 타입으로 만약 state가 0에서 1로 바뀌면 참조값의 내부가 변화하는게 아니라 참조값 자체가 변한다.
그렇기에 참조값 내부를 추적할 수 없어서 해당 에러가 발생한다.
그래서 아래와 같이 코드를 바꿔준다. 바뀌는 부분을 붉은 박스로 표시했다.

Redux 는 컨텍스트와 달리 중앙 저장소 역할을 한다는 것을 알고 있다.
즉, 중앙 저장소에서 여러가지 state들과 reducer 를 관리한다는 것인데,
slice 에 state와 reducer 의 정보가 보관되어 있다.
결국 이 slice 를 중앙 저장소에 등록해야 우리는 중앙 저장소에 원하는 reducer 와 state 를 호출해서 사용할 수 있다.
이것을 configureStore 가 해준다.
const store = configureStore({
reducer : countSlice.reducer
});
export type CountStateType = ReturnType<typeof store.getState>;
export const countActions = countSlice.actions;
export default store;
위와 같이 리듀서를 등록해주고 export default 로 store를 내보내준다.
그리고 아래와 같이 Context 를 사용한다.

여러 중첩된 컨텍스트들과 달리 하나의 컨텍스트로 상태를 관리할 수 있게된다.
useReducer 를 사용할때에는 아래와 같이 어떤 action 들이 있는지 interface 을 같이 정의해줬기 때문에 같은 파일 내에서 쉽게 사용할 수 있었다.

물론 다른 파일에 아래와 같이 작성해서 내보내도 쉽게 사용할 수 있다.
export const ACTION_TYPES = {
PLUS : "PLUS",
MORE_PLUS : "MORE_PLUS",
MINUS : "MINUS",
INIT : "INIT"
}
그럼 Redux 에서는 어떻게 action 을 내보낼까?
const store = configureStore({
reducer : countSlice.reducer
});
export type CountStateType = ReturnType<typeof store.getState>;
export const countActions = countSlice.actions;
export default store;
위의 코드 중 export const countActions = countSlice.actions; 이 코드가 action 을 담당하고 우리는 바깥에서도 이 action 을 받아와서 사용할 수 있게 된다.
아래처럼 사용할 수 있다. 아래는 Redux 를 사용한 counter 전체 코드이다.

위처럼 countActions 를 가져와서 action 을 사용할 수 있다.
마지막으로 const [state, dispatch] = useReducer(...) 에서 state와 dispatch 만 가져오면 된다.
이에 대한 답은 바로 위에 있는 카운터 코드 에 있다.

타입스크립트를 사용하지 않으면 CountStateType 을 작성할 필요가 없다.
어쨌든 useDispatch 와 useSelector 를 통해 dispatch 와 state 를 가져온다.