본문 바로가기

React/이론

[React] Redux - toolkit편

Redux Toolkit? 

Redux Toolkit은 Redux 팀에서 세 가지 걱정을 해결하기 위해서 등장했다. 

 

1. Redux 스토어 환경 설정이 너무 복잡하다. 

2. Redux를 쉽게 사용하기 위해선 추가로 패키지들을 설치해야 한다. 

3. Redux는 많은 보일러플레이트 코드를 요구한다. 

 

즉, Redux Toolkit은 Redux로 개발하는 과정을 단순화하여 흔한 실수를 방지할 수 있게 해 준다

 

※ 보일러플레이트 코드? 

최소한의 변경으로 여러 곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다. 

 

Redux Toolkit 사용법 

Redux Toolkit API는 크게 7가지가 있다. 

1. configureStore

Redux의 createStore 함수와 유사한 함수이다. Reducer들을 자동으로 합치고,

미들웨어를 추가할 수 있다. 

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

const store = configureStore({
    reducer: rootReducer,
    // or 
    reducer: {
        todos,
        counter,
    }
    
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(anyMiddleware);
    devTools: process.env.NODE_ENV !== 'production',   
});

configureStore 함수는 reducer, middleware, devTools와 예시엔 없는, preloadedState,

enchancer를 받는다.

  • reducer : 내부에 combineReducers 함수가 있기 때문에 자동으로 리듀서들을 병합하여
                   루트 리듀서를 만들어준다. 
  • middleware : Redux 미들웨어를 담는 배열이다. 기본적으로 제공하는 getDefaultMiddleware와 함께
                          예시처럼 사용할 수 있다. 
  • devTools : Boolean 값으로 Redux 개발자 도구를 끄거나 켤 수 있다. 
  • preloadedState : 스토어의 초기값을 설정할 수 있다. 
  • enchaners : 원하는 콜백 함수를 미들웨어가 적용되는 순서보다 앞에 추가할 수 있다. 

 

2. creacteReducer

상태 변화를 일으키는 리듀서 함수를 생성하는 유틸 함수이다. 내부에 immer 라이브러리가

존재해서 state.todos[3].complated = true 같은 형태인 Mutative한 코드도 불변 업데이트가 이루어진다. 

 

3. createAction

기존 Redux에서는 액션을 정의할 때, 액션 타입 상수와 액션 생성자 함수를 분리해서 선언하였다. 

하지만 createAction을 사용하면 하나로 결합할 수 있다. 

// Before
const INCREMENT = 'counter/increment';

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

const action = increment(3)

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

const increment = createAction('counter/increment');

const action = increment(3)

 

4. createSlice

앞서 설명한 createAction, createReducer 함수가 내부적으로 사용되고, 자체적으로 이름을

작성하고, 내부 옵션에 따라 리듀서와 액션 생성자, 액션 타입을 자동으로 생성한다. 

 

즉, createSlice를 사용하면 createAction과 createReducer는 포함되어 있기 때문에 작성할 필요가 없다. 

 

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

interface Item {
    id: string,
    text: string,
}

const todosSlice = createSlice({
    name: 'todos',
    initialState: [] as Item[],
    reducers: {
        addTodo: {
        	reducer: (state, action: PayloadAction) => {
            	state.push(action.payload)
            },
            prepare: (text: string) => {
            	return { payload: { text }}
            }
        }
    }
})

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

export default reducer

reducers에서 만들어진 addTodo는 ' todos/addTodo ' 라는 명칭으로 액션 타입이 자동 생성된다. 

또한 prepare에서 리듀서가 실행되기 전에 액션의 내용을 편집할 수 있다. 

 

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: false },
  reducers: {},
  
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {          // 호출 전
          state.loading = true
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {// 호출 성공
	      state.loading = false
          state.entities.push(action.payload)
      })
      .addCase(fetchUserById.rejected, (state) => {         // 호출 실패
          state.loading = false
      })
  },
})

// ...

dispatch(fetchUserById(123))

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

즉, 외부의 액션을 참조할 경우에 사용하는 방식이다. 

 

비동기 작업은 진행 상황에 따라 pending ( 호출 전 ), fulfilled ( 호출 성공 ), rejected ( 호출 실패 )가 있다. 

 

비동기 작업을 할 때, 몇 가지 신경 쓰는 부분이 있는데, 

첫 번째로 오류 처리하기이다.

createAsyncThunk는 각각의 컴포넌트 내부에서 오류를 처리할 수 있다. 

const onClick = async () => {
  try {
    const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap();
    // handle result here
  } catch (rejectedValueOrSerializedError) {
    // handle error here
  }
}

dispatch된 thunk는 unwrap 프로퍼티를 가지고 있는데, 컴포넌트 내부에서 오류처리가 가능하다. 

 

const updateUser = createAsyncThunk(
  'users/update',
  async (userData, { rejectWithValue }) => {
    const { id, ...fields } = userData;
    try {
      const response = await userAPI.updateById(id, fields);
      return response.data.user;
    } catch (err) {
      // Use `err.response.data` as `action.payload` for a `rejected` action,
      // by explicitly returning it using the `rejectWithValue()` utility
      return rejectWithValue(err.response.data);
    }
  }
);

rejectWithValue 함수를 사용하면 createAsyncThunk에서 사용되어 rejected에서 에러를 사용할 수 있다.

 

두 번째로 취소하기이다. 

비동기 처리 전에 취소하기 

createAsyncThunk의 세 번째 파라미터의 condition 속성을 사용하면 비동기 처리 전에 취소가 가능하다. 

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId);
    return response.data;
  },
  {
    condition: (userId, { getState, extra }) => {
      // ...
      
      if( ... ) {
      	return flase;
      }
    },
    // 만약, thunk가 취소되더라도 rejected 액션이 디스패치되길 원한다면
    // 옵션의 dispatchConditionRejection 속성을 true로 설정한다. (기본값은 false)
    dispatchConditionRejection: true, 
  }
);

condition 속성은 thunk 인자와 { getState, extra } 형식의 객체를 매개변수로 받는 함수이다.

condition 속성 함수가 false를 반환한다면 thunk가 취소되며, 그렇지 않다면 그대로 실행된다. 

 

만약 취소 시 rejected 액션이 디스패치되길 원한다면 

dispatchConditionRejection 옵션을 true로 설정하면 된다. 

 

비동기 실행 중 취소하기 

thunk가 이미 실행되고 있을 때, 취소하고자 한다면 dispatch가 반환하는 abort 메서드를 사용하면 된다. 

import { fetchUserById } from './slice'
import { useAppDispatch } from './store'
import React from 'react'

function MyComponent(props: { userId: string }) {
  const dispatch = useAppDispatch();
  React.useEffect(() => {
    // Dispatching the thunk returns a promise
    const promise = dispatch(fetchUserById(props.userId));
    return () => {
      // `createAsyncThunk` attaches an `abort()` method to the promise
      promise.abort();
    }
  }, [props.userId]);
}

 

6. createSelector

Redux Store에서 데이터를 추출할 수 있도록 도와주는 유틸리티이다. 

// todos
[
  { id: 1, text: '책 읽기', done: true },
  { id: 2, text: '블로그 글 쓰기', done: true },
  { id: 3, text: '운동하기', done: false },
  { id: 4, text: '요리하기', done: false }
]

function UndoneTasks() { 
   const tasks = useSelector(state => state.todos.filter(todo => todo.done));
   // ...
}

기존 useSelector는 Store를 사종으로 구독하고 있기 때문에 상태 트리가 갱신되어 컴포넌트를

다시 렌더링 해야 하는 상황에 매번 새로운 인스턴스를 생성하게 된다. 

 

useSelector를 사용해서 user의 subscribed가 true인 데이터만 가지고 오려고 filter를 사용했다. 

별 문제가 없어 보이지만, Redux에서 관리되는 다른 상태가 변경될 때도 배열의 filter 함수가 새로운

배열을 생성하기 때문에 리 렌더링이 발생한다. 

 

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

import { createSelector } from '@reduxjs/toolkit'

const todosSelector = (state) => state.todos;
const undoneTodos = createSelector(
  todosSelector, 
  (todos) => todos.filter((todo) => !todo.done)
);

function UndoneTasks() { 
   const tasks = useSelector(undoneTodos);
   // ...
}

createSelector는 selector를 연달아서 넣을 수 있다. 

예를 들어 todoSelector가 있다면, 이게 undoneTodos의 첫 번째 인자로 지정되어 있다. 

그래서 첫 번째 selector에서 반환된 값이 변경될 때만 다음 selector가 호출돼 원하는 값을 연산해 조회한다.

 

이렇게 해서 todos 배열에 실제 변화가 있을 때만 filter 함수를 돌리고 리 렌더링을 하게 된다.

 

7. createEntiryAdapter

중복을 최소화하기 위해서 데이터 구조화가 되고 일관성이 보장되는 구조에서 효율적인 CRUD를

수행하기 위해서 미리 빌드된 리듀서 및 셀렉터를 생성하는 함수이다. 

import {
  createEntityAdapter,
  createSlice,
  configureStore,
} from '@reduxjs/toolkit'

// Since we don't provide `selectId`, it defaults to assuming `entity.id` is the right field
const booksAdapter = createEntityAdapter({
  // Keep the "all IDs" array sorted based on book titles
  sortComparer: (a, b) => a.title.localeCompare(b.title),
})

const booksSlice = createSlice({
  name: 'books',
  initialState: booksAdapter.getInitialState({
    loading: 'idle',
  }),
  reducers: {
    // Can pass adapter functions directly as case reducers.  Because we're passing this
    // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator
    bookAdded: booksAdapter.addOne,
    booksLoading(state, action) {
      if (state.loading === 'idle') {
        state.loading = 'pending'
      }
    },
    booksReceived(state, action) {
      if (state.loading === 'pending') {
        // Or, call them as "mutating" helpers in a case reducer
        booksAdapter.setAll(state, action.payload)
        state.loading = 'idle'
      }
    },
    bookUpdated: booksAdapter.updateOne,
  },
})

 

 

 

 

반응형

'React > 이론' 카테고리의 다른 글

[React] Redux  (1) 2022.10.14
[React] Flux  (1) 2022.10.13
[React] Redux - 기본편  (1) 2022.07.21
[React] MobX - 심화  (2) 2022.05.05
[React] MobX - React에서 사용하기  (3) 2022.05.03