Redux Toolkit 시작하기

모성종·2022년 7월 26일
0

소개

리덕스 툴킷은 타입스크립트로 만들어져 있어서 TS definition이 내장되어있다.
React-Redux는 별도의 패키지인 @types/react-redux에 타입 정의를 가지고 있다.
React-Redux는 v7.2.3 이후 @types/react-redux에 의존성을 가지며 자동으로 설지된다.

패키지 설치

yarn add @reduxjs/toolkit react-redux
yarn add -D redux-logger @types/redux-logger

redux-logger패키지는 개발자도구 콘솔에 redux state 변화를 로그로 남겨주는 패키지이다.
redux devtools extension을 사용하면 굳이 사용하지 않아도 될 것 같다.

store 셋업

RootState와 Dispatch Types 정의하기

리덕스 툴킷은 configureStore를 사용하여 기본적인 설정을 제공한다.

import { configureStore } from '@reduxjs/toolkit'
import loginSlice from '../slices/loginSlice'
import joinSlice from '../slices/joinSlice'

export const store = configureStore({
  reducer: {
    posts: postsReducer,
    comments: commentsReducer,
    users: usersReducer,
    loginData: loginSlice.reducer
    joinData: joinSlice.reducer
  }
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

위 설정을 통해 store는 자체적으로 RootStateAppDispatch의 타입을 추론한다.
slice 구성에 따라 다르겠지만 아래와 같이 타입이 추론될 것이다.
{ posts: PostsState, comments: CommentsState, users: UsersState }

여기서, configureStore가 아닌 combineReducers를 사용한다면 아래와 같이 사용한다

import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>

State 타입을 얻는 가장 쉬운방법은 root reducer를 정의하는 것이고 그것의 ReturnType을 추출하는 것이다.
State는 보통 흔히 사용되기 때문에 혼동방지를 위해 RootState와 같은 이름을 추천한다.

Typed Hook 정의

RootStateAppDispatch 타입을 각 컴포넌트에서 import해서 사용할 수도 있지만, 애플리케이션에서 사용할 타입이 지정된 useDispatchuseSelector 을 만들어서 사용하는 것이 더 좋다.

  • useSelector는 매번 (state: RootState)를 입력할 필요가 없음
  • useDispatch의 경우, 기본 Dispatch타입은 thunk와 같은 미들웨어를 포함하지 않는다. thunks를 올바르게 dispatch하기 위해, thunk 미들웨어 타입을 포함하는 store로부터 커스터마이징된 특정 AppDispatch타입을 사용할 필요가 있고 그것을 useDispatch로 사용해야한다.
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch

useAppSelector와 useAppDispatch는 기존 useSelector, useDispatch를 추상화한 것이다. 이렇게 사용하면 각 컴포넌트에서 useSelector, useDispatch를 매번 설정하지 않고 앱 전역에서 사용할 수 있다.

앱에서 사용하기

Slice State와 Action Types 정의하기

초기 상태값에 대한 type 정의를 포함한 상태 slice 파일을 만든다. 만든 초기 상태값을 createSlice에 포함시키면 각 reducer 케이스 별로 state의 type을 알아서 추론한다.
생성되는 action들은 리덕스 툴킷의 PayloadAction<T> 타입을 사용해야한다.
여기서 사용된 제네릭 타입은 action.payload 인자로 사용된다.

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// slice 상태 타입정의
interface CounterState {
  value: number;
}

// slice 상태타입을 사용하는 초기상태 정의
const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  // 'createSlice'는 initialState를 통해 state type을 추론한다.
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    // action.payload 사용을 위해 PayloadAction<T> 타입 사용
    incrementByAmout: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } counterSlice.actions
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface JoinState {
  email: string;
  name: string;
  password: string;
  password_check: string;
}

const initialState: JoinState = {
  email: '',
  name: '',
  password: '',
  password_check: '',
}

const joinSlice = createSlice({
  name: 'joinData',
  initialState,
  reducers: {
    setJoinData: (state, action: PayloadAction<JoinState>) {
      return { ...joinData, ...action.payload }
    }
  }
})

export const { setJoinData } = joinSlice.actions;
export default joinSlice;

컴포넌트에서 typed hook 사용하기

import React, { useState } from 'react'
import { useAppSelector, useAppDispatch } from 'app/hooks'
import { decrement, increment } from './counterSlice'

export functoin Counter() {
  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()
  // ...
}
import { useAppDispatch, useAppSelector } from '../../sample/store/config';
import { setJoinData } from '../../sample/store/slices/joinSlice';

const JoinForm = () => {
  const joinData = useAppSelector((state) => state.joinData);
  const dispatch = useAppDispatch();
  
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    dispatch(setJoinData({ ...joinData, [name]: value }));
  }
}
// ...
profile
FE Developer

0개의 댓글