[react] Redux Toolkit 사용 가이드(TypeScript)

sue·2021년 4월 29일
16

react note

목록 보기
17/17

Redux Toolkit 공식문서를 개인 학습용으로 정리한 글입니다.

  • 배울 것
    TypeScript와 함께 각 Redux Toolkit API를 사용하는 방법에 대한 세부 정보

소개

Redux Toolkit은 TypeScript로 작성되었으며 API는 TypeScript 애플리케이션과의 뛰어난 통합을 가능하게하도록 설계되었습니다.

configureStore

State 타입 얻기

State 타입을 얻는 가장 쉬운 방법은 루트리듀서를 미리 정의하고 ReturnType 타입을 추출하는 것입니다. State 라는 이름은 일반적으로 과도하게 사용되므로 혼동을 방지하기 위해 RootState와 같이 다른 이름을 지정하는 것이 좋습니다.

import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>

루트 리듀서를 생성하지 않는다면 다음과 같은 방식 사용

import { configureStore } from '@reduxjs/toolkit'
// ...
const store = configureStore({
  reducer: {
    one: oneSlice.reducer,
    two: twoSlice.reducer,
  },
})
export type RootState = ReturnType<typeof store.getState>

export default store

Dispatch 타입 얻기

스토어에서 Dispatch 타입을 가져 오려면 스토어를 작성한 후 추출 할 수 있습니다. Dispatch은 일반적으로 과도하게 사용 되므로 혼동을 방지하기 위해 AppDispatch와 같이 다른 이름을 지정하는 것이 좋습니다. 또한 아래 useAppDispatch 같이 훅을 내보낸 다음 호출 할 때마다 useDispatch를 불러 사용하는 것이 더 편리 할 수도 있습니다

import { configureStore } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import rootReducer from './rootReducer'

const store = configureStore({
  reducer: rootReducer,
})

export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook that can be reused to resolve types

export default store

Dispatch 타입에 대한 올바른 입력

dispatch 함수의 타입은 middleware 옵션에 따라 직접 유추됩니다. 따라서 올바르게 typed된 미들웨어를 추가하면 dispatch 이미 올바르게 typed되어 있습니다.

TypeScript는 스프레드 연산자를 사용하여 배열을 결합 할 때 배열 타입을 확장하는 경우가 많으므로 getDefaultMiddleware()에서 반환된 MiddlewareArray.concat(...).prepend(...) 메서드를 사용하는 것이 좋습니다.

또한 제네릭을 직접 지정할 필요가 없는 올바르게 사전 타입화된 getDefaultMiddleware 버전을 가져 오는 middleware 옵션에 대한 콜백 표기법을 사용하는 것이 좋습니다.

export type RootState = ReturnType<typeof rootReducer>
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),
})

export type AppDispatch = typeof store.dispatch

export default store

React Redux에서 추출된 Dispatch 타입 사용

기본적으로 React Redux useDispatch 훅에는 미들웨어를 고려하는 타입이 없습니다. 디스패치 할 때 dispatch 함수에 대해 더 구체적인 타입이 필요한 경우, 반환된 dispatch 함수의 타입을 지정 하거나 useSelector의 사용자 정의 타입 버전을 만들 수 있습니다 자세한 내용 은 React Redux 문서를 참조하십시오.

CreateSlice

createSlice는 당신을 위해 당신의 액션과 리듀서를 생성하기 때문에 여기서 타입 안전성에 대해 걱정할 필요가 없습니다. 액션 타입은 인라인으로 제공 할 수 있습니다.

const slice = createSlice({
  name: 'test',
  initialState: 0,
  reducers: {
    increment: (state, action: PayloadAction<number>) => state + action.payload,
  },
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })

케이스 리듀서가 너무 많고 인라인으로 정의하는 것이 지저분하거나 슬라이스간에 케이스 리듀서를 재사용하려는 경우, createSlice 외부에서 정의하고 다음과 같이 CaseReducer로 입력할 수도 있습니다

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

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

초기 상태 타입 정의

SliceState 타입을 createSlice처럼 제네릭으로 전달하는 것이 좋지 않다는 것을 알았을 것입니다. 이는 거의 모든 경우에 createSlice의 후속 제네릭 매개 변수를 추론해야 하며, TypeScript는 동일한 "제네릭 블록" 내에서 제네릭 타입의 명시적 선언과 추론을 혼합할 수 없기 때문입니다.

표준 접근 방식은 상태에 대한 인터페이스 또는 타입을 선언하고 해당 타입을 사용하는 초기 상태 값을 만들고 초기 상태 값을 createSlice에 전달하는 것입니다. initialState: myInitialState as SliceState. 구성을 사용할 수도 있습니다.

type SliceState = { state: 'loading' } | { state: 'finished'; data: string }

// First approach: define the initial state using that type
// 첫 번째 접근 방식 : 해당 타입을 사용하여 초기 상태 정의
const initialState: SliceState = { state: 'loading' }

createSlice({
  name: 'test1',
  initialState, // type SliceState is inferred for the state of the slice 
  // 타입 SliceState는 슬라이스 상태에 대해 유추됩니다. 
  reducers: {},
})

// Or, cast the initial state as necessary
// 또는 필요에 따라 초기 상태를 캐스팅
createSlice({
  name: 'test2',
  initialState: { state: 'loading' } as SliceState,
  reducers: {},
})

결과는 Slice<SliceState, ...>.

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 } }
      },
    },
  },
})

슬라이스에 대해 생성된 액션 타입

TS는 두 개의 문자열 리터럴 (slice.nameactionMap의 키)을 새 리터럴로 결합 할 수 없으므로 createSlice에서 생성된 모든 액션 생성자는 '문자열' 타입입니다. 이러한 타입은 리터럴로 거의 사용되지 않으므로 일반적으로 문제가 되지 않습니다.

type 리터럴로 필요한 대부분의 경우 slice.action.myAction.match 타입 참조는 실행 가능한 대안이어야 합니다.

const slice = createSlice({
  name: 'test',
  initialState: 0,
  reducers: {
    increment: (state, action: PayloadAction<number>) => state + action.payload,
  },
})

function myCustomMiddleware(action: Action) {
  if (slice.actions.increment.match(action)) {
    // `action` is narrowed down to the type `PayloadAction<number>` here.
    // 여기서`action`은`PayloadAction <number>`타입으로 좁혀집니다.
  }
}

실제로 해당 타입이 필요한 경우 불행히도 수동 캐스팅 외에 다른 방법은 없습니다.

createAsyncThunk

일반적인 사용 사례에서는 createAsyncThunk 호출 자체에 대해 어떤 타입도 명시적으로 선언 할 필요가 없습니다.

함수 인수처럼 payloadCreator 인수의 첫 번째 인수에 대한 타입을 제공하면, 결과 썽크가 입력 매개 변수와 동일한 타입을 허용합니다. payloadCreator의 반환 타입은 생성된 모든 액션 타입에도 반영됩니다.

interface MyData {
  // ...
}

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))

thunkApi로 알려진 payloadCreator의 두번째 인수는, dispatch, getStateextra 썽크 미들웨어뿐만 아니라 rejectWithValue라는 유틸리티 함수의 인수를 포함하는 객체입니다.payloadCreator 안에서 이들을 사용하려면, 이러한 인수의 타입을 유추 할 수 없으므로 일부 일반 인수를 정의해야 합니다. 또한 TS는 명시적 매개 변수와 추론된 일반 매개 변수를 혼합할 수 없기 때문에 이 시점부터 ReturnedThunkArg 일반 매개 변수도 정의해야합니다.

이러한 인수의 유형을 정의하려면 다음 필드의 일부 또는 전체에 대한 타입 선언과 함께 객체를 세 번째 일반 인수로 전달합니다.
{dispatch?, state?, extra?, rejectValue?}..

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
})

당신은 요청을 수행하는 경우에 일반적으로 성공하거나, 알고 있거나 예상되는 오류 형식을 가지고 rejectValue 타입에 전달 혹은 액션 생성자에서 rejectWithValue(knownPayload)을 반환합니다. 이를 통해 createAsyncThunk 액션을 디스패치 한 후 컴포넌트 뿐만 아니라 리듀서에서 오류 페이로드를 참조 할 수 있습니다.

interface MyKnownError {
  errorMessage: string
  // ...
}
interface UserAttributes {
  id: string
  first_name: string
  last_name: string
  email: string
}

const updateUser = createAsyncThunk<
  // Return type of the payload creator
  MyData,
  // First argument to the payload creator
  UserAttributes,
  // Types for ThunkAPI
  {
    extra: {
      jwt: string
    }
    rejectValue: MyKnownError
  }
>('users/update', async (user, thunkApi) => {
  const { id, ...userData } = user
  const response = await fetch(`https://reqres.in/api/users/${id}`, {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${thunkApi.extra.jwt}`,
    },
    body: JSON.stringify(userData),
  })
  if (response.status === 400) {
    // Return the known error for future handling
    return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
  }
  return (await response.json()) as MyData
})

0개의 댓글