이번엔 타입스크립트와 리덕스를 사용해보자.
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=
먼저 카운터 예제를 한번 만들어보자.
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
라이브러리에 대한 정보는 위 링크를 참조하면 된다.
그러면 우선 만들어본 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가 호출되기 때문이다.
이제 타입스크립트로 리덕스를 사용하는 방법에 알아보았다. 다음은 리덕스 미들웨어를 타입스크립트에 적용시켜 보자.
'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 |