ํฐ์คํ ๋ฆฌ ๋ทฐ
react-redux์ typescript ํ์ฉํ๊ธฐ_typesafe-actions ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ๊ธฐ
choi95 2022. 4. 7. 21:31Issue
ํ๋ก์ ํธ ๋์ค typescript ๊ธฐ๋ฐ์ react-redux ์ ์ญ ์ํ๋ฅผ ์ธํ ํด์ผ ๋๋ ์ํฉ์ด ์์๋ค.
redux ๋ด action, ์ก์ ์์ฑ ํจ์, ๋ฆฌ๋์์ ๋ํ ์ ์ ํ ํ์ ์ ์ง์ ํด์ฃผ๊ธฐ ๊น๋ค๋ก์ด ์ํฉ ์์์ typesafe-actions ๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๊ฒ ๋์๋ค.
https://github.com/piotrwitek/typesafe-actions
GitHub - piotrwitek/typesafe-actions: Typesafe utilities for "action-creators" in Redux / Flux Architecture
Typesafe utilities for "action-creators" in Redux / Flux Architecture - GitHub - piotrwitek/typesafe-actions: Typesafe utilities for "action-creators" in Redux / Flux Architecture
github.com
์ด์ typesafe-actions ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ์ฌ ์ฑํ ๋ก๊ทธ๋ฅผ ์ ์ฅํ๋ ์ ์ญ store์ ๊ตฌํํ์๊ณ ์ด์ ๋ํด ํ๊ณ ํด ๋ณด๊ณ ์ ํ๋ค.
Code
1) redux ์ํ ๊ด๋ฆฌ ์ฝ๋ ๋ถ๋ฆฝ
flux ๊ธฐ๋ฐ์ ๋ฐ์ดํฐ ํ๋ฆ์ ์ํด redux์ ๊ด๋ จ๋ ์ฝ๋๋ฅผ modules ํด๋์ ๋ถ๋ฆฝํ์ฌ ์ฃผ์๋ค.
(module์ด๋ ํค์๋๋ ํ๋ก๊ทธ๋๋ฐ ์์์ ๊ด๋ฒ์ํ๊ฒ ์ฐ์ด๊ธฐ ๋๋ฌธ์, ์ง๊ธ ๋ณด๋ modules ํด๋๋ช ์ ๊ทธ๋ฅ ์ข์ง ๋ชปํ ๊ฒ ๊ฐ๋ค)
์ฑํ ๊ด๋ จ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ์๋ comments ํด๋ ๋ด์ ํฌ๊ฒ actions, reducer, types ํ์ ํด๋๋ก ๋ถ๋ฆฝํด์ค์ผ๋ก์จ, ๊ฐ ๊ธฐ๋ฅ์ ๋ํ ์ฝ๋๋ฅผ ๊ฐ๋ต ๋ฐ ๋ช ๋ชฉํ ์์ผฐ๋ค.
๐ฆmodules
โฃ ๐comments
โ โฃ ๐actions.ts
โ โฃ ๐reducer.ts
โ โ ๐types.ts
โ ๐index.ts
2) ์ก์ ์์ฑ ํจ์์ ๋ํด createAction ํจ์ ์ฌ์ฉ
import { createAction } from 'typesafe-actions';
import type { CommentInfo } from './types';
export const ADD_COMMENT = 'comments/ADD_COMMENT' as const;
export const DELETE_COMMENT = 'comments/DELETE_COMMENT';
export const RESPONSE_COMMENT = 'response/RESPONSE_COMMENT';
let nextId = 5;
export const addComment = (commentInfo: CommentInfo) => ({
type: ADD_COMMENT,
payload: {
...commentInfo,
messageId: ++nextId,
},
});
export const deleteComment = createAction(DELETE_COMMENT)<number>();
export const responseComment = createAction(RESPONSE_COMMENT)<number | null>();
์๋ ํ์ ์คํฌ๋ฆฝํธ ๋ด์์ ์ก์ ๊ฐ์ ๋ค์ as const ๋ผ๊ณ const assertion์ ํด์ค์ผ, string literal type์ด ์๋ ์์ ๊ทธ ์์ฒด ๊ฐ์ผ๋ก ์ธ์๋๋ค.
typesafe-acions ๋ด createAction ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ์ก์ ์ ์ง์ ํ๋ฉด ์๋์ผ๋ก ์ด์ ๋ํ ํ์ ์ง์ ๊ณผ์ ์ ์ฒ๋ฆฌํด์ฃผ๊ธฐ ๋๋ฌธ์ ์๋ต์ด ๊ฐ๋ฅํ๋ค.
ํ์ง๋ง ์ด ๊ฒฝ์ฐ์๋ as const๋ฅผ ์จ์ผ ๋ ๊ฒฝ์ฐ์ ์ฐ์ง ์์ ๊ฒฝ์ฐ๋ก ๋๋ ์ง๋๋ฐ, action.payload ๊ฐ์ ๊ทธ๋๋ก ์ฐ์ง ์๊ณ ์ ์ ํด์ผ ๋ ๊ฒฝ์ฐ(ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ์จ ๋ฐ์ดํฐ์ ๋ฆฌํด๋๋ action.payload ๊ฐ์ด ๋ค๋ฅผ ๊ฒฝ์ฐ)์๋ createAction ํจ์๋ฅผ ์ฐ์ง ์๊ณ const assertion์ ํ ๋ค ์ผ๋ฐ ์ก์ ์์ฑ ํจ์๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์ข๋ค.
// ์์ ์ฝ๋
// const assertion ์ฌ์ฉ
export const addTodo = (text: string) => ({
type: ADD_TODO,
payload: {
id: nextId++,
text
}
})
// createAction ํจ์ ์ฌ์ฉ => ์ฝ๋๊ฐ ๋ ๋ณต์กํด ์ง ์ ์์
export const addTodo = createAction(ADD_TODO, action => (text: string) => action({
id: nextId++,
text
}))
3) InitialState์ Action์ ๋ํ ํ์ ์ง์
import { ActionType } from 'typesafe-actions';
import * as actions from './actions';
export type CommentsAction = ActionType<typeof actions>;
export type ResponseAction = ActionType<typeof actions>;
export type CommentInfo = {
userid: number;
messageId: number;
userName: string;
profileImage: string;
content: string;
date: string;
responseId: number | null;
};
export type CommentsInfoState = CommentInfo[];
export type ResponseState = {
responseId: null | number;
responseActive: boolean;
};
์ก์ ์ ๋ํ ํ์ ์ ์ง์ ํด ์ฃผ๊ธฐ ์ํด acions ๋ชจ๋์์ ๊ฐ action๋ค์ ํ ๋ฒ์ import ํด ์จ ๋ค, typeof ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ํด๋น acions ๊ฐ์ฒด์ ๋ํ ํ์ ์ถ๋ก ์ ํด์ฃผ์๋ค.
๊ทธ๋ฆฌ๊ณ ํด๋น typeof ์ฐ์ฐ์ typesafe-actions ๋ด ActionType ํจ์์ ์ ๋๋ฆญ์ผ๋ก ์ง์ ํด์ฃผ์ด, action์ ๋ํ ํ์ ๊ฐ์ ์ ์ฝ๊ฒ ์ง์ ํ์ฌ ์ฃผ์๋ค.
// ์์ ์ฝ๋
// ActionType์ ์ฐ์ง ์์ ๊ฒฝ์ฐ(๋ชจ๋ ์ก์
์ ๋ํด ReturnType ์ฐ์ฐ์ ์ฌ์ฉ)
type TodosAction = ReturnType<typeof addTodo> | ReturnType<typeof toggleTodo> | ReturnType<typeof removeTodo>
// ActionType์ ์ธ ๊ฒฝ์ฐ
const actions = { addTodo, toggleTodo, removeTodo };
type TodosAction = ActionType<typeof actions>;
4) createReducer ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ฆฌ๋์ ์ ์
import { createReducer } from 'typesafe-actions';
import { CommentsAction, CommentsInfoState } from './types';
import { ADD_COMMENT, DELETE_COMMENT, RESPONSE_COMMENT } from './actions';
const initialState: CommentsInfoState = [
{
userid: 1,
messageId: 1,
userName: '์ถ์',
profileImage: './img/profile_yongwoo.png',
content: '๋ค์ด๋ฒ๋ก ๋ณด๋ด์ค~',
date: '2021-02-10 23:26:30',
responseId: null,
},
( ... )
];
const comments = createReducer<CommentsInfoState, CommentsAction>(initialState, {
[ADD_COMMENT]: (state, action) => state.concat(action.payload),
[DELETE_COMMENT]: (state, action) => state.filter((info) => info.messageId !== action.payload),
});
export default comments;
types ๋ชจ๋์์ ์ ์ ํ type ๊ฐ๋ค์ importํ์ฌ initialState์ ํ์ ์ ์ง์ ํด์ฃผ๋ฉฐ createReducer ํจ์์ ์ ๋๋ฆญ์ผ๋ก ์ฌ์ฉํ์๋ค.
// ์์ ์ฝ๋
// createReducer ํจ์๋ฅผ ์ฌ์ฉํ์ง ์์ ๊ฒฝ์ฐ
function todos(state: TodosState = initialState, action: TodosAction): 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
}
}
// createReducer ํจ์๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ
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)
})