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