# 시작
<실전 리액트 프로그래밍> 리덕스에서 타입스크립트를 사용하는 실습을 따라해본다.
# 실습
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),
});