Dog foot print

redux-toolkit은 무엇을 해결하려고 했는가 ? 본문

REACT

redux-toolkit은 무엇을 해결하려고 했는가 ?

개 발자국 2023. 7. 24. 13:50

엄청난양의 보일러 플레이트 코드.

reduxflux-pattern을 이용한 전역 상태관리 라이브러리이다. [React, 라이브러리] react-redux . redux를 사용하기 위해서는 action , reducer, store 의 관리를 위해 반복적인 코드 작성이 필요하다. 새로운 액션을 추가할때 마다, 액션 타입을 정의하고, 액션 생성자 함수를 만들어야 하며, 리듀서에서 해당 액션을 처리해야한다.

아래는 간단한 카운터를 구현한 “리듀서”이다.

// Actions
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";
const UPDATE = "UPDATE";

const genAction = <T1 = undefined, T2 = T1>(action: string, f?: (arg?: T1) => T2) => (arg?: T1) => ({
    type: action,
    payload: f && f(arg)
})

// Action Constructor
export const increase = genAction(INCREASE);
export const decrease = genAction(DECREASE);
export const update = genAction<number>(UPDATE, (n?: number) => n || 0);

type Action = {
    type: typeof INCREASE | typeof DECREASE | typeof UPDATE
    payload?: number
}


const initialState = 1;
const reducer = (state = initialState, action: Action): number => {
    switch (action.type) {
        case INCREASE: return state + 1;
        case DECREASE: return state - 1;
        case UPDATE: return action.payload!;
        default: return state
    }
}

export default reducer

여기서 문제는 “action” 증가 할때마다 액션타입에 대한 reducer의 행동을 정의해야하며, 액션 생성자 함수를 만들어야 해야해서, 작성해야 하는 코드의 양이 매우 많아지는 것이다. 아래는 redux-toolkit으로 작성한 예제이다.

import { PayloadAction, createSlice } from "@reduxjs/toolkit";

const name = "COUNTER"
const initialState = 0
const counterSlice = createSlice({
    name,
    initialState,
    reducers: {
        increase: (s) => s + 1,
        decrease: (s) => s - 1,
        update: (_, action: PayloadAction<number>) => action.payload
    }
})

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

불변성 유지 로직

react를 다루면서 항상 말을 듣는 것이 불변성(Immutable)일 것이다. 이는 컴포넌트를 다시 렌더링 하기위해서 최신의 상태와 프롭스를 기존 상태와 프롭스에 대해 ===(Strict Equal) 를 이용해 비교하여, 값의 최신상태여부를 확인하는데 이용된다.

“redux”에서 객체나 배열의 구조가 변경될 때, Object.assign[...]전개 연산자를 사용하여, 아래와 같이 새로운 배열이나 객체를 반환해야 한다.

const ADD = "ADD";
const TOGGLE = "TOGGLE";

export type Todo = { id: string, title: string, todo: string, completed: boolean }

const genAction = <T1 = any, T2 = T1>(action: string, f?: (arg: T1) => T2) => (arg: T1) => ({
    type: action,
    payload: f && f(arg)
})

export const add = genAction(ADD, (t: Todo) => t)
export const toggle = genAction(TOGGLE, (id: string) => id);

type Action<T = any> = {
    type: typeof ADD | typeof TOGGLE
    payload?: T
}

const initialState: Todo[] = [];
const reducer = (state = initialState, action: Action<Todo>): Todo[] => {
    switch (action.type) {
        case ADD: return action.payload ? [...state, action.payload] : state;
        case TOGGLE: return action.payload ? state.map((v) => {
            const todo = state.find((todo) => todo.id === action.payload!.id);
            if (todo) todo.completed = !todo.completed;
            return v;
        }) : state;
        default: return state
    }
}

export default reducer

“redux”에 비해, “toolkit”은 내부적으로 Immer를 사용하기 때문에, 가변 스타일의 코드를 작성할 수 있으며, 불변성을 자동으로 유지하게 해준다.

import { PayloadAction, createSlice } from "@reduxjs/toolkit";

export interface Todo {
    title: string,
    id: string,
    completed: boolean,

}

const name = "TODOS"
const initialState: Todo[] = []
const counterSlice = createSlice({
    name,
    initialState,
    reducers: {
        add: (s, action: PayloadAction<Todo>) => {
            s.push(action.payload);
        },
        toggle: (s, action: PayloadAction<string>) => {
            const find = s.find(v => v.id === action.payload);
            if (find) find.completed = !find.completed
        }
    }
})

export default counterSlice;
export const { add, toggle } = counterSlice.actions;

redux-toolkit의 reducer 액션 함수들에서 가변 스타일 코드를 이용해, 중첩구조의 값을 직접 변경하는 것으로 상태를 새롭게 변경 할 수 있다. 만약 함수의 반환값이 존재한다면, 새로운 반환값으로 상태가 결정된다. 분명 값을 이렇게 변경하는 것은 매우 편리하지만, “react”를 사용하는 프로젝트에서 다른 스타일로 상태를 변경하는 것은 조금 위험해보인다.

초기 실행 설정

redux를 이용한 프로젝트에서는 보통 react-devtools를 사용하거나, redux-thunk와 같은 미들웨어를 추가해야하나, redux-toolkit에는 이를 위한 설정들이 미리되어 있다. 다음은 redux만을 사용했을때 흔히들 사용하는 설정이다.

import { combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { legacy_createStore as createStore, compose, applyMiddleware } from 'redux';

import todos from "./todos";
import counter from './counter';

declare global {
    interface Window {
        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
    }
}
const rootReducer = combineReducers({
    counter,
    todos
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
    rootReducer,
    composeEnhancers(applyMiddleware(thunkMiddleware))
)

export default store;
export type RootState = ReturnType<typeof rootReducer>;

매번 프로젝트를 진행해야 할때마다, 이러한 행동을 하는 것은 매우 귀찮다. 이를 위해, redux-toolkit에서는 별다른 설정없이도 위의 설정을 그대로 기본값으로 지정해두었다. 아래는 redux-toolkit의 아주 기본적인 설정예시이다.

import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";
import todosSlice from "./todos"
const store = configureStore({
    reducer: {
        counter: counterSlice.reducer,
        todos: todosSlice.reducer
    }
})

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch

export default store

configureStore의 옵션객체에는 devtoolsmiddleware 옵션이 있는데, 이를 활용해서 기본값과 함께 조합 할 수 있다.

반응형
Comments