728x90
상태관리 라이브러리

 

❓상황

가장 많이 쓰이는 상태관리 라이브러리인 Redux의 최신버전인 Redux Toolkit을 정리하기 위함

 

📖 Redux Toolkit 이란?

상태를 저장하는 파일을 따로 분리하여 관리할 수 있다.

 

여러 컴포넌트를 거치지 않아도, 상태가 필요한 컴포넌트에 곧바로 상태를 줄 수 있다.

 

Redux Toolkit 패키지는 Redux 로직을 작성하는 표준 방법이 되도록 고안되었습니다. 원래 reducour에 대한 세 가지 일반적인 우려를 해결하기 위해 만들어졌다.

"Redux 스토어 구성이 너무 복잡합니다."
"Redux가 유용한 작업을 수행하기 위해서는 많은 패키지를 추가해야 합니다."
"리듀스에는 너무 많은 보일러 플레이트 코드가 필요합니다."

 

🐸 설치

CRA + Redux

# Redux + Plain JS template
yarn create react-app <프로젝트 파일명> --template redux

# Redux + TypeScript template
yarn create react-app my-app --template redux-typescript

 

Redux Toolkit

react-redux와 확장버전의 리덕스 툴킷, 2개의 라이브러리를 설치해야한다.

# yarn
yarn add react-redux @reduxjs/toolkit

 

🧮 정리

Store 생성

state들을 가질 저장소인 store를 생성해줘야한다.

 

일반적으로 src/app/store.ts 라는 경로에 생성하며,

configureStore라는 함수를 @reduxjs/toolki에서 불러와 store를 생성한다.

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {
  	# slice들이 넣어져야한다.
  },
})

// useSelector의 파라미터인 state의 type을 지정할때 사용한다.
// state는 위에서 정의한, store의 reducer들이다.
export type RootState = ReturnType<typeof store.getState>

// 나중에 사용할 useDispatch의 type을 지정할때 사용한다.
export type AppDispatch = typeof store.dispatch

 

Provider

store가 생성되었다면, 해당 store를 모든 컴포넌트에서 사용할 수 있도록 제공해줘야한다.

 

일반적으로 <Provider> 컴포넌트를 불러와서, src/index.tsx에 <App /> 컴포넌트를 감싼다.

그리고 속성으로 store를 작성해주면 된다.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

 

Slice 생성

store의 구성요소 중 하나인, slice를 생성해줘야한다.

 

일반적으로 src/features 라는 경로에 사용될 유형의 폴더를 생성하고, 해당 폴더안에 ts파일을 생성한다.

예시로 숫자가 증가하는 state를 갖는 slice를 생성하려고 한다면,

src/features/counter/counter.ts 라는 경로로 파일을 생성하면 된다.

 

import { createSlice } from '@reduxjs/toolkit'

# payload type을 지정하기 위해 불러온다.
import type { PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

# state 초기값을 설정한다.
const initialState: CounterState = {
  value: 0,
}

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
  	# state는 counterSlice의 state를 뜻한다.
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    # 메소드는 2개의 파라미터를 갖는데, 첫번째는 해당 slice의 state이고,
    # 두번째는 해당 메소드가 사용되는 곳에서 인자를 받을때,action이 해당 인자를 객체값으로 받는다.
    # action은 인자를 받기때문에, type을 지정해줘야한다.
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

// 내보낸 함수는 counterSlice의 state를 변경할 때 사용한다.
# dispatch(increment());
// counterSlice.actions는 counterSlice의 reducers들을 의미한다.
export const { increment, decrement, incrementByAmount } = counterSlice.actions

# counterSlice.reducer는 store에 slice를 등록할때 사용된다.
export default counterSlice.reducer

 

Store에 Slice 등록하기

생성된 slice를 store에 등록해야한다.

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

 

사용하기

# useSelector의 type을 지정할때 사용한다.
import type { RootState } from '../../app/store'

# useDispatch의 type을 지정할때 사용한다.
import type { AppDispatch } from '../../app/store'

# useSelector는 store에 속한 slice의 value를 가져올때 사용한다.
# useDispatch는 slice의 action들을 시행할때 고차함수로 사용한다.
import { useSelector, useDispatch } from 'react-redux'

# slice의 action들을 불러온다.
import { decrement, increment, incrementByAmount } from './counterSlice'

export function Counter() {
  # state는 store의 reducer에 적힌 slice들을 불러온다.
  # 실습에서는 counter만 적었기때문에, counter만 있다.
  # 확인하고 싶다면, console.log(state)를 이용하면 확인할 수 있다.
  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch<AppDispatch>()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(incrementByAmount(2))}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

 

hook으로 만들기

사용하기에서 조금 아쉬운 부분이 있다.

  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch<AppDispatch>()

typescript를 사용하는 궁극적인 이유는, 컴파일 전에 미리 오류를 감지하여 쉽게 디버깅을 할 수 있기때문에 typescript를 사용한다.

typescript는 type을 필수적으로 지정해줘야하는데, type을 지정해주기때문에 코드가 조금 읽기가 어렵다.

 

이를 해결하기 위해, 별도의 hook으로 분리하고, 해당 파일안에 type을 지정하고 hook을 불러오면 코드가 깔끔해진다.

import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

물론 이것이 필수가아닌 선택사항이기 때문에, 지금 배우기에 복잡하다면, 나중에 redux-toolkit이 익숙해진 다음에 해도 늦지 않을 것이다.

 

데이터 패칭 : 비동기

이번에는 API 호출을 통해, 비동기적으로 데이터를 가져와보자

# 데이터 패칭 비동기 함수를 생성하는 createAsyncThunk를 불러온다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

# 데이터 패칭 함수를 생성하는데, 일반적인 방식이 아닌, createAsyncThunk 함수를 이용해야한다.
# createSyncThunk는 2개의 파라미터를 갖는데,
# 첫번째 파라미터는 고유한 key값이고,
# 두번째 파라미터는 콜백함수이다.
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (id: number) => {
    const response = await fetch(`url/${id}`)
    return response.json();
  }
)

interface UsersState {
  entities: []
  loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState: UsersState = {
  entities: [],
  loading: 'idle',
}

const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    # 일반적인 action을 정의할때 사용한다.
  },
  # 비동기 함수의 상태에 따라, action을 정의할 수 있다.
  # extraReducers는 builder 콜백함수를 첫번째 파라미터로 갖는다.
  extraReducers: (builder) => {
    
    # builder는 addCase 메소드를 갖고, 2개의 파라미터를 가진다.
    # 첫번째는 createAsyncThunk로 생성된 fetchUserById의 상태를 받고,
    # 두번째는 fetchUserById의 상태일때의 action 함수를 정의한다.
    # 즉, state는 useSlice의 state이고,
    # action은 fetchUserById에서 return된 값을 가진다.
    # 물론 action.payload를 이용하여 값을 이용할 수 있다.
    builder.addCase(fetchUserById.pending, (state, action) => {
      state.loading = 'pending';
    }),
    
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      state.loading = 'succeeded';
      state.entities.push(action.payload)
    })
    
    builder.addCase(fetchUserById.rejected, (state, action) => {
      state.loading = 'failed';
    })
  },
})

 

extraReducers의 'builder' 콜백이 사용되는 이유는, 올바른 타입의 reducer를 제공하기 때문입니다...라고 한다.

복사했습니다!