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

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

jungwon_ 2022. 11. 14. 00:40

이 글은 리액트를 기준으로 작성되었습니다.


 

앞선 두 글에서 Redux Toolkit 에 대해 알아봤다.

 

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

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

jwdevv.tistory.com

 

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

지난 글에서 Redux와 Redux Toolkit의 차이점에 대해 알아보았다. [Redux Toolkit] 1 - Redux Toolkit이란? | Redux와 Redux Toolkit 차이 Redux Toolkit이란? 리액트 개발자라면 리덕스에 대해서 들어봤을 것이다. 리덕스

jwdevv.tistory.com

 

위 글에서는 예제를 자바스크립트로 작성했는데, 오늘은 타입스크립트를 사용하는 방법에 대해 알아보고자 한다.


 

패키지 설치

프로젝트에 v7.2.3 이상의 react-redux@reduxjs/toolkit 을 설치했다면 (혹은 dependency에 추가되어있다면)

 

react-redux@types/react-redux 를 dependency로 가지고 있기 때문에 리액트 툴킷에서 타입스크립트를 사용하기 위해 별도로 설치해야하는 패키지는 없다.

 

만약 react-redux 버전이 v7.2.3 보다 낮다면 그 이상 버전을 사용하거나 @types/react-redux를 설치해주자.


store.ts

store.ts는 기존의 자바스크립트 코드와 같이 작성하고, state와 dispatch 타입을 store에서 추론(Infer)해서 export한다. (마지막 두 줄)

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

export const store = configureStore({
  reducer: {
    posts: postsReducer,
    comments: commentsReducer,
    users: usersReducer
  }
})

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

hooks.ts

컴포넌트에서 데이터를 불러오기 위해는 useSelector를, action을 dispatch하기 위해서는 useDispatch를 사용한다.

 

그런데 RTK(Redux toolkit)를 타입스크립트와 사용하려면 useSelectoruseDispatch를 타입스크립트 버전으로 바꿔줘야한다.

 

이런 타입화 과정을 모든 컴포넌트에 적용하는 것보다는 hooks.ts라는 파일을 만들어 pre-typed 된 버전을 만들어주는 것이 편하다.

 

// hooks.ts

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

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

 

그런 다음 컴포넌트에서 아래와 같이 pre-타입화된 dispatch와 selector를 사용한다.

// components/counter.tsx
// ...
import { useAppSelector, useAppDispatch } from '../hooks'

export function Counter() {
  const count = useAppSelector(state => state.counter.value)
  const dispatch = useAppDispatch()
  // ...
}

Slice 생성

slice의 state에 타입을 정의한다.

// features/counter/counterSlice.ts
// ...
interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0
}

그리고 reducer에서 payload를 인자로 받는 경우 파라미터에 아래와 같이 action.payload의 타입을 정의해준다.

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

// ...

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

Thunk

thunk 함수를 사용한다면 createAsyncThunk에서 타입 정의가 필요하다.

 

1. thunkAPI를 사용하지 않고 인자값만 받는 경우

// ...
const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: number) => { // 인자의 타입을 정의
    const response = await fetch(`https://reqres.in/api/users/${userId}`)
    return (await response.json()) as Returned // 리턴 타입 정의
  }
)
// ...

인자값의 타입과 리턴 타입을 정의해준다.

 

2. thunkAPI를 사용하는 경우

// ...
import type { AppDispatch, RootState } from "../../store";
// ...

const fetchUserById = createAsyncThunk<
  MyData, // 리턴 타입
  number, // 첫번째 인자의 타입
  {
    // thunkApi 필드 타입 정의
    dispatch: AppDispatch
    state: RootState
    extra: {
      jwt: string
    }
  }
>('users/fetchById', async (userId, thunkApi) => {
  const response = await fetch(`https://reqres.in/api/users/${userId}`, {
    headers: {
      Authorization: `Bearer ${thunkApi.extra.jwt}`,
    },
  })
  return (await response.json()) as MyData
})

thunkAPI 타입을 정의하기 위해서는 아래 AsyncThunkConfig 중 사용할 필드를 정의한 객체를 제네릭 인자 세번째에 추가해준다.

type AsyncThunkConfig = {
  state?: unknown
  dispatch?: Dispatch
  extra?: unknown
  rejectValue?: unknown
  serializedErrorType?: unknown
  pendingMeta?: unknown
  fulfilledMeta?: unknown
  rejectedMeta?: unknown
}

- state: `thunkApi.getState`의 리턴 타입

- dispatch: `thunkApi.dispatch`의 타입

- extra: thunk middleware를 위한 `thunkApi.extra` 인자의 타입

- rejectValue: `rejectWithValue`의 첫번째 인자, 즉 `rejectedAction.payload`의 타입

- serializedErrorType: `serializeError` option 콜백의 리턴 타입


Reference

- [Redux Docs] Usage With TypeScript

- [Redux Toolkit Docs] Usage With TypeScript - type safety with extraReducers

 

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