본문 바로가기
개발/React

[React] Redux-Toolkit 대표적인 API 알아보기

by 그레이웅 2022. 10. 4. 23:55
반응형

Redux-Toolkit 대표 API 정리

 

1. configureStore() 

리덕스 코어 라이브러리인 createStore()를 추상화한 함수.

store를 구성하는 함수이다. 

기본 리덕스 미들웨어인 redux-thunk를 내장하고, redux Devtools를 활성화해준다.

 

//기본 사용 예제
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from '../todo/tdooSlice';

export const store = configureStore({
  reducer: {
    todos: todoReducer,
  },
});

 

 

configureStore 전체적인 구성

import logger from 'redux-logger'
import { reduxBatch } from '@manaflair/redux-batch'

import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'

const rootReducer = {
  todos: todosReducer,
  visibility: visibilityReducer,
}

const preloadedState = {
  todos: [
    {
      text: 'Eat food',
      completed: true,
    },
    {
      text: 'Exercise',
      completed: false,
    },
  ],
  visibilityFilter: 'SHOW_COMPLETED',
}

const store = configureStore({
  reducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
  devTools: process.env.NODE_ENV !== 'production',
  preloadedState,
  enhancers: [reduxBatch],
})

configureStore 함수는 reducer, middleware, devtools, preloadedState, enhancer 정보를 전달한다.

 

- reducer : 리듀서에는 단일 함수를 전달해 스토어의 루트 리듀서(root reducer)로 바로 사용할 수 있다. 슬라이스 리듀서로 구성된 객체를 전달해 루트 리듀서를 생성할 수도 있다. 이런 경우 내부적으로 combineReducers 함수를 사용해 자동으로 병합하여 루트 리듀서를 생성해줌

- middleware : 기본적으로는 리덕스 미들웨어를 담는 배열, 사용할 미들웨어를 명시적으로 배열에 담아서도 작성 가능.

기본은 getDefaultMiddleware를 호출함

- devTools : 불리언 값으로 개발자 도구를 끄거나 켬

- preloadedState : 스토어의 초기값 설정

- enhancers : 기본은 배열, 콜백으로 정의하기도 함. 아래와 같이 작성하면, 개발자가 원하는 store enhancer를 미들웨어가 적용되는 순서보다 앞서서 추가 가능

 

const store = configureStore({
  ...
  enhancers: (defaultEnhancers) => [reduxBatch, ...defaultEnhancers],
})

// [reduxBatch, applyMiddleware, devToolsExtension]

 

 


2. createReducer()

상태에 변화를 일으키는 리듀서 함수를 생성하는 유틸 함수.

내부적으로 immer 라이브러리를 사용한다.

state.todo[3].completed = true 의 형태로 작성하여도 불변(immutable) 업데이트가 이루어지도록 로직을 간단히 할 수 있다.

 

아래는 사용 예시이다.

// 기존 스위치 문으로 이루어진 카운터 리듀서 함수입니다.
// 많은 보일러플레이트 코드와 에러를 발생시키기 쉬운 구조를 보여주고 있습니다.

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'UPDATE_VALUE': {
      return {
        ...state,
        first: {
          ...state.first,
          second: {
            ...state.first.second,
            [action.someId]: {
              ...state.first.second[action.someId],
              fourth: action.someValue,
            },
          },
        },
      };
    }
    default: {
      return state;
    }
  }
}

// 하지만 createReducer 함수를 사용하면 아래처럼 간단히 작성할 수 있습니다.

const todosReducer = createReducer(state = [], (builder) => {
  builder.addCase('UPDATE_VALUE', (state, action) => {
    const {someId, someValue} = action.payload;

    state.first.second[someId].fourth = someValue;
  })
})

 

RTK에서 case reducer 가 액션을 처리하는 방식은 두 가지가 있다.

builder callback 표기법과 map object 표기법이다.

타입 스크립트 환경에서는 builder callback 표기법이 더 선호된다.

Builder Callback 표기법

builder callback 표기법에서는 createReducer의 콜백 인자 builder 객체 안에는 addCase, addMatcher, addDefaultCase라는 메서드가 존재한다.

// 각 라인마다 빌더 메서드를 나누어 호출합니다.
const counterReducer = createReducer(initialState, (builder) => {
  builder.addCase(increment, (state) => {})
  builder.addCase(decrement, (state) => {})
})

// 또는 메서드 호출을 연결하여 연속적으로 작성합니다.
const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state) => {})
    .addCase(decrement, (state) => {})
})

주요 메서드는 다음과 같다.

 

+ createReducer(initalState, bullderCallback)

 

- builder.addCase : 액션 타입과 맵핑되는 케이스 리듀서를 추가하여 액션을 처리한다. addMatcher 또는 addDefaultCase 메서드 보다 먼저 작성되어야 한다.

- builder.addMatcher(matcher, reducer) : 새로 들어오는 모든 액션에 대해 주어진 패턴과 일치하는지 확인하고 실행

- builder.addDefaultCase : 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았을 경우 defaultCase 가 실행

 

예시는 아래와 같다.

 

const increment = createAction('increment')
const decrement = createAction('decrement')

function isActionWithNumberPayload(
  action: AnyAction
): action is PayloadAction {
  return typeof action.payload === 'number'
}

const initialState = {
  counter: 0,
  sumOfNumberPayloads: 0,
  unhandledActions: 0,
};

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.counter += action.payload
    })
    .addCase(decrement, (state, action) => {
      state.counter -= action.payload
    })
    .addMatcher(isActionWithNumberPayload, (state, action) => {})
    .addDefaultCase((state, action) => {})
})

 

Map Object 표기법

액션 타입 문자열을 '키'로 사용하는 객체를 받아 케이스 리듀서에 맵핑함

builder callback 표기법보다는 짧다는 장점이 있지만 Javascript 프로젝트에 유효한 방법.

TS는 builder callback 권장

 

const counterReducer = createReducer(0, {
  increment: (state, action) => state + action.payload,
  decrement: (state, action) => state - action.payload
})

// 위 예제처럼 작성하거나 
// 또는 'createAction'에서 생성된 액션 생성자(action creator)를
// 연산된 프로퍼티(computed property) 문법을 사용해서 바로 '키'로 사용할 수 있습니다.

const increment = createAction('increment')
const decrement = createAction('decrement')

const counterReducer = createReducer(0, {
  [increment]: (state, action) => state + action.payload,
  [decrement.type]: (state, action) => state - action.payload
})

 

+ createReducer(initialState, actionsMap, actionMatchers, defaultCaseReducer)

- initalState : 리듀서가 최초로 호출될 때 사용될 값

- actionsMap : 액션 타입이 케이스 리듀서에 맵핑되어 있는 객체

- actionMatchers : {matcher, reducer } 형태로 정의된 매처를 배열로 담음. 매칭 된 리듀서는 순서대로 독립적으로 실행

- defaultCaseReducer : 그 어떤 케이스 리듀서나 매처 리듀서도 실행되지 않았다면, 기본 케이스 리듀서가 실행

 

// matcher
const isStringPayloadAction = (action) => typeof action.payload === 'string'

const lengthOfAllStringsReducer = createReducer(
  // initialState
  { strLen: 0, nonStringActions: 0 }, 
  // actionsMap
  { 
    /* [...]: (state, action) => {} */
  },
  // actionMatchers
  [
    {
      matcher: isStringPayloadAction,
      reducer(state, action) {
        state.strLen += action.payload.length
      },
    },
  ],
  // defaultCaseReducer
  (state) => {
    state.nonStringActions++
  }

 


 

3. createAction()

기존 리덕스 형태에서는 액션 타입 상수와 액션 생성자 함수를 분리하여 선언하였다.

RTK에서는 이 두 과정을 createAction함수로 하나로 결합하여 사용할 수 있다.

 

기존 리덕스

// BEFOR
const INCREMENT = 'counter/increment'

function increment(amount: number) {
  return {
    type: INCREMENT,
    payload: amount,
  }
}

const action = increment(3)
// { type: 'counter/increment', payload: 3 }

 

RTK

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

const increment = createAction('counter/increment')

const action = increment(3)
// { type: 'counter/increment', payload: 3 }

 

createAction 함수는 toString() 메서드를 오버 라이딩하여 액션 타입 문자열로도 표현이 가능하다.

map object 표현에서 액션 생성자를 키값으로 사용이 가능하다.

 

const increment = createAction('counter/increment')

const counterReducer = createReducer(0, {
  [increment]: (state, action) => state + action.payload,
  [decrement.type]: (state, action) => state - action.payload
})

console.log(increment.toString())
// 'counter/increment'

console.log(`The action type is: ${increment}`)
// 'The action type is: counter/increment'

 

 


 

4. createSlice()

이 createSlice()를 사용하면 2번과 3번 항목인 createAction, createReducer를 따로 작성할 필요가 없어진다.

redux 공식문서에서는 slice파일은 feature 폴더 안에서 상태 도메인 별로 나누어 정리하라고 한다.

 

// features/todos/todosSlice.js

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'

interface Item {
  id: string
  text: string
}

// 투두 슬라이스
const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Item[],
  reducers: {
    // 액션 타입은 슬라이스 이름을 접두어로 사용해서 자동 생성됩니다. -> 'todos/addTodo'
    // 이에 상응하는 액션 타입을 가진 액션이 디스패치 되면 리듀서가 실행됩니다.
    addTodo: {
      reducer: (state, action: PayloadAction) => {
        state.push(action.payload)
      },
      // 리듀서가 실행되기 이전에 액션의 내용을 편집할 수 있습니다.
      prepare: (text: string) => {
        const id = nanoid()
        return { payload: { id, text } }
      },
    },
  },
})

const { actions, reducer } = todosSlice
export const { addTodo } = actions

export default reducer

 

extraReducers

extraReducers는 createSlice가 생성한 액션 타입 외 다른 액션 타입에 응답할 수 있도록 한다. 

외부의 액션을 참조할 때 사용할 수 있다.

 

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {},
  extraReducers: (builder) => {

    // 'users/fetchUserById' 액션 타입과 상응하는 리듀서가 정의되어 있지 않지만
    // 아래처럼 슬라이스 외부에서 액션 타입을 참조하여 상태를 변화시킬 수 있습니다.

    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      state.entities.push(action.payload)
    })
  },
})

 

 


5. createAsyncThunk()

createAction의 비동기 버전을 위해 만들어졌다.

액션 타입 문자열과 프로미스를 반환하는 콜백 함수를 인자로 받아 주어진 액션 타입을 접두어로 사용하는 프로미스 생명주기 기반의 액션 타입을 생성한다.

 

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)

    return response.data
  }
)

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {},
  // extraReducers에 케이스 리듀서를 추가하면 
  // 프로미스의 진행 상태에 따라서 리듀서를 실행할 수 있습니다.
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {})
      .addCase(fetchUserById.fulfilled, (state, action) => {
	      state.entities.push(action.payload)
      })
      .addCase(fetchUserById.rejected, (state) => {})
  },
})

// 위에서 fetchUserById, 즉 thunk를 작성해두고
// 앱에서 필요한 시점에 디스패치 하여 사용합니다.

// ...

dispatch(fetchUserById(123))

 

 

6. createSelector()

createSelector는 리덕스 스토어에서 데이터를 추출하도록 도와준다. Reselect 라이브러리에서 제공하는 함수를 그대로 가져온 것인데 useSelector 함수의 결점을 보완하기 위한 좋은 설루션이기 때문이다.

 

// useSelector는 스토어에서 값을 조회합니다.
const users = useSelector((state) => state.users)

 

Reselect 라이브러리는 createSelector 함수가 메모이제이션 기반으로(이전의 값을 메모리에 저장하고 변경된 값이 있을 때만 변경되도록) 동작한다. 

 

이는 아래와 같은 문제점을 개선할 수 있다.

 

예제를 보면

 

const users = useSelector(
  (state) => state.users.filter(user => user.subscribed)
);

이 예제의 useSelector는 스토어를 자동으로 구독하기 때문에 컴포넌트를 다시 렌더링 하는 경우 매번 새로운 인스턴스를 생성한다.

 

filter 함수는 useSelector가 실행될 때마다. 매번 새로운 배열을 반환하고, 이전 주소와 새로운 주소가 차이를 발생시켜 리 렌더링을 발생시킨다.

 

그래서 createSelector를 사용하면 애플리케이션 최적화를 할 수 있다.

 

const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent

// subtotal 값을 메모이제이션 합니다.
const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((subtotal, item) => subtotal + item.value, 0)
)

// 메모이제이션된 subtotal 값과 taxPercentSelector를 합성하여
// 새로운 값을 메모이제이션 합니다.
const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

const exampleState = {
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
}

console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState))      // 0.172
console.log(totalSelector(exampleState))    // { total: 2.322 }

 


나의 정리

 

아직 RTK는 간단한 사이드 프로젝트 밖에 만들지 않아서, 또 Redux를 제대로 사용해보지 않아.

이런 예제나 이론들이 한눈에 들어오지 않는다.

 

하지만 이런 식으로 redux의 개념을 정리하고, 내가 작성한 글로 찾아보고 적용해 보는 습관을 들이고, 

이러한 세부적인 기능들도 구현해본다면 좀 더 RTK랑 친해질 수 있지 않을까..!?

 

 

 

 

 

참고한 글

- http://blog.hwahae.co.kr/all/tech/tech-tech/6946/

 

Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까?

Redux Toolkit 최근 훅 기반의 API 지원이 가속화되고 React Query, SWR 등 강력한 데이터 패칭과 캐싱 라이브러리를 사용하면서 리덕스 사용이 줄어드는 방향으로 프론트엔드 기술 트렌드가 변화하고 있

blog.hwahae.co.kr

- https://velog.io/@yoonvelog/Redux-toolkit

 

Redux-toolkit

이번에 구글 설문지의 주요 기능을 구현해보면서 redux-toolkit을 사용해보게 되었다. [프로젝트 보기] 기존에는 redux와, thunk 미들웨어를 사용하여 상태관리를 했었는데 redux-toolkit의 장점들도 분명

velog.io

 

반응형

댓글