React

타입스크립트 리액트 리덕스 사용해보기 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
Bash
복사

폴더 구조

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> ); }
TypeScript
복사
useSelector를 사용할 때마다 ReduxState와 반환값의 타입을 입력하는 게 번거로운데, ReduxState 타입이 미리 입력된 훅을 만들어서 사용하면 편하다.
> common
> useTypedSelector.ts
import { useSelector, TypedUseSelectorHook } from "react-redux"; import { ReduxState } from "./store"; const useTypedSelector: TypedUseSelectorHook<ReduxState> = useSelector; export default useTypedSelector;
TypeScript
복사
// @ : 1) ReduxState 타입과 반환값의 타입을 입력할 필요가 없다.const name = useTypedSelector((state) => state.person.name); const age = useTypedSelector((state) => state.person.age);
TypeScript
복사
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); } }); }; }
TypeScript
복사
> 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 }), };
TypeScript
복사
> 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), });
TypeScript
복사