[21장] 타입스크립트 in React (Redux)

2022. 4. 16. 17:09책/리액트를 다루는 기술

리덕스에 타입스크립트를 고-급지게 넣는 방법을 알아보자. 리덕스의 흐름은 아래와 같다.

  • 액션 타입을 선언하고 / 액션 생성 함수를 정의한 뒤 / 액션을 받아서 실제로 작업해주는 리듀서를 작성한다
  • 리듀서가 여러개라면 루트리듀서라는 이름 하에 하나로 통합해준다 (combineReducers)
  • 상태를 관리해줄 스토어를 선언한다 (createStore)
  • 스토어를 뿌려준다 (Provide)
  • 리덕스와 연동할 컨테이너를 만들고 스토어와 연결해서 state와 dispatch를 가져온다(useSelector, useDispatch)
  • 가져온걸로 지지고볶고 한다

1. 카운트

뭐만하면 자꾸 나오는 이 창을 구현해봅시다

1) 액션 및 리듀서 정의(modules/counter.ts)

// 액션 타입
const INCREASE = 'counter/INCREASE' as const; // as const로 assertion 하는 이유는
const DECREASE = 'counter/DECREASE' as const; // 비록 const로 선언한 변수일 지라도
const INCREASE_BY = 'counter/INCREASE_BY' as const; // 객체안에 넣으면 타입추론은 값을 기준으로 처리되기 때문

ex) const a = 'sexy' ← 얘는 type이 sexy인 변수이나
    const obj = { '우흥' : a } ← obj의 '우흥' type은 string으로 추론됨

// 액션 생성 함수
export function increase() {   // 그러니까 위에서 as const를 써준 이유는
    return { type: INCREASE }; // 이 객체에서 type이 string으로 추론되길 원한게 아니라 
}                              // 'counter/INCREASE' ← 이 자체가 type이 되길 원한다는 거임

export function decrease() {
    return { type: DECREASE };
}
export function increaseBy(diff :number) {
    return {
        type: INCREASE_BY,
        payload: diff
    };
}
// 관리될 상태
type CounterState = { count :number };
const initialState :CounterState = {
    count: 0
};

type CounterAction = 
    | ReturnType<typeof increase> // ReturnType<T> : 함수의 반환값이 가지는 type을 퉤 뱉어줌
    | ReturnType<typeof decrease> // 그러니까 지금 얘는 { type: DECREASE } 를 뱉어낼거임
    | ReturnType<typeof increaseBy>;

// 리듀서
function counter(
    state :CounterState = initialState, 
    action :CounterAction) {
    switch (action.type) {
        case INCREASE:
            return { count: state.count + 1 };
        case DECREASE:
            return { count: state.count -1 };
        case INCREASE_BY:
            return { count: state.count + action.payload }
        default:
            return state;
    }
};

export default counter;

 

2) 리듀서 통합(modules/index.ts)

import { combineReducers } from "redux";
import counter from './counter';

const rootReducer = combineReducers({
    counter,
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;
// rootReducer의 type을 굳이 따로 뽑아내서 export까지 시켜주는 이유는
// 나중에 컨테이너에서 state의 타입을 지정할 때 필요하기 때문
// 좀만 내려보면 나올거임
// 근데 rootReducer 함수가 반환하는 타입이 뭐냐구요?
// 어렵게 생각할거 없음. 
// 리듀서란게 결국 액션 종류에따라 state를 조금씩 바꿔서 퉤 뱉어주잖아?
// 그러니까 좀 변하긴 했어도 결국은 state를 뱉어내는 함수가 리듀서인거고
// 그 리듀서들을 하나로 합친것뿐 본질이 변한건 아니기 때문에 결국 리듀서마냥
// 좀 변한 state를 내뱉어주는 것 뿐임

 

3) 스토어 생성 / 배포

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>
);

reportWebVitals();

4) 컨테이너 연결

import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../modules";
import { increase, decrease, increaseBy } from '../modules/counter';
import Counter from "../components/Counter";

function CounterContainer() {
	// 바로 여기 state꺼내올 때 아까 타입선언한 RootState가 들어가는거임
    const count = useSelector((state :RootState) => state.counter.count);
    
    const dispatch = useDispatch();

    const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
    const onIncreaseBy = useCallback((diff :number) => dispatch(increaseBy(diff)), [dispatch]);

    return(
        <Counter 
            count={count} 
            onIncrease={onIncrease}
            onDecrease={onDecrease}
            onIncreaseBy={onIncreaseBy}
        />
    );
}

export default CounterContainer;

 

2. Todo리스트 (도대체 왜 모든 강의는 todo 리스트를 못해서 안달임?)

모든 코드를 다 작성하진 않겠음. 중요한건 리덕스의 흐름이고, 이것만 알면 필요한 부분은 직접 스스로 생각해보고 구현해보길 바람.

1) 액션 및 리듀서 정의

// 초기상태
type Todo = {
    id :number,
    text :string,
    done :boolean
};

const initialState :Todo[] = [];

let nextId = 1;

// 액션 타입
const TODO_ADD = 'todo/ADD' as const;
const TODO_TOGGLE = 'todo/TOGGLE' as const;
const TODO_DELETE = 'todo/DELETE' as const;

// 액션 생성 함수
type ActionTodo = 
    | ReturnType<typeof addTodo>
    | ReturnType<typeof toggleTodo>
    | ReturnType<typeof deleteTodo>


export const addTodo = (input :string) => ({
    type: TODO_ADD,
    payload: {
        id: nextId++,
        input
    }
});

export const toggleTodo = (id :number) => ({
    type: TODO_TOGGLE,
    payload: id
})

export const deleteTodo = (id :number) => ({
    type: TODO_DELETE,
    payload: id
});

// 리듀서
function todo(
    state :Todo[] = initialState, 
    action :ActionTodo) :Todo[]
    {
        switch (action.type) {
            case TODO_ADD:
                return [...state, {
                    id: action.payload.id,
                    text: action.payload.input,
                    done: false
                }];
            case TODO_TOGGLE:
                return state.map(todo => 
                    todo.id === action.payload
                    ? { ...todo, done: !todo.done }
                    : todo);
            case TODO_DELETE:
                return state.filter(todo => todo.id !== action.payload);
            default:
                return state;
        }      
};

export default todo;

2) 리듀서 통합

import { combineReducers } from "redux";
import counter from './counter';
import todo from "./todo";

const rootReducer = combineReducers({
    counter,
    todo
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

3) 스토어 생성 / 배포

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>
);

reportWebVitals();

4) 컨테이너 연결

import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../modules";
import { addTodo, toggleTodo, deleteTodo } from "../modules/todo"

function TodoContainer() {
    const todoList = useSelector((state :RootState) => state.todo);
    const dispatch = useDispatch();

    const onInsert = useCallback((text :string) => dispatch(addTodo(text)), [dispatch]);
    const onToggle = useCallback((id :number) => dispatch(toggleTodo(id)), [dispatch]);
    const onDelete = useCallback((id :number) => dispatch(deleteTodo(id)), [dispatch]);

}

export default TodoContainer;