Redux Toolkit 알아보기

이병수·2021년 1월 7일
35
post-thumbnail
post-custom-banner

오늘은 리덕스 툴킷에 대해 알아보자~!

Redux Toolkit이란

사용이유

Redux Toolkit은 Redux를 더 사용하기 쉽게 만들기 위해 Redux에서 공식 제공하는 개발도구이다. Redux Toolkit은 아래와 같은 Redux의 문제점을 보완하기 위해 등장하였다.

Redux 사용시 문제점

  • 저장소 구성의 복잡성
  • 많은 패키지 필요성(의존성)
  • 한 작업 시 필요한 수 많은 코드양(boilerplate)

리덕스를 라이브러리 없이 사용 시 1개의 액션을 생성해도 액션타입 정의 -> 액션함수 생성 -> 리듀서 정의 의 작업이 필요하다. 많아지는 액션을 관리하기 위해 redux-actions을, 불변성 보존을 위한 immer, store값을 효율적으로 핸들링하여 불필요한 리렌더링을 막기 위해 reselect, 비동기 작업을 위한 thunksaga리덕스의 유효한 기능을 사용하기 위해 4~5개의 라이브러리를 사용해야 했다.하지만 Redux Toolkit은 내장된 기능으로 saga를 제외한 위의 모든 기능을 제공한다.

Redux Toolkit 특징


(출처 : 리덕스 홈페이지)

  • Simple: 스토어 설정, 리듀서 생성, 불변성 업데이트 로직 사용을 편리하게 하는 기능 포함
  • Opitionated: 스토어 설정에 관한 기본 설정 제공, 일반적으로 사용되는 redux addon이 내장
  • Powerful : Immer에 영감을 받아 '변경'로직으로 '불변성'로직 작성 가능, state 전체를 slice로 자동으로 만들 수 있음
  • Effective : 적은 코드에 많은 작업을 수행 가능

이를 위해 immer produce,reselect, ducks pattern, Redux Devtools, FSA 규약, typescript, 미들웨어(thunk 한정) 등을 지원하고 있다. 오늘은 configureStore,createAction, createReducer,createSlice,createAsyncThunk에 대해 알아보자

사용방법

createAction

기존

const INCREMENT = 'counter/increment' 📌 1

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

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

action함수를 정의하기 위해 위와 같이 2번의 과정이 필요하며 별도의 액션타입을 설정해줘야 한다.

변경

const increment = createAction('counter/increment') 📌 1

let action = increment() ✅ 사용
// { type: 'counter/increment' }

action = increment(3) ✅ 사용
// returns { type: 'counter/increment', payload: 3 }

action을 사용하기 위해 한번의 과정이면 된다.
type만 인자로 넣어주면 default로 type을 가진 action object(액션함수)를 생성해주며
만약 이 액셤함수를 호출 시 parameter를 추가로 넣어준다면 자동으로 payload의 value값으로 들어간다.

createReducer

기존

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'increment':
      return state + action.payload
    case 'decrement':
      return state - action.payload
    default:
      return state
  }
}

1)기존에는 reducer 사용시 switch 등의 조건문으로 action type을 구분해 특정 로직을 수행했다.
2) default를 항상 명시 해주어야 한다.

변경

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

별도 조건문 필요 없이 첫 번째 인자는 initailState, 두 번째 인자는 caseReducers를 가진다.

createAction + createReducer

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
})

createSlice = action + reducer

createSlice는 Ducks 패턴을 사용해 action과 reducer를 전부 가진 함수이다.

기본형태

createSlice({
    name: 'reducerName',
    initialState: [],
    reducers: {
        action1(state, payload) {
            //action1 logic
        },
        action2(state, payload) {
            //action2 logic
        },
        action3(state, payload) {
            //action3 logic
        }
    }
})

name 속성은 액션의 경로를 잡아줄 이름이고 initialState은 초기 state를 나타낸다.
reducer는 이전의 리듀서 함수와 같이 액션에 따라 특정 로직을 수행하는 것은 같으나
기존에는 액션생성함수액션 타입을 선언해 사용했다면 createSlice는 액션을 선언하고 해당 액션이 dispatch 되면 바로 state을 가지고 해당 액션을 처리한다.

즉, reducers 안의 코드들은 Action Type, Action Create Function, Reducer 의 기능이 합쳐져 있는 셈이다.

configureStore

기존

import { createStore } from "redux";
import rootReducer from "./Redux/Reducer";

const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);

1) store를 생성할 때 redux가 제공하는 createStore을 이용해 생성하며 redux가 제공하는 combineReducers로 리듀서 함수를 묶어준 root리듀서를 인자로 넣는다.
2) devTools 사용 시 두번째 인자로 넣어준다.

변경

import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer,
  },
});

1) 별도의 combineReducers로 리듀서를 묶어줄 필요 없이 reducer 필드를 필수적으로 넣고 그안에 리듀서 함수를 넣으면 된다.
2) default로 redux devtool을 제공한다.

기존

//액션타입 
const increase = "INCREASE";
const decrease = "DECREASE";
const increaseFive = "INCREASE_FIVE";

//액션생성함수
export const increaseAction = () => {
  return { type: increase };
};

export const decreaseAction = () => {
  return { type: decrease };
};

export const increaseOtherAction = (number) => {
  return { type: increaseFive, data: number };
};

//초기 상태값
const initialState = {
  number: 30,
};

//리듀서
export const numberReducer = (state = initialState, action) => {
  switch (action.type) {
    case increase:
      return {
        ...state,
        number: state.number + 1,
      };

    case decrease:
      return (state = {
        ...state,
        number: state.number - 1,
      });

    case increaseFive:
      return (state = {
        ...state,
        number: state.number + action.data,
      });

    default:
      return state; 
  }
};

name : 해당 모듈의 이름 작성
initialState : 해당 모듈의 초기값을 세팅
reducers : 리듀서를 작성하고 이때 해당 리듀서의 키값으로 액션함수가 자동으로 생성됨..
extraReducers : 액션함수가 자동으로 생성되지 않는 별도의 액션함수가 존재하는 리듀서를 정의 (선택 옵션)

변경

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '../../app/store';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  }; 
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

createAsyncThunk

기존

기존 redux에서 비동기 처리를 할 경우 thunk, saga, redux-observable 등의 미들웨어를 사용하여 한 개의 비동기 액션에 대해 pending, success, failure의 상태를 생성하여 처리하였다. 이때 각 상태를 만드는 것을 유틸 패키지를 받거나 직접 구현해야 했다

변경

리덕스 툴킷에서는 createAsyncThunk를 제공하여 Thunk의 미들웨어를 간편하게 사용할 수 있다
(단 다른 것은 이전 버전과 같이 직접 구현하거나 패키지를 받아야함)
또한 액션에 대한 상태의 이름을 지정하여 각기 다른 이름으로 생기는 혼란을 방지한다
(pending: 비동기 호출전, fulfilled: 비동기 호출성공, rejected: 비동기 호출실패)

export const fetchRecipes: any = createAsyncThunk<
  any
>(
  'recipes/fetchRecipes', // 액션 이름을 정의한다. 
  async () => { // 비동기 호출 함수를 정의 
    try {
      const response = await fetch("https://www.themealdb.com/api/json/v1/1/search.php?s=")
      const data = await response.json()
      console.log("data: ", data)
      return data.meals
    } catch (error) {
    }
  },
);

위와 같이 createAsyncThunk 를 선언하게 되면 첫번째 파라미터로 선언한 액션 이름 에 pending, fulfilled, rejected 의 상태에 대한 action 을 자동으로 생성해주게 된다.

createSlice + createAsyncThunk

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

interface RecipeState {
  loading: boolean,
  hasErrors: boolean,
  recipes: Array<RecipeData>,
}

export interface RecipeData {
  idMeal: any,
  strMeal: any
  strMealThumb: any
}

const initialState: RecipeState = {
  loading: false,
  hasErrors: false,
  recipes: [],
};

export const fetchRecipes: any = createAsyncThunk<
  any
>(
  'recipes/fetchRecipes',
  async () => {
    try {
      const response = await fetch("https://www.themealdb.com/api/json/v1/1/search.php?s=")
      const data = await response.json()
      console.log("data: ", data)
      return data.meals
    } catch (error) {
      //에러 처리 로직 
    }
  },
);

export const recipesSlice = createSlice({
  name: 'recipes',
  initialState,
  reducers: {
    getRecipes: (state, { payload }) => {
      state.recipes = payload
    }
  },
  extraReducers: {
    [fetchRecipes.pending]: (state) => {
      state.loading = true
    },
    [fetchRecipes.fulfilled]: (state, { payload }) => {
      state.recipes = payload
      state.loading = false
      state.hasErrors = false
    },
    [fetchRecipes.rejected]: (state) => {
      state.loading = false
      state.hasErrors = true
    },
  },
})

export const recipesSelector = (state: any) => state.recipes

export default recipesSlice.reducer
post-custom-banner

3개의 댓글

comment-user-thumbnail
2021년 5월 3일

대박 도움되었습니다!

답글 달기
comment-user-thumbnail
2021년 11월 25일

대박대박 감사합니다!!!

답글 달기
comment-user-thumbnail
2022년 1월 13일

진짜 정리 잘해주셨네요 감사합니다!! 근데 중간에 전체 코드가 짧아진 부분 보니 소름이 쫙.. ㄷㄷ

답글 달기