[Redux] TypeScript Quick Start (번역, 정리)

chaevivi·2023년 11월 28일
0
post-thumbnail

타입스크립트에서 리덕스 시작하기

💡 이 챕터에서 배우는 내용

  • 타입스크립트에서 Redux Toolkit과 React-Redux 셋업하고 사용하는 방법


1. 개요

  • Redux Toolkit은 이미 타입스크립트로 쓰여져 있기 때문에 타입 정의가 되어 있습니다.
  • React Redux는 NPM의 별도 @types/react-redux typedefs 패키지에 타입이 정의되어 있습니다. 라이브러리 함수를 입력하는 것 외에도, 타입들은 우리의 리덕스 스토어와 리액트 컴포넌트 사이의 타입안정 인터페이스를 쉽게 작성할 수 있도록 도움 주는 것들을 내보낼(export) 수 있습니다.
  • 현재 React Redux v7.2.3(23.11.28 기준)에는 @types/react-redux가 자동으로 포함되어 있으므로 따로 설치하지 않아도 됩니다.
    • 수동 설치
      $ npm install @types/react-redux


2. 프로젝트 설정


2.1. 최상위 상태(Root State)와 디스패치 타입 정의

  • Redux Toolkit의 configureStore API는 추가로 타입을 정의할 필요가 없습니다.

  • 하지만 RootState의 타입과 Dispatch 타입을 추출해서 필요한 곳에 사용하길 원할 때가 있습니다. 스토어로부터 해당 타입들을 자체적으로 추론하는 것은 상태 슬라이스를 더 추가하거나 미들웨어 설정을 수정하면 올바르게 업데이트했다는 것을 의미합니다.

  • 이들은 타입이기 때문에 app/store.ts 같은 스토어 세팅 파일로부터 직접 내보내고(export) 다른 파일에서 직접 임포트(import)하는 것이 안전합니다.

    // app/store.ts
    
    import { configureStore } from '@reduxjs/toolkit'
    // ...
    
    const store = configureStore({
      reducer: {
        posts: postsReducer,
        comments: commentsReducer,
        users: usersReducer
      }
    })
    
    // 스토어 자체에서 `RootState`와 `AppDispatch` 타입을 추론
    export type RootState = ReturnType<typeof store.getState>
    // 추론된 타입: {posts: PostsState, comments: CommentsState, users: UsersState}
    export type AppDispatch = typeof store.dispatch

2.2. 훅의 타입 정의

  • 각각의 컴포넌트에서 RootStateAppDispatch 타입을 임포트할 수 있는 반면에, 애플리케이션에서 사용하기 위해 useDispatchuseSelector 훅의 타입을 작성하는 것이 좋다.
  • 위의 내용이 중요한 이유
    • useSelector에서는 매번 (state: RootState) 타입을 입력할 필요가 없습니다.
    • useDispatch에서는 기본 Dispatch 타입이 thunks에 대해 알 지 못합니다. thunks를 올바르게 디스패치하기 위해서는 thunk 미들웨어 타입을 포함하는 스토어의 특정 맞춤형 AppDispatch 타입을 사용하고 AppDispatch와 함께 useDispatch를 사용해야 합니다. 미리 타입이 정의된 useDispatch 훅을 추가하면 AppDispatch 임포트를 잊는 것을 방지해 줍니다.

  • 이것들은 타입이 아니라 실제 변수이기 때문에 스토어 세팅 파일이 아니라 app/hook.ts 파일 같이 분리된 곳에서 정의해야 합니다.

  • 이렇게 하면 훅이 필요한 컴포넌트에서 임포트할 수 있고 잠재적인 순환 임포트 디펜던시 문제(potential circular import dependency issues)를 피할 수 있습니다.

    // app/hooks.ts
    
    import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
    import type { RootState, AppDispatch } from './store'
    
    // 기본 `useDispatch`와 `useSelector` 대신 사용하세요.
    export const useAppDispatch: () => AppDispatch = useDispatch
    export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector


3. 애플리케이션에서 사용하는 법


3.1. 슬라이스 상태와 액션 타입 정의

  • 각각의 슬라이스 파일은 초기 상태 값의 타입을 정의해야 하기 때문에 createSlice는 리듀서마다 state의 타입을 올바르게 추론할 수 있습니다.
  • 모든 액션들은 Redux Toolkit의 PayloadAction<T> 타입을 사용하여 정의해야 합니다. PayloadAction<T>는 제네릭 인자로 action.payload 필드의 타입을 갖습니다.

  • store 파일의 RootState 타입을 counterSlice.ts에서 안전하게 임포트할 수 있습니다. 이는 순환 임포트이지만 타입스크립트 컴파일러는 타입에 대해 이를 올바르게 처리할 수 있습니다. 이는 선택자(selector) 함수를 작성과 같은 사용 사례에 필요할 수 있습니다.

    // features/counter/counterSlice.js
    
    import { createSlice, PayloadAction } from '@reduxjs/toolkit'
    import type { RootState } from '../../app/store'
    
    // 슬라이스 상태값의 타입 정의
    interface CounterState {
      value: number
    }
    
    // 해당 타입을 사용해서 초기 상태값 정의
    const initialState: CounterState = {
      value: 0
    }
    
    export const counterSlice = createSlice({
      name: 'counter',
      // `createSlice`는 `initialState` 인자로 상태 타입을 추론
      initialState,
      reducers: {
        increment: state => {
          state.value += 1
        },
        decrement: state => {
          state.value -= 1
        },
        // `action.payload`의 내용을 선언하기 위해 PayloadAction 타입 사용
        incrementByAmount: (state, action: PayloadAction<number>) => {
          state.value += action.payload
        }
      }
    })
    
    export const { increment, decrement, incrementByAmount } = counterSlice.actions
    
    // 선택자같은 다른 코드는 임포트한 `RootState`의 타입 사용
    export const selectCount = (state: RootState) => state.counter.value
    
    export default counterSlice.reducer
    • 생성된 액션 생성자들은 리듀서에 제공한 PayloadAction<T>를 기반으로 한 payload 인자를 받아들이기 위해 올바르게 타입이 작성될 것입니다. 예를 들어, incrementByAmount는 인자로 number 타입을 요구합니다.

  • 가끔, 타입스크립트가 불필요하게 초기 상태의 타입을 강화할 수 있습니다. 만약 그런일이 발생한다면, 변수에 타입을 선언하는 것 대신에 as를 사용하여 초기 값을 캐스팅해 문제를 해결할 수 있습니다.
    // 문제 해결: 변수 타입을 선언하는 대신 상태 캐스트
    const initialState = {
      value: 0
    } as CounterState

3.2. 컴포넌트에서 타입화된 훅 사용

  • 컴포넌트 파일에서 React-Redux의 기존 훅 대신에 미리 타입을 선언한 훅을 임포트합니다.
// features/counter/Counter.tsx

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

export function Counter() {
  // `state` 인자는 이미 `RootState`에서 올바르게 타입이 선언되었다.
  const count = useAppSelector(state => state.counter.value)
  const dispatch = useAppDispatch()
  }



출처
🔗 공식 문서: https://ko.redux.js.org/tutorials/typescript-quick-start

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글