타입스크립트 리액트 리덕스 사용해보기 Try TypeScript React Redux

# 시작

<실전 리액트 프로그래밍> 리덕스에서 타입스크립트를 사용하는 실습을 따라해본다.

# 실습

npx create-react-app ts-redux --template typescript
cd ts-redux
npm install react react-dom redux react-redux immer
npm install @types/react @types/react-dom @types/react-redux

폴더 구조

src
> App.tsx
> common
> redux.ts
> store.ts
> useTypedSelector.ts
> index.ts
> person
> component
> Person.tsx
> state
> action.ts
> reducer.ts
> product
>component
> Product.tsx
> state
> action.ts
> reducer.ts
> person
> component
> Person.tsx
import React from "react";
import { ReduxState } from "../../common/store";
import { actions } from "../state/action";
import { useSelector, useDispatch } from "react-redux";

interface Props {
    birthday: string;
}

export default function Person({ birthday }: Props) {
// @ : 1) 첫번째 제네릭 타입은 리덕스의 상탯값을 의미한다. 두번째 제네릭 타입은 매개변수로 입력된 함수의 반환값const name = useSelector<ReduxState, string>((state) => state.person.name);
    const age = useSelector<ReduxState, string>((state) => state.person.age);
    const dispatch = useDispatch();
    function onClick() {
        dispatch(actions.setName("mike"));
        dispatch(actions.setAge(23));
    }

    return (
        <div><p>{name}</p><p>{age}</p><p>{birthday}</p><button onClick={onClick}>정보 추가하기</button></div>
    );
}
useSelector를 사용할 때마다 ReduxState와 반환값의 타입을 입력하는 게 번거로운데, ReduxState 타입이 미리 입력된 훅을 만들어서 사용하면 편하다.
> common
> useTypedSelector.ts
import { useSelector, TypedUseSelectorHook } from "react-redux";
import { ReduxState } from "./store";

const useTypedSelector: TypedUseSelectorHook<ReduxState> = useSelector;
export default useTypedSelector;
// @ : 1) ReduxState 타입과 반환값의 타입을 입력할 필요가 없다.const name = useTypedSelector((state) => state.person.name);
    const age = useTypedSelector((state) => state.person.age);
createAction 함수와 createReducer 함수 정의
> common
> redux.ts
import produce from "immer";
// @ : 1) 액션 객체의 타입, 데이터 있는, 없는 경우로 2개
interface TypedAction<T extends string> {
    type: T;
}

interface TypedPayloadAction<T extends string, P> extends TypedAction<T> {
    payload: P;
}
// @ : 2) 액션 생성자 함수의 타입, 데이터 유무 구별을 위해 오버로드 사용export function createAction<T extends string>(type: T): TypedAction<T>;
export function createAction<T extends string, P>(
    type: T,
    payload: P
): TypedPayloadAction<T, P>;
// @ts-ignoreexport function createAction(type, payload?) {
    return payload !== undefined ? { type, payload } : { type };
}
// @ : 3) 리듀서 생성 함수의 타입, S: 상탯값 타입, T: 액션 타입, A: 모든 액션 객체의 유니온 타입export function createReducer<S, T extends string, A extends TypedAction<T>>(
  // @ : 4) 초기 상탯값을 첫 번째 매개변수
    initialState: S,
    // @ : 5) 모든 액션 처리함수가 담긴 객체를 두 번째 매개변수
    handlerMap: {
// @ : 6) 각 액션 객체가 가진 payload타입을 알 수 있게 됨
        [key in T]: (
            state: Draft<S>,
            action: Extract<A, TypedAction<key>>
        ) => void;
    }
) {
    return function (
        state: S = initialState,
        action: Extract<A, TypedAction<T>>
    ) {
// @ : 7) 이머를 통해 불변 객체를 쉽게 다룰 수 있다.return produce(state, (draft) => {
// @ : 8) 입력된 액션에 해당하는 액션 처리 함수 실행const handler = handlerMap[action.type];
            if (handler) {
                handler(draft, action);
            }
        });
    };
}
> person
> state
> action.ts
import { createAction } from "../../common/redux";
// @ : 1) enum으로 액션 타입 정의export enum ActionType {
    SetName = "person_setName",
    SetAge = "person_setAge",
}
// @ : 2) createAction 함수를 이용해 액션 생성자 함수 정의export const actions = {
    SetName: (name: string) => createAction(ActionType.SetName, { name }),
    SetAge: (age: number) => createAction(ActionType.SetAge, { age }),
};
> person
> state
> reducer.ts
import { ActionType, actions } from "./action";
import { createReducer } from "../../common/redux";

// @ : 1) 인터페이스로 상탯값 타입 정의export interface StatePerson {
    name: string;
    age: number;
}
// @ : 2) 초기 상탯값 정의const INITIAL_STATE = {
    name: "empty",
    age: 0,
};
// @ : 3) ReturnType 내장 타입을 이용해 모든 액션 객체 타입을 유니온 타입으로 만듦
type Action = ReturnType<typeof actions[keyof typeof actions]>;
// @ : 4) createReducer로 리듀서를 만든다. 모든 타입을 제네릭으로export default createReducer<StatePerson, ActionType, Action>(INITIAL_STATE, {
// @ : 5) action.payload가 SetName 액션 객체의 데이터라는 걸 알고 있음
    [ActionType.SetName]: (state, action) => (state.name = action.payload.name),
    [ActionType.SetAge]: (state, action) => (state.age = action.payload.age),
});