처음 만난 리덕스 (Redux) 문서


14.3 Redux Toolkit 주요 API

지금부터는 Redux Toolkit의 주요 API들에 대해서 하나씩 배워보도록 하겠습니다.

14.3.1 configureStore()

먼저 configureStore() 함수입니다.

configureStore() 함수는 Redux의 createStore() 함수와 동일하게 Redux Store를 만들 때 사용하는 함수인데, 대신 조금 더 사용하기 편하게 만든 것이라고 보면 됩니다.

import { configureStore } from '@reduxjs/toolkit';

import rootReducer from './reducers';

// redux-thunk가 내장되어 있으며, redux-devtools가 자동으로 연동됨
const store = configureStore({ reducer: rootReducer });

이 코드는 configureStore() 함수를 사용하는 예시 코드입니다.

Redux Toolkit을 사용하게 되면 기본적으로 redux-thunk가 내장되어 있고 redux-devtools도 자동으로 연동됩니다.
그리고 createStore() 함수와 마찬가지로 추가적인 middlewareenhancer 등도 연동해서 사용할 수 있습니다.

14.3.2 createAction()

다음으로 나오는 Redux Toolkit의 API는 createAction() 함수입니다.

createAction() 함수는 이름 그대로 Action Type과 Action Creator를 만들어 주는 함수입니다.

import { createAction } from '@reduxjs/toolkit';

const increase = createAction('counter/INCREASE');

// 출력 결과: { type: 'counter/INCREASE' }
console.log(increase());

// 출력 결과: { type: 'counter/INCREASE', payload: 10 }
console.log(increase(10));

// 출력 결과: 'counter/INCREASE'
console.log(increase.toString());

// 출력 결과: 'Action Type: counter/INCREASE'
console.log(`Action Type: ${increase}`);

이 코드는 createAction() 함수를 사용하는 예시 코드입니다.

이렇게 createAction() 함수의 파라미터로 Action Type을 넣어서 호출하게 되면 Action Creator 함수가 나옵니다.
이렇게 만들어진 Action Creator를 호출하면 Action 객체가 생성되고, 파라미터를 넣어서 호출하면 payload가 포함된 Action 객체가 생성됩니다.

그리고 Action Creator의 toString() 함수를 호출하면 Action Type이 나오게 됩니다.
그래서 마지막 줄의 console.log()처럼 template literals 문법을 사용하면 toString()의 값인 Action Type이 출력됩니다.

이처럼 createAction() 함수를 사용하면 쉽게 Action Creator를 생성할 수 있습니다.

14.3.3 createReducer()

다음 Redux Toolkit의 API는 createReducer() 함수입니다.

createReducer() 함수는 이름 그대로 Reducer를 만들어 주는 함수입니다.

import { createAction, createReducer } from '@reduxjs/toolkit';

const increase = createAction('counter/INCREASE');
const decrease = createAction('counter/DECREASE');

const initialState = { count: 0 };

const counterReducer = createReducer(initialState, (builder) => {
    builder
        .addCase(increase, (state, action) => {
            state.count++;
        })
        .addCase(decrease, (state, action) => {
            state.count--;
        });
});

이 코드는 createReducer() 함수를 사용하는 예시 코드입니다.

먼저 앞에서 나왔던 createAction() 함수를 사용해서 두 개의 Action Creator를 만들었습니다.
이후 초기 State를 선언하고 createReducer() 함수를 사용해서 Reducer를 만들어주고 있습니다.

createReducer() 함수의 첫 번째 파라미터로는 initialState가 들어가고,
두 번째 파라미터로는 builderCallback 함수가 들어갑니다.

builderCallback 함수는 builder라는 것을 파라미터로 받게 되는데, 이 builderaddCase() 함수를 사용해서 각 Action에 대해 Reducer 함수를 추가할 수 있습니다.

여기에서는 카운트를 증가시키거나 감소시키는 두 개의 Reducer를 만드는 것을 볼 수 있습니다.
이처럼 createReducer() 함수는 Reducer를 쉽게 만들 수 있도록 해준다고 보면 됩니다.

그런데 여기서 뭔가 이상한 점이 보이지 않나요?

우리가 알던 Reducer에서는 Immutable Update를 위해서, 현재 state에 변화를 준 새로운 state를 만들어서 리턴을 했었습니다.
그런데 여기에 있는 Reducer들은 새로운 state를 만들어서 리턴하는 것이 아니라, 기존 state의 값을 변경하고 있습니다.
이 코드만 놓고 보면 immutable update가 아닌 것처럼 보입니다.

여기에는 우리가 잠깐 잊고 있었던 Redux Toolkit의 한 가지 특징이 있습니다.
바로 Redux Toolkit에서 Immutable Update를 위해서 내부적으로 immer.js를 사용한다는 사실입니다.

앞에서 Reducer에 대해서 배울 때, Immutable Update를 위해서 일일이 state를 새로 만들어줘야만 했습니다.
state의 계층이 복잡해질수록 새로운 state를 만드는 코드가 굉장히 양도 많고 지저분해졌었죠.

그래서 Redux Toolkit에서는 개발자가 일일이 새로운 state를 만들지 않고, 편리하게 Immutable Update를 할 수 있게 하기 위해서 immer를 사용하는 것입니다.

import produce from 'immer';

const baseState = [
    { todo: '처음 만난 리액트 공부', done: true },
    { todo: '처음 만난 리덕스 공부', done: false },
];

const nextState = produce(baseState, (draftState) => {
    // draftState 배열을 직접 수정(mutate)
    draftState.push({ todo: '운동하기', done: false });

    // draftState 배열의 아이템의 값을 직접 수정(mutate)
    draftState[1].done = true;
});

// 출력 결과: false (배열이 복사되었기 때문)
console.log(baseState === nextState);

// 출력 결과: true (첫 번째 아이템은 바뀌지 않았기 때문에 같은 reference 유지)
console.log(baseState[0] === nextState[0]);

// 출력 결과: false (두 번째 아이템은 복사된 이후 변경되었기 때문)
console.log(baseState[1] === nextState[1]);

이 코드는 immer를 사용한 Immutable Update를 나타낸 것입니다.

immer에는 produce() 라는 함수가 있는데, 이 함수는 기존 State와 callback 함수를 파라미터로 받아서 새로운 state를 생성해주는 함수입니다.

여기서 두 번째 파라미터인 callback 함수는 파라미터로 draftState라는 것을 받게 되는데, draftState는 이름의 의미 그대로 수정 중인 State를 의미합니다.
그래서 draftState는 맘껏 수정이 가능한데, immer에서는 이렇게 draftState를 수정하는 모든 과정을 현재 state에 동일하게 적용해서 새로운 state를 생성해줍니다.
결과적으로 Immutable Update가 되는 것이죠.

우리가 Redux Toolkit을 사용할 때는 이러한 내부적인 원리를 잘 알고 있는 것이 중요합니다.
그냥 Redux Toolkit의 예시 코드들에 state를 직접 수정하는 코드들이 나오니까, '어? 그냥 이렇게 직접 수정해도 되나보네?' 라고 생각하고 수정하면 안 된다는 것입니다.

Redux에서의 Immutability와 Immutable Update의 개념, 그리고 immer 내부의 작동방식까지 잘 이해하고 사용하는 것이 중요합니다.
Redux Toolkit에서 Immer를 사용하는 다양한 패턴들에 대해서는 뒤에서 더 자세하게 살펴보도록 하겠습니다.

14.3.4 createSlice()

다음으로 나오는 Redux Toolkit의 API는 createSlice() 함수입니다.

createSlice() 함수는 앞에서 Slice에 대해서 배울 때 이미 살펴본 것처럼, Redux 구성 요소들의 조각인 Slice를 만들어주는 함수입니다.

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
    name: 'counter',
    initialState: { count: 0 },
    reducers: {
        increase: (state, action) => {
            state.count++;
        },
        decrease: (state, action) => {
            state.count--;
        },
    },
});

export const { increase, decrease } = counterSlice.actions;

export default counterSlice.reducer;

이 코드는 앞에서 잠깐 살펴봤던 createSlice() 함수를 사용하는 예시 코드입니다.

createSlice() 함수의 파라미터로 slice의 이름을 나타내는 name, 초기 State를 나타내는 initialState, 그리고 각 Action에 대한 reducers까지 넣어준 것을 볼 수 있습니다.

그리고 여기에 작성된 Reducer의 함수들 역시 immer에서 draftState를 수정하는 방식과 동일하게, state를 직접 수정하는 형태로 Immutable Update를 하게 됩니다.

또한 createSlice() 함수의 호출 결과로 생성된 slice에는 actionsreducer가 포함되어 있는데, 이를 export 해줌으로써 외부에서 가져다 쓸 수 있게 해준 것을 볼 수 있습니다.

14.3.5 createAsyncThunk()

다음으로 나오는 Redux Toolkit의 API는 createAsyncThunk() 함수입니다.

createAsyncThunk() 함수는 이름 그대로 비동기 로직을 위한 Thunk를 만들어 주는 함수입니다.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import apiClient from '../api/client';

export const fetchPostById = createAsyncThunk(
    'post/fetchPostById',
    async (postId, thunkAPI) => {
        const response = await apiClient.fetchPostById(postId);
        return response.data;
    }
);

const initialState = {
    pending: false,
    data: null,
    error: null,
};

const postSlice = createSlice({
    name: 'post',
    initialState,
    reducers: {
        ...
    },
    extraReducers: (builder) => {
        builder
            .addCase(fetchPostById.pending, (state, action) => {
                state.pending = true;
                state.data = null;
                state.error = null;
            })
            .addCase(fetchPostById.fulfilled, (state, action) => {
                state.pending = false;
                state.data = action.payload;
            })
            .addCase(fetchPostById.rejected, (state, action) => {
                state.pending = false;
                state.error = action.error;
            });
    },
});

export default postSlice;
// fetchPostById() Thunk 호출
dispatch(fetchPostById(1));

위 코드는 createAsyncThunk() 함수를 사용하는 예시 코드입니다.

먼저 위쪽에서 createAsyncThunk() 함수를 사용해서 비동기 요청을 수행하는 Thunk를 생성합니다.
이 때 첫 번째 파라미터로는 Action Type이 들어가고, 두 번째 파라미터로는 payloadCreator() 함수가 들어갑니다.

payloadCreator() 함수는 Thunk Action Creator로 전달할 값과 thunkAPI를 파라미터로 받게 됩니다.
여기서는 postId를 값으로 전달한 것을 볼 수 있습니다.
그리고 thunkAPIdispatch(), getState() 함수 등을 사용할 수 있도록 해주는 객체라고 보면 됩니다.

Action Type은 파라미터로 넣은 타입 뒤에 pending, fulfilled, rejected, 이렇게 총 세 가지의 문자열이 붙은 Action Type들로 만들어집니다.

그리고 payloadCreator() 함수에서는 이렇게 비동기 요청을 처리한 이후에 결과값을 리턴하거나, Promise를 리턴하면 됩니다.

이렇게 생성된 Thunk는 createSlice() 함수에 들어가는 객체의 extraReducers 속성을 통해 Thunk의 각 Action Type에 대한 Reducer를 만들 수 있습니다.

이 코드가 Redux Toolkit에서 Thunk를 사용하는 가장 단순한 형태라고 보시면 됩니다.

그리고 앞에서 말한 것처럼 createAsyncThunk() 함수를 호출하면 총 3개의 Action Type이 생성되는데, 바로 아래에 나와 있는 세 가지 입니다.

  • pending
    • 비동기 요청 진행 중
    • ex) "post/fetchPostById/pending"
  • fulfilled
    • 비동기 요청 성공
    • ex) "post/fetchPostById/fulfilled"
  • rejected
    • 비동기 요청 실패
    • ex) "post/fetchPostById/rejected"

먼저 pending은 비동기 요청이 진행 중인 것을 나타내고, fulfilled는 비동기 요청이 성공해서 데이터를 받아온 것을 나타냅니다.
그리고 마지막으로 rejected는 비동기 요청이 실패한 경우를 나타냅니다.

이렇게 세 가지 기본 Action Type을 통해 비동기 요청의 진행 여부 및 성공, 실패 여부를 나눌 수 있고, 각 Action을 처리하는 Reducer에서 알맞게 State를 업데이트 하면 됩니다.

여기에서는 createAsyncThunk()에 대한 가장 단순한 형태의 사용법만 소개했습니다.
더 자세한 내용이 궁금한 분들은 아래 링크를 참고하기 바랍니다.

https://redux-toolkit.js.org/api/createAsyncThunk

14.3.6 current()

마지막으로 나오는 Redux Toolkit의 API는 current() 함수입니다.

current() 함수는 immer에서 제공하는 함수인데, Reducer 내에서 state를 변환하는 과정 중에 draftState의 현재 state 값을 가져올 때 사용하는 함수입니다.

import { createSlice, current } from '@reduxjs/toolkit';

const counterSlice = createSlice({
    name: 'counter',
    initialState: { count: 0 },
    reducers: {
        increase: (state, action) => {
            state.count++;
            console.log(`현재 카운트: ${current(state.count)}`);
        },
        decrease: (state, action) => {
            state.count--;
            console.log(`현재 카운트: ${current(state.count)}`);
        },
    },
});

export const { increase, decrease } = counterSlice.actions;

export default counterSlice.reducer;

위 코드는 current() 함수를 사용하는 예시 코드입니다.

Redux Toolkit에서는 state의 업데이를 위해 내부적으로 immer를 사용하기 때문에, Reducer 내에서 state의 값을 console에 출력해보면 실제 값이 아닌 proxy instance가 나오게 됩니다.

그래서 해당 시점의 state값을 가져오기 위해서 이렇게 current() 함수를 사용하는 것입니다.
current() 함수는 Reducer를 디버깅 할 때 자주 사용하므로 꼭 기억해두시기 바랍니다.


마지막 업데이트: 2023년 07월 14일 00시 00분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.