26_타입스크립트 & 리덕스
FrontEnd/React

26_타입스크립트 & 리덕스

728x90

이번엔 타입스크립트와 리덕스를 사용해보자.

 

npx create-react-app my-app --template typescript

먼저 타입스크립트를 사용하는 리액트 프로젝트를 하나 만들어준뒤,

 

yarn add redux react-redux

redux와 react-redux 라이브러리를 설치해준다.

 

 

완성된 프로젝트의 node modules파일안에서 설치한 라이브러리를 보면

 

index.d.ts가 있는걸 볼 수 있는데 이 파일이 있다면 redux에선 타입스크립트를 지원해주는 것이다.

 

react-redux에는 따로 지원해주지 않기 때문에

 

yarn add @types/react-redux

 

이를 이용해서 typescript를 적용시킬 수 있다.

 

어떠한 라이브러리에서 타입스크립트를 지원해주는지 아닌지는 아래 사이트에서 검색하여 확인할 수 있다.

 

https://www.typescriptlang.org/dt/search?search= 

 

Search for typed packages

Find npm packages that have type declarations, either bundled or on Definitely Typed.

www.typescriptlang.org

 

 

 

먼저 카운터 예제를 한번 만들어보자.

 

src파일 내에 modules폴더를 만든 후에, counts.ts파일을 생성해주자.

 

//counter.ts

/*액션 */
const INCREASE = "counter/INCREASE" as const; //as const를 적음으로써 자료형이 string이 아닌 글자 그대로 들어가게 된다.
const DECREASE = "counter/DECREASE" as const;
const INCREASE_BY = "counter/INCREASE_BY" as const;

/* 액션 생성함수 */
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
  type: INCREASE_BY,
  payload: diff,
});

type CounterState = {
  count: number;
};

const initailState = {
  count: 0,
};

type CounterAction =  //카운터 액션 타입설정
  | ReturnType<typeof increase> //CounterAction에 마우스를 가져다대면 타입이 잘 들어가있음을 알 수 있다
  | ReturnType<typeof decrease> //함수의 결과물의 타입을 가져오게 됨
  | ReturnType<typeof increaseBy>;

function counter(
  state: CounterState = initailState,
  action: CounterAction
): CounterState {
  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;

액션과 액션생성함수를 만들고 이전에 리덕스를 만들었던 것처럼 작성해준다.

 

그후, modules 안에 index.ts를 만들고

 

//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를 만들어준다.

 

 

그 후, index.tsx에

 

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

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 store를 하나 만들어주고 Provider로 감싸주면 리덕스를 사용할 준비가 완료된다.

 

 

이제 컴포넌트와 컨테이너를 간단하게 작성해보자.

 

 

 

더보기
//Counter.tsx

import React from "react";

type CounterProps = {
  count: number;
  onIncrease: () => void;
  onDecrease: () => void;
  onIncreaseBy: (diff: number) => void;
};

function Counter({
  count,
  onIncrease,
  onDecrease,
  onIncreaseBy,
}: CounterProps) {
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
      <button onClick={() => onIncreaseBy(5)}>+5</button>
    </div>
  );
}

export default Counter;

 

//CounterContainer.tsx

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

function CounterContainer() {
  const count = useSelector((state: RootState) => state.counter.count); //상태 조회
  const dispatch = useDispatch();

  const onIncrease = () => {
    dispatch(increase());
  };

  const onDecrease = () => {
    dispatch(decrease());
  };

  const onIncreaseBy = (diff: number) => {
    dispatch(increaseBy(diff));
  };

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

export default CounterContainer;

 

 

그후 프로젝트 서버를 열면

 

잘 되는것을 볼 수 있다.

 

역시 이전에 리덕스를 사용할 때와 크게 다른건없지만 타입들을 일일히 지정해주는것이 다르다고 볼 수 있다. 어떠한 자료를 볼때 그 type을 마우스만 올리면 볼 수 있기에 코드작성에 실수도 적어지지만 이해하기도 쉬워진다.

 

 

 

이번엔 Todo리스트를 한번 만들어보자.

 

//modules/todos.ts

const ADD_TODO = "todos/ADD_TODO" as const;
const TOGGLE_TODO = "todos/TOGGLE_TODO" as const;
const REMOVE_TODO = "todos/REMOVE_TODO" as const;

let nextId = 1;

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

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

export const removeTodo = (id: number) => ({
  type: REMOVE_TODO,
  payload: id,
});

type TodoAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof toggleTodo>
  | ReturnType<typeof removeTodo>;

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const initialState: TodosState = [];

/* 리듀서 */
function todos(
  state: TodosState = initialState,
  action: TodoAction
): TodosState {
  switch (action.type) {
    case ADD_TODO:
      return state.concat({
        id: action.payload.id,
        text: action.payload.text,
        done: false,
      });
    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);
    default:
      return state;
  }
}

export default todos;

 

TodoList기능을 하는 코드를 modules안에 작성해준후에,

 

//modules/index.ts
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

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

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

rootReducer에 추가시켜준다.

 

 

이제 컴포넌트들을 만들어야 한다.

 

투두 리스트의 입력을 받는용도, 리스트 개개인이 완료된목록인지 아닌지 판별해서 출력하는 용도, 리스트들을 쭉 출력하는용도로 총 3개의 컴포넌트를 만들어야 한다.

 

//Todoinsert.tsx

import React, { useState } from "react";

type TodoInsertProps = {
  onInsert: (text: string) => void;
};

function TodoInsert({ onInsert }: TodoInsertProps) {
  const [value, setValue] = useState("");
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onInsert(value);
    setValue("");
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        placeholder="할 일을 입력하세요."
        value={value}
        onChange={onChange}
      />
      <button type="submit">등록</button>
    </form>
  );
}

export default TodoInsert;
//TodoItem.tsx
import React, { CSSProperties } from "react";
import { Todo } from "../modules/todos";

type TodoItemProps = {
  todo: Todo;
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
};

function TodoItem({ todo, onToggle, onRemove }: TodoItemProps) {
  const handleToggle = () => onToggle(todo.id);
  const handleRemove = () => onRemove(todo.id);

  const textStyle: CSSProperties = {
    textDecoration: todo.done ? "line-through" : "none",
  };
  const removeStyle: CSSProperties = {
    color: "red",
    marginLeft: 8,
  };

  return (
    <li>
      <span onClick={handleToggle} style={textStyle}>
        {todo.text}
      </span>
      <span onClick={handleRemove}>(X)</span>
    </li>
  );
}

export default TodoItem;
//Todolist.tsx
import React from "react";
import { Todo } from "../modules/todos";
import TodoItem from "./TodoItem";

type TodoListProps = {
  todos: Todo[];
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
};

function TodoList({ todos, onToggle, onRemove }: TodoListProps) {
  if (todos.length === 0) return <p>등록된 사용자가 없음</p>;
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          todo={todo}
          onToggle={onToggle}
          onRemove={onRemove}
          key={todo.id} //비효율적인 랜더링 방지
        />
      ))}
    </ul>
  );
}

 

 

이제 ContainerComponent도 만들어주자.

 

//TodoApp.tsx

import React from "react";
import { useDispatch, useSelector } from "react-redux";
import TodoInsert from "../components/TodoInsert";
import TodoItem from "../components/TodoItem";
import TodoList from "../components/TodoList";
import { RootState } from "../modules";
import { addTodo, removeTodo, toggleTodo } from "../modules/todos";

function TodoApp() {
  const todos = useSelector((state: RootState) => state.todos); // 상태조회
  const dispatch = useDispatch(); //dispatch사용

  const onInsert = (text: string) => {
    dispatch(addTodo(text));
  };

  const onToggle = (id: number) => {
    dispatch(toggleTodo(id));
  };

  const onRemove = (id: number) => {
    dispatch(removeTodo(id));
  };

  return (
    <>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
    </>
  );
}

export default TodoApp;

만들어둔 것들을 합쳐주기만 하면 된다!

 

그이후 App.tsx에서 위 컨테이너를 불러오면

 

 

잘 작동하는것을 볼 수 있다 ㅎㅎ

 

이전에 만들었던 counter예제 밑에 출력되게 했다.

 

 

 

 

 

typesafe-actions

 

해당 라이브러리를 사용하면 액션생성함수와 리듀서를 훨씬 깔끔하게 만들 수 있다.

 

https://www.npmjs.com/package/typesafe-actions

 

typesafe-actions

Typesafe Action Creators for Redux / Flux Architectures (in TypeScript)

www.npmjs.com

라이브러리에 대한 정보는 위 링크를 참조하면 된다.

 

그러면 우선 만들어본 counter예제를 해당 라이브러리를 이용하여 리팩토링 해보자!

 

yarn add typesafe-actions

먼저 해당 라이브러리를 설치해준다.

 

 

그 다음 라이브러리 문법에 맞추어서 아래와 같이 리팩토링 해주면 된다. 주석된 부분을 아래 부분으로 바꾼 것이며 훨씬 코드가 간결해진 것을 볼 수 있다.

 

필자가 공부한 코드들은 typesafe-actions이전 버전인데, 이때는 createStandardAction이 typesafe-actions안에 바로 있어서 import가 가능했지만 지금은 deprecated로 바뀌어버려서 아래 코드를 수행하기 위해서 함수이름을 바꾸어주는 과정을  첫줄에 진행해 주었다!

//counter.ts
import { deprecated, ActionType, createReducer } from "typesafe-actions";
const { createStandardAction } = deprecated;
// /*액션 */
// const INCREASE = "counter/INCREASE"  as const 
// const DECREASE = "counter/DECREASE"  as const
// const INCREASE_BY = "counter/INCREASE_BY"  as const
const INCREASE = "counter/INCREASE"; // as const불필요
const DECREASE = "counter/DECREASE";
const INCREASE_BY = "counter/INCREASE_BY";

/* 액션 생성함수 */
// export const increase = () => ({ type: INCREASE });
// export const decrease = () => ({ type: DECREASE });
// export const increaseBy = (diff: number) => ({
//   type: INCREASE_BY,yar
//   payload: diff,
// });
export const increase = createStandardAction(INCREASE)();
export const decrease = createStandardAction(DECREASE)();
export const increaseBy = createStandardAction(INCREASE_BY)<number>();

type CounterState = {
  count: number;
};

const initialState = {
  count: 0,
};

// type CounterAction =  //카운터 액션 타입설정
//   | ReturnType<typeof increase> //CounterAction에 마우스를 가져다대면 타입이 잘 들어가있음을 알 수 있다
//   | ReturnType<typeof decrease> //함수의 결과물의 타입을 가져오게 됨
//   | ReturnType<typeof increaseBy>;
const actions = { increase, decrease, increaseBy };
type CounterAction = ActionType<typeof actions>;

// function counter(
//   state: CounterState = initailState,
//   action: CounterAction
// ): CounterState {
//   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;
//   }
// }
const counter = createReducer<CounterState, CounterAction>(initialState, {
  [INCREASE]: (state) => ({ count: state.count + 1 }),
  [DECREASE]: (state) => ({ count: state.count - 1 }),
  [INCREASE_BY]: (state, action) => ({ count: state.count + action.payload }),
});

export default counter;

 

코드가 훨씬 단순해졌다. Todo예제도 한번 리팩토링 해보자.

 

//modules/todos.ts
import { create } from "domain";
import { deprecated, ActionType, createReducer } from "typesafe-actions";
const { createStandardAction, createAction } = deprecated;

const ADD_TODO = "todos/ADD_TODO" as const;
const TOGGLE_TODO = "todos/TOGGLE_TODO";
const REMOVE_TODO = "todos/REMOVE_TODO";
let nextId = 1;

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

export const toggleTodo = createStandardAction(TOGGLE_TODO)<number>();
export const removeTodo = createStandardAction(REMOVE_TODO)<number>();

const actions = { addTodo, toggleTodo, removeTodo };

type TodosAction = ActionType<typeof actions>;

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodosState = Todo[];

const initialState: TodosState = [];

/* 리듀서 */
const todos = createReducer<TodosState, TodosAction>(initialState, {
  [ADD_TODO]: (state, action) =>
    state.concat({
      ...action.payload,
      done: false,
    }),
  [TOGGLE_TODO]: (state, action) =>
    state.map((todo) =>
      todo.id === action.payload ? { ...todo, done: !todo.done } : todo
    ),
  [REMOVE_TODO]: (state, action) =>
    state.filter((todo) => todo.id !== action.payload),
});

export default todos;

counter예제를 리팩토링한것과 똑같은 방식이다.

 

 

 

만약 액션의 개수가 많아지거나 하면 파일을 관리하기가 힘들어진다. 이를 대비해서 리덕스 모듈을 여러 파일로 분리하여 다루는 법을 알아보자.

 

위에서 만든 코드를 3개로 쪼개어 볼 것이다.

 

//types.ts
import { ActionType } from "typesafe-actions";
import * as actions from "./actions";

export type TodosAction = ActionType<typeof actions>;
//actions들의 type들이 모두 뱉어진다.

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

export type TodosState = Todo[];
//reducer.ts
import { createReducer } from "typesafe-actions";
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from "./actions";
import { TodosAction, TodosState } from "./types";

const initialState: TodosState = [];

const todos = createReducer<TodosState, TodosAction>(initialState, {
  [ADD_TODO]: (state, action) =>
    state.concat({
      ...action.payload,
      done: false,
    }),
  [TOGGLE_TODO]: (state, action) =>
    state.map((todo) =>
      todo.id === action.payload ? { ...todo, done: !todo.done } : todo
    ),
  [REMOVE_TODO]: (state, action) =>
    state.filter((todo) => todo.id !== action.payload),
});

export default todos;
//actions.ts
import { deprecated, ActionType, createReducer } from "typesafe-actions";
const { createStandardAction, createAction } = deprecated;

export const ADD_TODO = "todos/ADD_TODO" as const;
export const TOGGLE_TODO = "todos/TOGGLE_TODO";
export const REMOVE_TODO = "todos/REMOVE_TODO";

let nextId = 1;

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

export const toggleTodo = createStandardAction(TOGGLE_TODO)<number>();
export const removeTodo = createStandardAction(REMOVE_TODO)<number>();

 

3개로 나눈것을 index.ts에 모아준다

 

//modules/todos/index.ts

export { default } from "./reducer"; //reducer에서 내보낸걸 그대로 내보냄 (default로)
export * from "./actions"; //actions의 모든걸 내보냄,
export * from "./types";

이렇게 내보내는 이유는 이 index.ts에서 내보내야 다른 파일에서 import한 부분들을 다 수정하지 않아도 되기 때문이다.

 

즉, actions 의 ADD_TODO를 사용하기 위해서 /modules/todos/action을 import할 필요없이 이전대로 /modules/todos 만 import해줘도 자동으로 index.ts가 호출되기 때문이다.

 

 

이제 타입스크립트로 리덕스를 사용하는 방법에 알아보았다. 다음은 리덕스 미들웨어를 타입스크립트에 적용시켜 보자.

728x90

'FrontEnd > React' 카테고리의 다른 글

[React] .env 사용하기  (0) 2022.09.30
27_타입스크립트 & 리덕스 미들웨어  (0) 2022.01.10
25_타입스크립트 & 리액트  (0) 2022.01.07
23_리액트 리덕스 미들웨어(2)  (0) 2022.01.05
22_리덕스 미들웨어  (0) 2022.01.05