💡 이 챕터에서 배우는 내용
- 타입스크립트에서 Redux Toolkit과 React-Redux 셋업하고 사용하는 방법
@types/react-redux typedefs 패키지에 타입이 정의되어 있습니다. 라이브러리 함수를 입력하는 것 외에도, 타입들은 우리의 리덕스 스토어와 리액트 컴포넌트 사이의 타입안정 인터페이스를 쉽게 작성할 수 있도록 도움 주는 것들을 내보낼(export) 수 있습니다.@types/react-redux가 자동으로 포함되어 있으므로 따로 설치하지 않아도 됩니다.$ npm install @types/react-reduxRedux 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
RootState와 AppDispatch 타입을 임포트할 수 있는 반면에, 애플리케이션에서 사용하기 위해 useDispatch와 useSelector 훅의 타입을 작성하는 것이 좋다.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
createSlice는 리듀서마다 state의 타입을 올바르게 추론할 수 있습니다.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// 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