개발/프론트엔드 (JS & React)

[Redux toolkit] 2 - Slice, Reducer, Action, Thunk란?

jungwon_ 2022. 11. 11. 23:56

지난 글에서 Redux와 Redux Toolkit의 차이점에 대해 알아보았다.

 

[Redux Toolkit] 1 - Redux Toolkit이란? | Redux와 Redux Toolkit 차이

Redux Toolkit이란? 리액트 개발자라면 리덕스에 대해서 들어봤을 것이다. 리덕스란 자바스크립트 앱의 state 컨테이너로 1) 앱이 일관되게 동작하도록 하고, 2) 중앙화되어 있으며, 3) state가 언제, 어

jwdevv.tistory.com

오늘은 리덕스 툴킷의 API에 대해 더 자세히 알아보자.


Slice

지난 글에서 store를 구성하는 방법에 대해 알아보았다.

// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = { value: 0 }

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action) {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

 

Slice란 쉽게 생각해서 각 기능별로 store를 슬라이스하는 것이라 생각하면 된다.

 

예를 들어 Todo List 앱을 개발한다고 가정해보자.

 

1) 로그인 기능 2) Todo list를 보여주는 기능이 있다고 가정했을 때 state를 크게 1) 로그인과 연관된 state 2) Todo list와 연관된 state로 나누는 것이다.

import { configureStore } from '@reduxjs/toolkit';
import loginSlice from './features/login/loginSlice';
import todoSlice from './features/todo/todoSlice';
export const store = configureStore({
  reducer: {
    login: loginReducer,
    todo: todoReducer,
  },
});

 

Slice 파일을 위한 가장 흔한 코딩컨벤션은 src/features 폴더에 저장하는 방법이 있다.


Reducer, Action

reduceraction type에 따라 state를 컨트롤하는 함수라고 생각하면 된다.

// ...
reducers: {
    increment: (state) => {
      state.value++
    },
    decrement: (state) => {
      state.value--
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
},
// ...

slice를 생성할 때 위와 같이 reducers 프로퍼티 안에서 reducer를 정의하며,

 

reducer를 생성하면 action creator가 reducer와 같은 이름으로 action(increment, decrement 등)을 자동 생성한다.

 

 

컴포넌트에서 action을 사용해보자.

 

아래와 같이 'react-redux' 의 useDispatch를 사용해 dispatch를 정의하고 action을 dispatch하면, 그에 맞는 reducer가 실행된다.

// App.js
// ...
import { useDispatch } from 'react-redux';
import { increment } from '../features/counter/counterSlice'

function App() {
  const dispatch = useDispatch();
  
  // ...
  
  return (
    <div>
      <button onClick={() => dispatch(increment())}>increment</button>
    </div>
  )
}

 

또한 Redux Toolkit에서는 immer 라이브러리를 포함하기 때문에 state를 mutating 하는 것이 가능하다.

// ...
reducers: {
    increment: (state) => {
      state.value++ // reducer는 항상 새로운 state를 반환하므로 mutating 가능
    },
},
// ...

컴포넌트에서 State에 접근하기

그렇다면 컴포넌트에서 Store에 저장된 state에는 어떻게 접근할까?

 

마찬가지로 'react-redux'의 useSelector를 사용하면 된다.

 

useSelector는 첫 번째 인자로 store 자체를 넘겨주고, 이 store를 통해 slice의 state에 접근할 수 있다.

// ...
import { useSelector } from 'react-redux';

function App() {
  const { count } = useSelector((store) => store.counter);
  // ...
}

Thunk

일반적으로 reducer는 sync function만 사용가능하다.

 

그래서 API에서 데이터를 불러오는 기능을 reducer로 구현하려고 하면 구현할 수 없다. (API에서 데이터를 불러오기 위해서는 asyncronous하게 구현해야하기 때문)

 

이를 위해 제공되는 것이 createAsyncThunk 이다.

 

createAsyncThunk는 action type과 콜백 함수를 받아 Promise를 반환하는 함수이다.

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus', // action type
  async (userId, thunkAPI) => { // callback function
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

 

이렇게 생성된 함수는 Slice를 생성할 때 extraReducers 프로퍼티를 추가하며, createAsyncThunk는 Promise의 라이프사이클을 반환하기 때문에 각 라이프 사이클별로 action type을 생성한다.

 

위 경우에는 user/requestStatus의 action type으로 pending, fulfilled, rejected를 생성한다.

  • pending: users/requestStatus/pending
  • fulfilled: users/requestStatus/fulfilled
  • rejected: users/requestStatus/rejected
// ...
const usersSlice = createSlice({
  // ...
  extraReducers: (builder) => {
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      state.entities.push(action.payload)
    })
  },
})

 

컴포넌트에서 해당 reducer를 사용할 때는 일반 reducer처럼 사용하면 된다.

dispatch(fetchUserById(123))

thunkAPI

createAsyncThunk에서 콜백 함수를 생성할 때 두 가지 arguments가 주어진다.

 

첫번째는 넘겨줄 값(인자) 이고, 두번째는 thunkAPI 이다.

 

thunkAPI란 thunk 함수에 전달되는 모든 파라미터를 포함하는 객체를 말한다.

 

여기에서 주목할 점은 thunk 함수에 사용할 store 전체의 state에 접근할 수 있는 getStatedispatch 파라미터를 포함한다는 것이다.

 

이게 왜 중요하냐면, store가 여러 Slice로 나뉘어져 있을 때 전혀 상관없는 slice에도 접근해서 state값에 접근하거나 action을 dispatch할 수 있다는 이야기다.

 

예를 들어 1) loginSlice 2) modalSlice 가 있다고 가정하자.

 

login이 되었을 때 어떤 modal 을 열고 싶다고 했을 때, login과 modal은 완전히 다른 slice임에도 불구하고 login의 thunk 함수에서 thunkAPI를 사용해 modal reducer를 실행할 수 있다.

 

그 외에도 오류가 발생했을 경우 rejected action type에 넘겨줄 수 있는 rejectWithValue(value, [meta])도 있다.

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

const reducer = createReducer(initialState, (builder) => {
  builder.addCase(fetchUserById.rejected, (state, action) => {
  	console.log(action.payload) // rejectWithValue에서 넘겨준 err.response.data
  })
})

리덕스 툴킷에서 타입스크립트를 사용하는 법이 궁금하다면?

 

[Redux Toolkit] 3 - 리덕스 툴킷에서 타입스크립트 사용하기

이 글은 리액트를 기준으로 작성되었습니다. 앞선 두 글에서 Redux Toolkit 에 대해 알아봤다. [Redux Toolkit] 1 - Redux Toolkit이란? | Redux와 Redux Toolkit 차이 Redux Toolkit이란? 리액트 개발자라면 리덕스에 대

jwdevv.tistory.com


출처

- [RTK docs - createSlice]

- [RTK docs - createAsyncThunk]

 

해당 글의 모든 소스 코드 출처는 Redux Toolkit 공식 문서입니다.