본문으로 건너뛰기
비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

TypeScript와 함께 사용하기

배울 내용
  • TypeScript로 Redux 앱 설정하는 표준 패턴
  • Redux 로직의 각 부분을 올바르게 타이핑하는 기술
사전 요구 사항

개요

TypeScript는 소스 코드의 컴파일 타임 검사를 제공하는 JavaScript의 타입이 있는 상위 집합(superset)입니다. Redux와 함께 사용할 때 TypeScript는 다음과 같은 기능을 제공하는 데 도움이 됩니다:

  1. 리듀서, 상태 및 액션 생성자, UI 컴포넌트에 대한 타입 안전성

  2. 타입이 지정된 코드의 손쉬운 리팩토링

  3. 팀 환경에서 향상된 개발자 경험

Redux 애플리케이션에서 TypeScript 사용을 강력히 권장합니다. 그러나 모든 도구와 마찬가지로 TypeScript에는 트레이드오프가 있습니다. 추가 코드 작성, TS 구문 이해, 애플리케이션 빌드 측면에서 복잡성을 증가시킵니다. 동시에 개발 초기에 오류를 포착하여 더 안전하고 효율적인 리팩토링을 가능하게 하며 기존 소스 코드에 대한 문서 역할을 함으로써 가치를 제공합니다.

TypeScript의 실용적인 사용은 추가 오버헤드를 정당화하기에 충분한 가치와 이점을 제공한다고 믿습니다, 특히 대규모 코드베이스에서 더욱 그렇습니다. 하지만 트레이드오프를 평가하고 자신의 애플리케이션에서 TS 사용이 가치 있는지 결정하는 데 시간을 할애해야 합니다.

Redux 코드를 타입 체킹하는 데는 여러 접근 방식이 있습니다. 이 페이지는 Redux와 TypeScript를 함께 사용하기 위한 표준 권장 패턴을 보여주며, 완전한 가이드는 아닙니다. 이 패턴들을 따르면 타입 안전성과 코드베이스에 추가해야 하는 타입 선언 양 사이의 최상의 트레이드오프를 통해 좋은 TS 사용 경험을 얻을 수 있습니다.

TypeScript와 함께하는 표준 Redux Toolkit 프로젝트 설정

일반적인 Redux 프로젝트는 Redux Toolkit과 React Redux를 함께 사용한다고 가정합니다.

Redux Toolkit (RTK)은 현대적인 Redux 로직을 작성하는 표준 접근 방식입니다. RTK는 이미 TypeScript로 작성되었으며, 그 API는 TypeScript 사용에 좋은 경험을 제공하도록 설계되었습니다.

React Redux는 타입 정의를 별도의 NPM 패키지인 @types/react-redux typedefs 패키지에서 제공합니다. 라이브러리 함수 타이핑 외에도, 이러한 타입들은 Redux 스토어와 React 컴포넌트 간의 타입 안전한 인터페이스를 더 쉽게 작성할 수 있도록 도와주는 헬퍼도 내보냅니다.

React Redux v7.2.3부터 react-redux 패키지는 @types/react-redux에 대한 의존성을 가지므로 타입 정의가 자동으로 라이브러리와 함께 설치됩니다. 그렇지 않은 경우 수동으로 설치해야 합니다(일반적으로 npm install @types/react-redux).

Create-React-App용 Redux+TS 템플릿에는 이러한 패턴이 미리 구성된 작업 예제가 포함되어 있습니다.

Root State 및 Dispatch 타입 정의

configureStore 사용에는 추가 타이핑이 필요하지 않습니다. 하지만 필요할 때 참조할 수 있도록 RootState 타입과 Dispatch 타입을 추출하는 것이 좋습니다. 스토어 자체에서 이러한 타입을 추론하면 더 많은 상태 슬라이스를 추가하거나 미들웨어 설정을 수정할 때 올바르게 업데이트됨을 의미합니다.

해당 항목들은 타입이므로 app/store.ts와 같은 스토어 설정 파일에서 직접 내보내고 다른 파일로 직접 가져오는 것이 안전합니다.

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// ...

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

// Get the type of our store variable
export type AppStore = typeof store
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']

타이핑된 훅 정의

각 컴포넌트에 RootStateAppDispatch 타입을 가져오는 것이 가능하지만, 애플리케이션에서 사용할 미리 타이핑된 버전의 useDispatchuseSelector 훅을 생성하는 것이 더 좋습니다. 이는 몇 가지 이유로 중요합니다:

  • useSelector의 경우 매번 (state: RootState)를 입력할 필요가 없어집니다

  • useDispatch의 경우, 기본 Dispatch 타입은 thunk나 다른 미들웨어를 인식하지 못합니다. thunk를 올바르게 디스패치하려면 thunk 미들웨어 타입을 포함하는 스토어의 특수 커스터마이즈된 AppDispatch 타입을 사용해야 하며, 이를 useDispatch와 함께 사용해야 합니다. 미리 타이핑된 useDispatch 훅을 추가하면 필요한 곳에서 AppDispatch를 가져오는 것을 잊지 않도록 방지해줍니다.

이들은 실제 변수이지 타입이 아니므로 스토어 설정 파일이 아닌 app/hooks.ts와 같은 별도 파일에 정의하는 것이 중요합니다. 이렇게 하면 훅을 사용해야 하는 모든 컴포넌트 파일로 가져올 수 있으며 잠재적인 순환 가져오기 종속성 문제를 피할 수 있습니다.

.withTypes()

이전에는 앱 설정으로 훅을 "미리 타이핑"하는 접근 방식이 다소 다양했습니다. 결과는 아래 스니펫과 유사하게 표시되었습니다:

app/hooks.ts
import type { TypedUseSelectorHook } from 'react-redux'
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore

React Redux v9.1.0은 각 훅에 새로운 .withTypes 메서드를 추가했으며, 이는 Redux Toolkit의 createAsyncThunk에 있는 .withTypes 메서드와 유사합니다.

이제 설정은 다음과 같이 변경됩니다:

app/hooks.ts
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

애플리케이션 사용

슬라이스 상태 및 액션 타입 정의

각 슬라이스 파일은 초기 상태 값에 대한 타입을 정의해야 하므로 createSlice가 각 케이스 리듀서에서 state의 타입을 올바르게 추론할 수 있습니다.

생성된 모든 액션은 Redux Toolkit의 PayloadAction<T> 타입을 사용하여 정의해야 합니다. 이 타입은 제네릭 인자로 action.payload 필드의 타입을 받습니다.

여기서 스토어 파일에서 RootState 타입을 안전하게 가져올 수 있습니다. 순환 가져오기이지만 TypeScript 컴파일러는 타입에 대해 이를 올바르게 처리할 수 있습니다. 셀렉터 함수 작성과 같은 사용 사례에 필요할 수 있습니다.

features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// Define a type for the slice state
interface CounterState {
value: number
}

// Define the initial state using that type
const initialState: CounterState = {
value: 0
}

export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

생성된 액션 생성자는 리듀서에 제공한 PayloadAction<T> 타입을 기반으로 payload 인수를 올바르게 수락하도록 타이핑됩니다. 예를 들어 incrementByAmount는 인수로 number를 요구합니다.

경우에 따라 TypeScript가 초기 상태의 타입을 불필요하게 좁게 추론할 수 있습니다. 이럴 때는 변수의 타입을 선언하는 대신 as를 사용하여 초기 상태를 타입 캐스팅하면 해결할 수 있습니다:

// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0
} as CounterState

컴포넌트에서 Typed Hooks 사용하기

컴포넌트 파일에서는 React Redux의 표준 훅 대신 미리 타이핑된 훅을 가져옵니다.

features/counter/Counter.tsx
import React, { useState } from 'react'

import { useAppSelector, useAppDispatch } from 'app/hooks'

import { decrement, increment } from './counterSlice'

export function Counter() {
// The `state` arg is correctly typed as `RootState` already
const count = useAppSelector(state => state.counter.value)
const dispatch = useAppDispatch()

// omit rendering logic
}
잘못된 가져오기에 대한 경고

ESLint는 팀이 올바른 훅을 쉽게 가져올 수 있도록 도와줍니다. typescript-eslint/no-restricted-imports 규칙은 실수로 잘못된 가져오기를 사용했을 때 경고를 표시할 수 있습니다.

ESLint 설정에 다음과 같이 추가할 수 있습니다:

"no-restricted-imports": "off",
"@typescript-eslint/no-restricted-imports": [
"warn",
{
"name": "react-redux",
"importNames": ["useSelector", "useDispatch"],
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
}
],

추가 Redux 로직 타이핑하기

리듀서 타입 체크하기

리듀서는 현재 state와 들어오는 action을 인자로 받아 새로운 상태를 반환하는 순수 함수입니다.

Redux Toolkit의 createSlice를 사용하는 경우, 별도로 리듀서를 명시적으로 타이핑할 필요는 거의 없습니다. 독립된 리듀서를 작성할 때는 일반적으로 initialState 값의 타입을 선언하고 actionUnknownAction으로 타이핑하는 것으로 충분합니다:

import { UnknownAction } from 'redux'

interface CounterState {
value: number
}

const initialState: CounterState = {
value: 0
}

export default function counterReducer(
state = initialState,
action: UnknownAction
) {
// logic here
}

그러나 Redux 코어는 사용할 수 있는 Reducer<State, Action> 타입도 내보냅니다.

미들웨어 타입 체크하기

미들웨어는 Redux 스토어를 위한 확장 메커니즘입니다. 미들웨어는 스토어의 dispatch 메서드를 감싸는 파이프라인으로 구성되며, 스토어의 dispatchgetState 메서드에 접근할 수 있습니다.

Redux 코어는 미들웨어 함수를 올바르게 타이핑하는 데 사용할 수 있는 Middleware 타입을 내보냅니다:

export interface Middleware<
DispatchExt = {}, // optional override return behavior of `dispatch`
S = any, // type of the Redux store state
D extends Dispatch = Dispatch // type of the dispatch method
>

커스텀 미들웨어는 Middleware 타입을 사용해야 하며, 필요한 경우 S(상태) 및 D(디스패치)에 대한 제네릭 인자를 전달해야 합니다:

import { Middleware } from 'redux'

import { RootState } from '../store'

export const exampleMiddleware: Middleware<
{}, // Most middleware do not modify the dispatch return value
RootState
> = storeApi => next => action => {
const state = storeApi.getState() // correctly typed as RootState
}
주의

typescript-eslint를 사용하는 경우, 디스패치 값으로 {}를 사용하면 @typescript-eslint/ban-types 규칙이 오류를 보고할 수 있습니다. 이 규칙이 권장하는 변경 사항은 올바르지 않으며 Redux 스토어 타입을 손상시킬 수 있으므로, 해당 라인에 대해 규칙을 비활성화하고 {}를 계속 사용해야 합니다.

디스패치 제네릭은 미들웨어 내에서 추가적인 thunk를 디스패치할 때만 필요할 것입니다.

type RootState = ReturnType<typeof store.getState>를 사용하는 경우, 미들웨어와 스토어 정의 간의 순환 타입 참조RootState의 타입 정의를 다음과 같이 변경하여 피할 수 있습니다:

const rootReducer = combineReducers({ ... });
type RootState = ReturnType<typeof rootReducer>;

Redux Toolkit 예제로 RootState 타입 정의 변경하기:

// instead of defining the reducers in the reducer field of configureStore, combine them here:
const rootReducer = combineReducers({ counter: counterReducer })

// then set rootReducer as the reducer object of configureStore
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(yourMiddleware)
})

type RootState = ReturnType<typeof rootReducer>

Redux Thunk 타입 체크하기

Redux Thunk는 Redux 스토어와 상호작용하는 동기/비동기 로직 작성을 위한 표준 미들웨어입니다. Thunk 함수는 매개변수로 dispatchgetState를 받습니다. Redux Thunk는 이러한 인자들의 타입을 정의하는 데 사용할 수 있는 내장 ThunkAction 타입을 제공합니다:

export type ThunkAction<
R, // Return type of the thunk function
S, // state type used by getState
E, // any "extra argument" injected into the thunk
A extends Action // known types of actions that can be dispatched
> = (dispatch: ThunkDispatch<S, E, A>, getState: () => S, extraArgument: E) => R

일반적으로 R(반환 타입)과 S(상태) 제네릭 인자를 지정해야 합니다. 안타깝게도 TS는 일부 제네릭 인자만 제공하는 것을 허용하지 않으므로, 다른 인자들(EA)에 대해서는 일반적으로 unknownUnknownAction을 사용합니다:

import { UnknownAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction<void, RootState, unknown, UnknownAction> =>
async dispatch => {
const asyncResp = await exampleAPI()
dispatch(
sendMessage({
message,
user: asyncResp,
timestamp: new Date().getTime()
})
)
}

function exampleAPI() {
return Promise.resolve('Async Chat Bot')
}

반복을 줄이려면 스토어 파일에 재사용 가능한 AppThunk 타입을 한 번 정의하고, thunk를 작성할 때마다 이 타입을 사용할 수 있습니다:

export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
UnknownAction
>

이 방식은 thunk가 의미 있는 반환 값을 가지지 않는다고 가정합니다. thunk가 프로미스를 반환하고 thunk 디스패치 후 반환된 프로미스를 사용하려는 경우 AppThunk<Promise<SomeReturnType>> 형태로 사용해야 합니다.

주의

기본 useDispatch 훅은 thunk를 인식하지 못합니다. 따라서 thunk를 디스패치하면 타입 오류가 발생합니다. 컴포넌트에서 thunk를 디스패치 가능한 타입으로 인식하는 업데이트된 Dispatch 형식을 사용하세요.

React Redux와 함께 사용하기

React Redux는 Redux 자체와 별도의 라이브러리이지만, React와 함께 주로 사용됩니다.

TypeScript와 함께 React Redux를 올바르게 사용하는 전체 가이드는 **React Redux 문서의 "정적 타이핑" 페이지**를 참조하세요. 이 섹션에서는 표준 패턴을 강조합니다.

TypeScript를 사용 중이라면 React Redux 타입은 DefinitelyTyped에서 별도로 관리되지만, react-redux 패키지의 종속성으로 포함되어 있으므로 자동으로 설치됩니다. 수동 설치가 필요한 경우 다음 명령을 실행하세요:

npm install @types/react-redux

useSelector 훅 타이핑

셀렉터 함수의 state 매개변수 타입을 선언하면 useSelector의 반환 타입이 셀렉터의 반환 타입과 일치하도록 추론됩니다:

interface RootState {
isOn: boolean
}

// TS infers type: (state: RootState) => boolean
const selectIsOn = (state: RootState) => state.isOn

// TS infers `isOn` is boolean
const isOn = useSelector(selectIsOn)

인라인으로 작성할 수도 있습니다:

const isOn = useSelector((state: RootState) => state.isOn)

그러나 대신 미리 타이핑된 useAppSelector 훅을 만들어 state의 올바른 타입을 내장하는 것이 좋습니다.

useDispatch 훅 타이핑

기본적으로 useDispatch의 반환 값은 Redux 코어 타입에서 정의된 표준 Dispatch 타입이므로 선언이 필요 없습니다:

const dispatch = useDispatch()

그러나 대신 미리 타이핑된 useAppDispatch 훅을 만들어 올바른 Dispatch 타입을 내장하는 것이 좋습니다.

connect 고차 컴포넌트 타이핑

아직 connect를 사용 중이라면 @types/react-redux^7.1.2에서 내보내는 ConnectedProps<T> 타입을 사용해 connect의 props 타입을 자동으로 추론해야 합니다. 이 경우 connect(mapState, mapDispatch)(MyComponent) 호출을 두 부분으로 분리해야 합니다:

import { connect, ConnectedProps } from 'react-redux'

interface RootState {
isOn: boolean
}

const mapState = (state: RootState) => ({
isOn: state.isOn
})

const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
}

const connector = connect(mapState, mapDispatch)

// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>

type Props = PropsFromRedux & {
backgroundColor: string
}

const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)

export default connector(MyComponent)

Redux Toolkit과 함께 사용하기

TypeScript와 함께하는 표준 Redux Toolkit 프로젝트 설정 섹션에서 이미 configureStorecreateSlice의 일반적인 사용 패턴을 다뤘으며, Redux Toolkit "TypeScript와 함께 사용하기" 페이지에서 모든 RTK API를 상세히 설명합니다.

RTK 사용 시 자주 접하게 될 추가적인 타이핑 패턴은 다음과 같습니다.

configureStore 타이핑

configureStore는 제공된 루트 리듀서 함수에서 상태 값의 타입을 추론하므로 특별한 타입 선언이 필요하지 않습니다.

스토어에 추가 미들웨어를 추가하려면 getDefaultMiddleware()가 반환하는 배열에 포함된 전용 .concat().prepend() 메서드를 사용해야 합니다. 이 메서드들은 추가하는 미들웨어의 타입을 올바르게 보존해 주기 때문입니다. (일반 JS 배열 스프레드를 사용하면 이러한 타입이 손실되는 경우가 많습니다.)

const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger)
})

액션 매칭하기

RTK가 생성한 액션 생성자에는 타입 술어 역할을 하는 match 메서드가 있습니다. someActionCreator.match(action)를 호출하면 action.type 문자열에 대해 문자열 비교를 수행하고, 조건으로 사용된 경우 action의 타입을 올바른 TS 타입으로 좁힙니다:

const increment = createAction<number>('increment')
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
const num = 5 + action.payload
}
}

이는 커스텀 미들웨어, redux-observable, RxJS의 filter 메서드와 같은 Redux 미들웨어에서 액션 타입을 확인할 때 특히 유용합니다.

createSlice 타이핑하기

별도의 케이스 리듀서 정의하기

케이스 리듀서가 너무 많아서 인라인으로 정의하면 지저분해지거나, 여러 슬라이스에서 케이스 리듀서를 재사용하려는 경우 createSlice 호출 외부에서 CaseReducer로 타이핑하여 정의할 수도 있습니다:

type State = number
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload

createSlice({
name: 'test',
initialState: 0,
reducers: {
increment
}
})

extraReducers 타이핑하기

createSliceextraReducers 필드를 추가하는 경우 "플레인 객체(plain object)" 형식은 액션 타입을 올바르게 추론할 수 없으므로 "빌더 콜백(builder callback)" 형식을 사용해야 합니다. RTK로 생성된 액션 생성자를 builder.addCase()에 전달하면 action의 타입을 올바르게 추론할 수 있습니다:

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// fill in primary logic here
},
extraReducers: builder => {
builder.addCase(fetchUserById.pending, (state, action) => {
// both `state` and `action` are now correctly typed
// based on the slice state and the `pending` action creator
})
}
})

prepare 콜백 타이핑하기

액션에 meta 또는 error 속성을 추가하거나 액션의 payload를 커스터마이징하려면 케이스 리듀서를 정의할 때 prepare 표기법을 사용해야 합니다. TypeScript에서 이 표기법을 사용하는 방법은 다음과 같습니다:

const blogSlice = createSlice({
name: 'blogData',
initialState,
reducers: {
receivedAll: {
reducer(
state,
action: PayloadAction<Page[], string, { currentPage: number }>
) {
state.all = action.payload
state.meta = action.meta
},
prepare(payload: Page[], currentPage: number) {
return { payload, meta: { currentPage } }
}
}
}
})

내보낸 슬라이스에서 순환 타입 수정하기

마지막으로, 아주 드물게 순환 타입 종속성 문제를 해결하기 위해 특정 타입으로 슬라이스 리듀서를 내보내야 할 수도 있습니다. 다음과 같이 할 수 있습니다:

export default counterSlice.reducer as Reducer<Counter>

createAsyncThunk 타이핑하기

기본 사용법의 경우 createAsyncThunk에 제공해야 할 타입은 페이로드 생성 콜백에 대한 단일 인수의 타입뿐입니다. 또한 콜백의 반환 값이 올바르게 타이핑되었는지 확인해야 합니다:

const fetchUserById = createAsyncThunk(
'users/fetchById',
// Declare the type your function argument here:
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
// Inferred return type: Promise<MyData>
return (await response.json()) as MyData
}
)

// the parameter of `fetchUserById` is automatically inferred to `number` here
// and dispatching the resulting thunkAction will return a Promise of a correctly
// typed "fulfilled" or "rejected" action.
const lastReturnedAction = await store.dispatch(fetchUserById(3))

getState()가 반환하는 state의 타입을 제공하는 것과 같이 thunkApi 매개변수의 타입을 수정해야 하는 경우, 반환 타입과 페이로드 인수에 대한 처음 두 제네릭 인수를 제공해야 하며, 객체에서 관련된 "thunkApi 인수 필드"를 함께 제공해야 합니다:

const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
dispatch: AppDispatch
state: State
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
})

createEntityAdapter 타이핑하기

TypeScript에서 createEntityAdapter 사용법은 엔티티가 id 속성으로 정규화되는지, 아니면 커스텀 selectId가 필요한지에 따라 다릅니다.

엔티티가 id 속성으로 정규화되는 경우 createEntityAdapter는 엔티티 타입을 단일 제네릭 인수로 지정하기만 하면 됩니다. 예를 들어:

interface Book {
id: number
title: string
}

// no selectId needed here, as the entity has an `id` property we can default to
const booksAdapter = createEntityAdapter<Book>({
sortComparer: (a, b) => a.title.localeCompare(b.title)
})

const booksSlice = createSlice({
name: 'books',
// The type of the state is inferred here
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
}
}
})

반면, 엔티티를 다른 속성으로 정규화해야 하는 경우 커스텀 selectId 함수를 전달하고 여기에 주석을 다는 것을 권장합니다. 이렇게 하면 수동으로 제공해야 하는 대신 ID 타입을 올바르게 추론할 수 있습니다.

interface Book {
bookId: number
title: string
// ...
}

const booksAdapter = createEntityAdapter({
selectId: (book: Book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title)
})

const booksSlice = createSlice({
name: 'books',
// The type of the state is inferred here
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
}
}
})

추가 권장 사항

React Redux Hooks API 사용하기

기본 접근 방식으로 React Redux hooks API 사용을 권장합니다. hooks API는 TypeScript와 함께 사용하기가 훨씬 간단합니다. useSelector는 셀렉터 함수를 받는 간단한 훅이며, 반환 타입은 state 인수의 타입으로부터 쉽게 추론될 수 있기 때문입니다.

connect는 여전히 잘 작동하고 타이핑이 가능하지만, 올바르게 타이핑하는 것이 훨씬 어렵습니다.

액션 타입 유니온 피하기

액션 타입의 유니온을 생성하려는 시도는 특히 권장하지 않습니다. 이는 실제 이점을 제공하지 않을 뿐만 아니라 어떤 면에서는 컴파일러를 오히려 오도합니다. 이 문제점에 대한 설명은 RTK 메인테이너 Lenz Weber의 글 Redux 액션 타입으로 유니온 타입을 생성하지 마세요를 참조하세요.

게다가 createSlice를 사용 중이라면, 해당 슬라이스에서 정의된 모든 액션이 올바르게 처리된다는 것을 이미 알고 있을 것입니다.

추가 자료

더 자세한 정보는 다음 추가 자료를 참조하세요: