TypeScript์ ๊ธฐ์ด๋ฅผ ์์๋ณด์๊ณ Redux์ ์ฌ์ฉ๋ฒ๋ ์์์ผ๋ TypeScript์ Redux๋ฅผ ์ฌ์ฉํ์ฌ Todo-List๋ฅผ ๋ง๋ค์ด๋ณด์
์ด ๊ฒ์๊ธ์์๋ TypeScriptํ๊ฒฝ์์ Redux๋ฅผ ๋์ฑ ํธํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด typesafe-actions ๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ
์ฌ์ฉํ๋ค Redux์์๋ redux-actions๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ง๋ง TypeScript ํ๊ฒฝ์์๋ ์ฌ์ฉํ๊ธฐ ์ข์ง ์๋ค

ํ๋ก์ ํธ ๊ตฌ์กฐ
์ด ํ๋ก์ ํธ๋ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ๋ถ๋ถ Container ์ปดํฌ๋ํธ์ ํ๋ฉด์ ํํํ๋ ๋ถ๋ถ Presentational ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ๋ถํ์ฌ ๊ฐ๋ฐํ์๋ค. ์๋๋ฅผ ํตํด ์ดํด๋ณด์

components ํด๋์๋ Presentational ์ปดํฌ๋ํธ๋ค์ด , containers ํด๋์๋ container ์ปดํฌ๋ํธ, ๊ทธ๋ฆฌ๊ณ modules ํด๋์๋ ๋ฆฌ๋์ค ๋ชจ๋์ ๋ฃ๋ ๊ตฌ์กฐ๋ก ์งํํ๋ค.
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
redux ๋ฅผ ์ค์นํ๋ฉด TypeScript๊ฐ ์์ฒด์ ์ผ๋ก ์ง์๋์ง๋ง react-redux์ ๊ฒฝ์ฐ์๋ ์ง์์ด ๋์ง ์๊ธฐ๋๋ฌธ์ ํจํค์ง๋ช ์์
@types ๋ฅผ ๋ถ์ฌ์ ํจํค์ง๋ฅผ ์ค์นํด์ผ ํ๋ค
$ yarn create-react-app ts-react-app TS-Redux-Todo --typescript
$ cd TS-Redux-Todo
$ yarn add redux react-redux @types/react-redux
@types ๋ TypeScript๋ฅผ ์ง์ํ์ง์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ TypeScript๋ฅผ ์ง์ ๋ฐ์ ์ ์๊ฒ ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค
์ด์ typesafe-actions ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํด๋ณด์
$ yarn add typesafe-actions
๋ฆฌ๋์ค ๋ชจ๋ ์์ฑํ๊ธฐ
์ฐ์ ์ฝ๋์ ์ต์๋จ์ ์๋์ ๊ฐ์ ์ ํธ ํจ์ ๋ฐ ํ์ ์ ๋ถ๋ฌ์ค์
import {
ActionType,
createAction,
createReducer
} from 'typesafe-actions';
Interface ์ค์ ํ๊ธฐ
interface TodoItemDataParams {
id: number;
text: string;
done: boolean;
}
interface ToDosState {
todoItems: TodoItemDataParams[];
}
๋ฆฌ๋์์์ ์ก์ ์ ํ์ด๋ก๋๋ initialState์ ํ์ ์ ์ค์ ํ๊ธฐ ์ํด interface๋ฅผ ์ค์ ํด์ค๋ค.
์ก์ ํ์ ์ ์ธ
const SUBMIT = 'todo/SUBMIT';
const REMOVE = 'todo/REMOVE';
const TOGGLE = 'todo/TOGGLE';
typesafe-actions๋ฅผ ์ฌ์ฉํ์ง ์์๋ค๋ฉด const INCREASE = 'counter/INCREASE' as const; ์ ๊ฐ์ด as const๋ฅผ ๋ถ์ฌ์คฌ์ด์ผ ํ์ง๋ง typesafe-actions๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๋ถ์ผ ํ์๊ฐ ์๋ค
์ก์ ์์ฑ ํจ์ ๋ง๋ค๊ธฐ
์ก์ ์์ฑ ํจ์๋ฅผ ์ ์ธ ํ ๋๋ createAction ์ด๋ผ๋ ํจ์๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
export const submit = createAction(SUBMIT)<TodoItemDataParams>();
//๊ฐ์ฒด๋ฅผ ํ์ด๋ก๋๋ก ๋ฐ์์ค๊ธฐ ๋๋ฌธ์ interface์ TodoItemDataParams๋ฅผ ํ์
์ผ๋ก ์ง์
export const remove = createAction(REMOVE)<number>();
export const toggle = createAction(TOGGLE)<number>();
์ก์ ์์ฑํจ์๋ฅผ ๋ง๋ค ๋ ํ์ด๋ก๋๋ก ๋ค์ด๊ฐ๋ ๊ฐ์ Generic์ผ๋ก ์ ํด์ค ์ ์๋๋ฐ, ๋ง์ฝ ํ์ด๋ก๋์ ๋ค์ด๊ฐ๋ ๊ฐ์ด ์๋ค๋ฉด Generic์ ์๋ตํด๋ ๋๋ค ์์ ๊ฐ์ ๊ฒฝ์ฐ๋ submit์ ํ์ด๋ก๋๋ก ๋ค์ด๊ฐ๋ ๊ฐ์ ๊ฐ์ฒด์ด๊ธฐ ๋๋ฌธ์ ๊ฐ์ฒด์์ ๊ฐ๋ค์ ๋ด๊ณ ์๋Iinterface๋ฅผ Generic์ผ๋ก ์ ํด์ฃผ์๋ค
์ก์ ์ ๊ฐ์ฒด ํ์ ๋ง๋ค๊ธฐ
๋ฆฌ๋์๋ฅผ ์์ฑ ํ ๋ action ํ๋ผ๋ฏธํฐ์ ํ์ ์ ์ค์ ํ๊ธฐ ์ํด์ ๋ชจ๋ ์ก์ ๋ค์ TypeScript ํ์ ์ ์ค๋นํด์ฃผ์ด์ผ ํ๋ค
const actions = { submit, remove, toggle };
type TodoActions = ActionType<typeof actions>;
ActionType์ ์ฌ์ฉํ ๋๋ Actions๋ผ๋ ๊ฐ์ฒด์ ๋ชจ๋ ์ก์ ์์ฑํจ์๋ฅผ ๋ฃ์ ๋ค์์, ActionType์ผ๋ก ๊ฐ์ธ์ฃผ๋ฉด ๋๋ค
๋ฆฌ๋์ ๋ง๋ค๊ธฐ
createReducer๋ฅผ ์ฌ์ฉํ์ฌ ๋ฆฌ๋์๋ฅผ ์์ฑํ์ createReducer์ ์ฌ์ฉ๋ฒ์ Redux-toolkit์ createReducer์ ์ฌ์ฉ๋ฒ๊ณผ ๋์ผํ๋ค
createReducer๋ Generic ์ผ๋ก ์ํ์ ํ์ ๊ณผ ์ก์ ๋ค์ ํ์ ์ ๋ฃ์ด์ฃผ์ด์ผ ํ๋ค
const initialState: ToDosState = { // initialState์ interface ToDosState๋ฅผ ํ์
์ผ๋ก ์ง์
todoItems: [],
};
const todo = createReducer<ToDosState, TodoActions>(initialState, {
// ๊ฐ์ฒด๋ฅผ ํ์ด๋ก๋๋ก ๋ฐ์ todoItems State์ ์ถ๊ฐํด์ฃผ๋ SUBMIT ์ก์
[SUBMIT]: (state, action) => ({
...state,
todoItems: [...state.todoItems, action.payload],
}),
[REMOVE]: (state, action) => ({
// todoList์ค ์ ํ๋ todo์ Id๋ฅผ ํ์ด๋ก๋๋ก ๋ฐ์ ์ ํ๋ todo๋ฅผ ์ญ์ ํด์ฃผ๋ REMOVE ์ก์
...state,
todoItems: state.todoItems.filter(
(todo: { id: number }) => todo.id !== action.payload
),
}),
[TOGGLE]: (state, action) => ({
// todoList์ค ์ ํ๋ todo์ Id๋ฅผ ํ์ด๋ก๋๋ก ๋ฐ์ done ์์ฑ์ ๋ณ๊ฒฝํด์ฃผ๋ TOGGLE ์ก์
...state,
todoItems: state.todoItems.map((todo) =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
),
}),
});
ActionType์ผ๋ก ๊ฐ์ธ์ค TodoActions์ initialState๋ฅผ ์ค์ ํ์ฌ ๊ฐ๊ฐ์ ์ก์ ์ ์ฒ๋ฆฌํ๋ค
container ์ปดํฌ๋ํธ๋ค ์์ฑํ๊ธฐ
container ์ปดํฌ๋ํธ๋ก์จ TodoContainer, TodoFormContainer, TodoListContainer ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๊ฐ ์์ฑํ๋ค
์ฐ์ ๋ชจ๋ ์ปดํฌ๋ํธ๋ค์ ์ฐ๊ฒฐ์์ผ์ค TodoContainer ๋จผ์ ์์ฑํด๋ณด์
TodoContainer
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import Todo from '../components/todo/Todo';
interface TodoItem {
id: number;
text: string;
done: boolean;
}
interface ReduxState { // useSelector๋ก ๋ฐ์์จ State์ ์ํ๋ฅผ ์ง์ ํด์ฃผ๋ interface
todoItems: TodoItem[];
}
function TodoContainer() {
const [selected, setSelected] = useState('Doing');
// ์ ํ๋ todo๊ฐ ์ ํ๋์ด completed ๋์๋์ง ํ์ธํ๊ธฐ ์ํ state
const handleSelected = (e: React.MouseEvent<HTMLButtonElement>) => {
// ํด๋ฆญ๋ todo์ event๊ฐ์ฒด๋ฅผ ๋ฐ์์์ selected State๋ฅผ ๋ณ๊ฒฝํด์ฃผ๋ ํจ์
setSelected(e.currentTarget.value);
};
const todos = useSelector((state: ReduxState) => state.todoItems);
let doingTodo: TodoItem[] = todos.filter((todo) => todo.done === false);
let completedTodo: TodoItem[] = todos.filter((todo) => todo.done === true);
//doingTodo, completedTodo ๋ณ์๋ ์ํ๋ก ๋ฐ์์จ todos๋ฅผ ์์ฑ done์ด false์ผ๋, true์ผ๋๋ฅผ
//๊ตฌ๋ถํ์ฌ ์๋ก์ด ๋ฐฐ์ด์ ๋ฆฌํดํ์ฌ ๋ด๊ณ ์๋ ๋ณ์
return (
<Todo
todos={todos}
handleSelected={handleSelected}
selected={selected}
doingTodo={doingTodo}
completedTodo={completedTodo}
/>
);
}
export default TodoContainer;
์์ ๊ฐ์ด TodoContainer ์ปดํฌ๋ํธ์์ ๋ฆฌ๋์ค์ ์ํ๋ฅผ ๋ฐ๊ณ ๋ด๋ถ State๋ฅผ ์์ฑํ์ฌ Presentational ์ปดํฌ๋ํธ์ธ Todo ์ปดํฌ๋ํธ์ Props๋ก ์ ๋ฌํด์ค๋ค
TodoFormContainer
๋ค์์ผ๋ก TodoForm ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ TodoFormContainer ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด๋ณด์
import React, { useCallback } from 'react';
import TodoForm from '../components/todoForm/TodoForm';
import { useState } from 'react';
import { submit } from '../modules/todo'; // ์ก์
submit import
import { useDispatch } from 'react-redux';
function TodoFormContainer() {
const [form, setForm] = useState('');
const [id, setId] = useState(1);
const dispatch = useDispatch(); //useDispatch hook์ ์ฌ์ฉํ์ฌ dispatch ๋ณ์์ ๋ด๋๋ค
const onSubmit = useCallback(
// ์คํ๋์ด์ง๋ฉด ๋งค๊ฐ๋ณ์ text๋ฅผ ๋ฐ์ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ submit ์ก์
์ ๋์คํจ์น ํด์ฃผ๋ ํจ์์ด๋ค
(text) => {
dispatch(submit({ id: id, text: text, done: false }));
setId(id + 1); // submit๋๋ฉด ๋ค์ id๊ฐ 1 ์ฆ๊ฐ
},
[dispatch, id]
);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(form);
setForm('');
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setForm(value);
};
return (
<TodoForm
form={form}
handleSubmit={handleSubmit}
onChange={onChange}
></TodoForm>
);
}
export default TodoFormContainer;
TodoFormContainer ์ปดํฌ๋ํธ์์๋ useDispatch๋ฅผ ์ฌ์ฉํด ์ก์ submit์ ์คํ์์ผ TodoList์ ์ถ๊ฐ ๋ ๊ฐ์ฒด๋ฅผ ํ์ด๋ก๋๋ก ๋ณด๋ด์ค๋ค
TodoListContainer
๋ค์์ผ๋ก TodoList ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ TodoListContainer ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด๋ณด์
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import TodoList from '../components/todoList/TodoList';
import { remove, toggle } from '../modules/todo';
interface TodoListContainerProps {
// todo ์ปดํฌ๋ํธ์์ ๋ฐ์ Props์ ํ์
์ ์ง์ ํด์ค interface
todo: {
id: number;
text: string;
done: boolean;
};
}
function TodoListContainer({ todo }: TodoListContainerProps) {
const dispatch = useDispatch(); //useDispatch hook ์ฌ์ฉํ์ฌ dispatch ๋ณ์์ ๋ด๋๋ค
const onRemove = useCallback(
// todo๋ผ๋ ์ด๋ฆ์ ์ ํ๋ todo์ id๋ฅผ ๋ด์ ๋งค๊ฐ๋ณ์๋ฅผ remove ์ก์
์ ํ์ด๋ก๋๋ก ๋ฃ๋๋ค
(todo: number) => dispatch(remove(todo)),
[dispatch]
);
const onToggle = useCallback(
// todo๋ผ๋ ์ด๋ฆ์ ์ ํ๋ todo์ id๋ฅผ ๋ด์ ๋งค๊ฐ๋ณ์๋ฅผ toggle ์ก์
์ ํ์ด๋ก๋๋ก ๋ฃ๋๋ค
(todo: number) => dispatch(toggle(todo)),
[dispatch]
);
const handleDelete = () => {
onRemove(todo.id);
};
const handleToggle = () => {
onToggle(todo.id);
};
return (
<TodoList
todo={todo}
handleDelete={handleDelete}
handleToggle={handleToggle}
/>
);
}
export default React.memo(TodoListContainer);
TodoListContainer ์์๋ useDispatch๋ฅผ ์ฌ์ฉํ์ฌ remove, toggle ์ก์ ์ ๋์คํจ์น ํด์ค๋ค
Presentational ์ปดํฌ๋ํธ ์์ฑํ๊ธฐ
Todo
import React from 'react';
import TodoHeader from '../todoHeader/TodoHeader';
import styles from './Todo.module.css';
import TodoFormContainer from '../../containers/TodoFormContainer';
import TodoListContainer from '../../containers/TodoListContainer';
interface TodoItem {
id: number;
text: string;
done: boolean;
}
interface TodoProps {
todos: TodoItem[];
handleSelected: (e: React.MouseEvent<HTMLButtonElement>) => void;
selected: string;
doingTodo: TodoItem[];
completedTodo: TodoItem[];
}
function Todo({
todos,
handleSelected,
selected,
doingTodo,
completedTodo,
}: TodoProps) {
return (
<div className={styles.Todo}>
<header className={styles.header}>
<TodoHeader />
</header>
<section className={styles.form}>
<TodoFormContainer />
</section>
<div className={styles.buttons}>
// ๊ฐ ๋ฒํผ๋ง๋ค selected ์ํ๋ฅผ ํ์ธํ์ฌ ๋์ผํ๋ฉด class๋ฅผ ์ถ๊ฐํด์ฃผ๋๋ก ํจ
<button
className={`${styles.button} ${
selected === 'Doing' && styles.btnClicked
}`}
value="Doing"
onClick={handleSelected}
>
Doing
</button>
<button
className={`${styles.button} ${
selected === 'Completed' && styles.btnClicked
}`}
value="Completed"
onClick={handleSelected}
>
Completed
</button>
<button
className={`${styles.button} ${
selected === 'ViewAll' && styles.btnClicked
}`}
value="ViewAll"
onClick={handleSelected}
>
View All
</button>
</div>
<ul className={styles.list}>
// selected ์ํ๋ฅผ ํ์ธํ์ฌ ์ง์ ๋ ๊ฐ๊ณผ ์ผ์นํ๋ฉด ๊ทธ์๋ง๋ ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๋๋ก ํจ
{selected === 'ViewAll' &&
todos.map((todo) => <TodoListContainer todo={todo} key={todo.id} />)}
{selected === 'Doing' &&
doingTodo.map((todo) => (
<TodoListContainer todo={todo} key={todo.id} />
))}
{selected === 'Completed' &&
completedTodo.map((todo) => (
<TodoListContainer todo={todo} key={todo.id} />
))}
</ul>
</div>
);
}
export default Todo;
Todo ์ปดํฌ๋ํธ์์๋ selected ์ํ๋ฅผ ํ์ธํ์ฌ TodoContainer ์ปดํฌ๋ํธ์์ ๋๊ฒจ์ค doingTodo, completedTodo ๋ฐฐ์ด์ map ์ ํตํด ๋ณด์ฌ์ค๋ค
TodoForm
import React from 'react';
import styles from './TodoForm.module.css';
interface FormProps {
// Props๋ก ๋ฐ์ ํจ์์ State์ ํ์
์ง์
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
form: string;
}
function TodoForm({ handleSubmit, onChange, form }: FormProps) {
return (
<form onSubmit={handleSubmit} className={styles.form}>
<input value={form} onChange={onChange} className={styles.input} />
<button type="submit" className={styles.button}>
ADD
</button>
</form>
);
}
export default TodoForm;
TodoForm ์ปดํฌ๋ํธ์์๋ TodoFormContainer ์ปดํฌ๋ํธ์์ ๋ฐ์ Props์ onChange, handleSubmit ํจ์๋ก input์ ๊ฐ์ ๋ณ๊ฒฝํ๋ ํจ์๋ฅผ ์คํ, TodoList ์ Todo๋ฅผ ์ถ๊ฐํด์ฃผ๋ ํจ์๋ฅผ ์คํ์ํจ๋ค
TodoList
import React from 'react';
import styles from './TodoList.module.css';
interface TodoListProps {
todo: {
id: number;
text: string;
done: boolean;
};
handleDelete: (todo: any) => void;
handleToggle: (todo: any) => void;
}
function TodoList({ todo, handleDelete, handleToggle }: TodoListProps) {
return (
<li className={styles.todo}>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<div>
<input
type="checkbox"
checked={todo.done}
readOnly={true}
onClick={handleToggle}
/>
<button className={styles.delete} type="button" onClick={handleDelete}>
๐
</button>
</div>
</li>
);
}
export default TodoList;
TodoList ์ปดํฌ๋ํธ์์๋ TodoListContainer ์ปดํฌ๋ํธ์์ ๋ฐ์ Props์ handleToggle, handleDeleteํจ์๋ก TodoList๋ฅผ ์ญ์ ํ๊ฑฐ๋, ์ ํ๋ Todo์ done์์ฑ์ ๋ณ๊ฒฝํ๋ ํจ์๋ฅผ ์คํํ๋๋ก ํ๋ค
TodoHeader
๋ง์ง๋ง์ผ๋ก TodoList์ Header ์ด๋ค
import React from 'react';
import styles from './TodoHeader.module.css';
function TodoHeader() {
return (
<div className={styles.container}>
<header className={styles.header}>Todo-List</header>
<span className={styles.description}>what is your next plan?</span>
</div>
);
}
export default TodoHeader;
์ด์ ์์ฑ๋ ๋ชจ์ต์ ์ดํด๋ณด์ ๐
์ด๋ ๊ฒ Redux์ TypeScript๋ฅผ ๊ฐ์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์๋ค typesafe-actions๋ก ๋ฆฌ๋์ค๋ฅผ ๋์ฑ ํธํ๊ฒ ๋ค๋ฃฐ ์ ์๋ ๊ฒ ๊ฐ์ ๋์ฑ ๊ณต๋ถํ ํ์๋ฅผ ๋๋๋ค!
reference
TypeScript ํ๊ฒฝ์์ Redux๋ฅผ ํ๋ก์ฒ๋ผ ์ฌ์ฉํ๊ธฐ
TypeScript ํ๊ฒฝ์์ Redux๋ฅผ ํ๋ก์ฒ๋ผ ์ฌ์ฉํ๊ธฐ
์ด๋ฒ์ ์ค๋นํ ํํ ๋ฆฌ์ผ์์๋ TypeScript ํ๊ฒฝ์์ Redux๋ฅผ ํ๋ก์ฒ๋ผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ์ ์ ๋ชฉ์ด "ํ๋ก์ฒ๋ผ"์ด๋! ์ฌ์ค์ ์กฐ๊ธ ์ฃผ๊ด์ ์ ๋๋ค. ์ด ํํ ๋ฆฌ์ผ์์๋ ์ง๊ธ๊น์ง
velog.io
'TypeScript' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ํ์ ์คํฌ๋ฆฝํธ ์์ํ๊ธฐ (0) | 2021.06.12 |
|---|