Redux-Toolkit Usage Guide1

윤석주·2022년 10월 8일
2

Redux

목록 보기
3/4

Redux core library는 의도적으로 정해진 룰이 없습니다. 이는 store setup, reducer 디자인 등 모든 것들을 사용자가 원하는대로 사용할 수 있다는 것입니다.

이러한 특징은 사용자에게 유연함을 가져다주고 코드를 원하는대로 작성하도록 도와줍니다. 하지만 동시에 경험이 적거나 유연함이 필요없는 상황에선 단점으로 작용할 수 있죠. 큰 어플리케이션을 만들게 된다면 그저 형식적인 default 코드가 필요한 경우도 많이 존재합니다.

Redux toolkit은 redux의 usecase들을 쉽게 다루기 위해 탄생했죠. 이를 위해 몇 가지 툴킷 사용법을 살펴보며 어떻게 redux-toolkit을 잘 사용할 수 있을지 알아보겠습니다.

Store Setup

모든 리덕스 어플리케이션은 configure와 Redux store생성이 필요합니다. 이는 일반적으로 몇 가지 스탭을 포함하고 있습니다.

  • Importing or creating the root reducer function
  • Setting up middleware, likely including at least one middleware to handle asynchronous logic
  • Configuring the Redux DevTools Extension
  • Possibly altering some of the logic based on whether the application is being built for development or production

Manual Store Setup

Configuring Your Store의 예시는 전형적인 store setup 과정을 보여줍니다.

import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
  const middlewares = [loggerMiddleware, thunkMiddleware]
  const middlewareEnhancer = applyMiddleware(...middlewares)

  const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
  const composedEnhancers = composeWithDevTools(...enhancers)

  const store = createStore(rootReducer, preloadedState, composedEnhancers)

  if (process.env.NODE_ENV !== 'production' && module.hot) {
    module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
  }

  return store
}

읽기 쉬운 예제이지만, 모든 단계가 직관적으로 이해되지는 않을 수 있습니다.

  • The basic Redux createStore function takes positional arguments: (rootReducer, preloadedState, enhancer). Sometimes it's easy to forget which parameter is which
  • The process of setting up middleware and enhancers can be confusing, especially if you're trying to add several pieces of configuration.
  • The Redux DevTools Extension docs initially suggest using some hand-written code that checks the global namespace to see if the extension is available. Many users copy and paste those snippets, which make the setup code harder to read

Simplifying Store Setup with configureStore

configureStore는 아래와 같은 이슈들의 해결을 도와줍니다.

  • Having an options object with "named" parameters, which can be easier to read
  • Letting you provide arrays of middleware and enhancers you want to add to the store, and calling applyMiddleware and compose for you automatically

configureStore는 각각 특정한 목적이 있는 몇 가지 미들웨어를 디폴트로 더해줍니다.

  • redux-thunk is the most commonly used middleware for working with both synchronous and async logic outside of components
  • In development, middleware that check for common mistakes like mutating the state or using non-serializable values.

이는 store setup code 그 자체는 짧고 쉬우며 좋은 default behavior를 제공해준다는 것을 의미합니다.

이를 사용하는 가장 쉬운 방법은 단순하게 root reducer 함수를 reducer파라미터로 넘겨주는 것입니다.

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
  reducer: rootReducer,
})

export default store

또한 slice reducers로 구성된 객체도 넘길 수 있으며 configureStore는 내부적으로 combineReducers를 호출해줍니다.

import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'

const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
  },
})

export default store

이런 코드는 one level of reducers에만 동작하는 것을 기억합시다. 만약 그 이상의 중첩된 reducers를 원한다면, 중첩을 처리하기 위해 combineReducers를 호출해야 합니다.

만약 store setup을 커스터마이징 하고 싶다면, 추가적인 옵션을 전달할 수 있습니다. 아래는 hot reloading 사용에 대한 예시입니다.

import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureAppStore(preloadedState) {
  const store = configureStore({
    reducer: rootReducer,
    middleware: (getDefaultMiddleware) => 
      getDefaultMiddleware().concat(loggerMiddleware),
    preloadedState,
    enhancers: [monitoreReducersEnhancer],
  })
  
  if (process.env.NODE_ENV !== 'production' && module.hot) {
    module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
  }
  
  return store
}

middleware 아규먼트를 제공하는 경우, configureStore는 사용자가 제공하는 리스트에 들어있는 미들웨어만을 사용합니다. 만약 커스텀 미들웨어들과 default를 같이 사용하고 싶다면, callback notation을 사용해 getDefaultMiddleware를 호출하고 리턴하는 middleware array에 포함시키며 됩니다.

Writing Reducers

Reducers는 Redux 개념중 가장 중요한 부분입니다. 전형적인 Reducer 함수는 다음과 같아야 합니다.

  • Look at the type field of the action object to see how it should respond
  • Update its state immutably, by making copies of the parts of the state that need to change and only modifying those copies

reducer에서 어떤 conditional logic이던 사용할 수 있지만, 가장 보편적인 접근은 switch 문을 사용하는 것입니다. 왜냐하면 multiple possible values for a single field를 다루는데 직관적인 방법이기 때문입니다. 한편, 많은 사람들이 switch statements를 좋아하지 않습니다. Redux docs에서는 writing a function that acts as a lookup table based on action types에 대한 예시를 보여주었지만, 함수를 커스터마이징 하는 것은 유저들의 몫으로 남겨두었습니다.

리듀서를 작성하며 마주하는 또 다른 대표적인 고통 포인트(...)는 state update를 반드시 immutable하게 해야 한다는 것입니다(updating state immutably). 자바스크립트는 mutable language이고, updating nested immutable data by hand는 어려운 작업입니다. 또한 실수를 만들기 너무 쉽습니다.

Simplifying Reducers with createReducer

"lookup table" 접근이 유명하기에, 리덕스 툴킷은 Redux docs에서 본 방식과 비슷한 createReducer 함수를 포함하고 있습니다. 또한 createReducer 유틸리티는 사용성을 더 좋게 만드는 특별한 "마법"을 가지고 있습니다. 내부적으로 immer 라이브러리를 사용하여 사용자가 코드에서 몇몇 데이터를 "mutates"하더라도 실제로 updates를 immutable하게 적용해줍니다. 이러한 방식은 아주 효과적으로 "reducer 내부에서 실수로 상태를 mutate하는 것"을 불가능하게 만들었습니다.

일반적으로 switch 문을 사용하는 모든 Redux reducer는 createReducer로 즉각적인 대체가 가능합니다. switch 내의 각각의 case들은 createReducer에 전달되는 object의 key가 됩니다. spreading objects or copying arrays와 같은 immutable update logic은 직접적인 "mutation"으로 대체 가능합니다. 물론 immutable updates를 이전 그대로 두어도 상관 없습니다.

createReducer를 어떻게 사용할 수 있는지 보여주는 몇 가지 예시입니다. switch문과 immutable updates를 사용하는 전형적인 "todo list" reducer부터 시작해보겠습니다.

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO': {
      return state.concat(action.payload)
    }
    case 'TOGGLE_TODO': {
      const { index } = action.payload
      return state.map((todo, i) => {
        if (i !== index) return todo
        return {
          ...todo,
          completed: !todo.completed,
        }
      })
    }
    case 'REMOVE_TODO': {
      return state.filter((todo, i) => i !== action.payload.index)
    }
    default:
      return state
  }
}

copied array를 새로운 todo entry로 리턴하기 위해 state.concat()을 사용하는 부분이나, copied array for toggle case를 리턴하기 위해 state.map()을 사용하는 것, update가 필요한 todo를 copy하기 위해 object spread operator를 사용하는 부분들을 눈치챘을 것입니다. 상태의 Immutable을 유지하기 위해서이죠.

createReducer를 이용하면 우리는 위의 예시를 아래와 같이 짧게 줄일 수 있습니다.

const todosReducer = createReducer([], (builder) => {
  builder
    .addCase('ADD_TODO', (state, action) => {
      // "mutate" the array by calling push()
      state.push(action.payload)
    })
    .addCase('TOGGLE_TODO', (state, action) => {
      const todo = state[action.payload.index]
      // "mutate" the object by overwriting a field
      todo.completed = !todo.completed
    })
    .addCase('REMOVE_TODO', (state, action) => {
      // Can still return an immutably-updated value if we want to
      return state.filter((todo, i) => i !== action.payload.index)
    })
})

상태를 "mutate" 할 수 있는 능력은 깊게 중첩된 상태를 업데이트 하려고 할 때 특히 효과적입니다. 아래의 복잡하고 고통스러운 코드를 살펴봅시다.

case "UPDATE_VALUE":
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }

이런 코드가 아래와 같이 단순하게 표시가 됩니다

updateValue(state, action) {
  const {someId, someValue} = action.payload;
  state.first.second[someId].fourth = someValue;
}

코드가 훨씬 좋아진 것을 볼 수 있습니다.

Considerations for Using createReducer

리덕스 툴킷의 createReducer 함수는 매우 유용합니다. 사용시 아래와 같은 내용을 기억해야 합니다.

  • The "mutative" code only works correctly inside of our createReducer function
  • Immer won't let you mix "mutating" the draft state and also returning a new state value

더 자세한 부분은 createReducer API reference을 참고할 수 있습니다.

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글