React에서는 컴포넌트(component) 외부에서 상태(state)를 관리하기 위한 방법으로 useReducer가 제공됩니다.
useState와 useReducer 모두 상태를 관리하기 위한 Hook으로 어느 것을 사용해도 문제가 없지만 프로젝트 규모가 큰 경우 useReducer를 사용하는 것이 편리할 거 같습니다😊
오늘은 typescript로 useReducer를 사용하는 예제를 정리했습니다.
useReducer에 대한 자세한 설명은 아래 공식 문서에서 확인할 수 있습니다.
https://reactjs.org/docs/hooks-reference.html#usereducer
Hooks API Reference – React
A JavaScript library for building user interfaces
reactjs.org
reducer의 type 지정
reduce의 type 지정은 아래와 같은 형식으로 할 수 있습니다.
function reducer(state: stateType, action: actionType): stateType {}
이에 대한 코드 예시는 아래와 같습니다.
CounterWithReducer.tsx
import React, { useReducer } from 'react';
interface CounterProps {
}
interface Action {
type: string,
}
// state와 action에 모두 타입을 선언
// reducer의 반환 값은 state 이므로 반환 값 타입은 state와 일치하도록 지정
function reducer (state: number, action: Action): number {
switch(action.type) {
case "INCREMENT":
return state+1;
case "DECREMENT":
return state-1;
default:
return state;
}
}
function Counter(props: CounterProps) {
const [ number, dispatch ] = useReducer(reducer, 0);
const onIncrease: () => void = () => {
dispatch({ type: "INCREMENT" });
}
const onDecrease: () => void = () => {
dispatch({ type: "DECREMENT" });
}
return (
<>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</>
);
}
export default Counter;
예시 - useReducer로 crud 기능 구현
useReducer를 제대로 연습하려면 action tpye을 여러 개 정의하고 action에 payload(state 관리에 필요한 data)를 정의해봐야 할 거 같았습니다.
그래서 useReducer를 사용하여 메뉴를 추가, 수정, 삭제를 할 수 있는 간단한 메뉴 관리 시스템을 만들었습니다.
interface.ts
menuItem(메뉴 정보), state, action의 interface를 정의한 파일
// 각 menu의 interface
interface MenuItemInterface {
id: number,
name: string,
price: number|string,
isSelected: boolean
}
// reducer에서 사용할 state의 interface를 정의
interface StateInterface {
input: {
name: string,
price: string| number
},
menuItems: MenuItemInterface[],
isEditMode: boolean,
selectedId: number,
}
// Action의 interface를 정의
interface Action {
type: string,
payload?: any,
}
export type { MenuItemInterface, StateInterface, Action }
menuPageActions.ts
reducer에서 사용되는 action을 정의한 파일
// Action의 각 type을 상수로 정의
const CHANGE_INPUT: string = "CHANGE_INPUT"; // 입력 action
const CREATE_MENU: string = "CREATE_MENU"; // menu 생성 action
const SELECT_EQUAL_MENU: string = "SELECT_EQUAL_MENU"; // menu 선택 action(edit mode off)
const SELECT_DIFFERENT_MENU: string = "SELECT_DIFFERENT_MENU"; // menu 선택 action(edit mode on)
const DELETE_MENU: string = "DELETE_MENU"; // menu 삭제 action
const UPDATE_MENU: string = "UPDATE_MENU"; // menu 수정 action
export { CHANGE_INPUT, CREATE_MENU, SELECT_EQUAL_MENU, SELECT_DIFFERENT_MENU, DELETE_MENU, UPDATE_MENU }
MenuPageWithReducer.tsx
메뉴를 입력하는 입력 부분과 저장된 메뉴를 보여주는 출력 부분으로 구성된 컴포넌트
- 메뉴 입력 부분: MenuInput component
- 메뉴 출력 부분: MenuList component
import React, { useReducer, useRef, useCallback } from 'react';
import MenuInput from '../components/MenuInput';
import MenuList from '../components/MenuList';
import { MenuItemInterface, StateInterface, Action } from '../lib/interface/index';
import { CHANGE_INPUT, CREATE_MENU, SELECT_EQUAL_MENU, SELECT_DIFFERENT_MENU, DELETE_MENU, UPDATE_MENU } from '../lib/actions/menuPageActions';
interface MenuPageWithReducerProps {
}
const initialState: StateInterface = {
input: {
name: "",
price: "",
},
menuItems: [
{
id: 1,
name: "Lemon",
price: 1000,
isSelected: false,
},
{
id: 2,
name: "Melon",
price: 2000,
isSelected: false,
},
{
id: 3,
name: "Apple",
price: 3000,
isSelected: false,
}
],
isEditMode: false,
selectedId: -1,
}
function reducer(state: StateInterface, action: Action): StateInterface { // reducer에서 사용된 state와 action에도 type을 명시했다.
switch(action.type) {
case CHANGE_INPUT: // 입력창에 입력이 됐을 때 발생하는 action
return {
...state,
input: {
...state.input,
[action.payload.name]: action.payload.value
}
}
case CREATE_MENU: // menu를 추가할 때 발생하는 action
return {
...state,
menuItems: [ ...state.menuItems, action.payload.newMenu ],
input: {
name: "",
price: "",
}
}
case SELECT_EQUAL_MENU: // 동일한 menu를 선택했을 때 발생하는 action(turn off edit mode)
return {
...state,
input: {
name: "",
price: "",
},
menuItems: state.menuItems.map((item, index) => ({...item, isSelected: false})),
selectedId: -1,
isEditMode: false,
}
case SELECT_DIFFERENT_MENU: // 다른 menu를 선택했을 때 발생하는 action(turn on edit mode)
return {
...state,
input: {
name: action.payload.menuItem.name,
price: action.payload.menuItem.price,
},
menuItems: state.menuItems.map((item, index) => ({...item, isSelected: action.payload.menuItem.id === item.id})),
selectedId: action.payload.menuItem.id,
isEditMode: true,
}
case DELETE_MENU: // menu를 삭제할 때 발생하는 action
return {
...state,
input: {
name: "",
price: ""
},
menuItems: state.menuItems.filter((item, index) => { return item.id !== action.payload.menuItemId }),
selectedId: -1,
isEditMode: false,
}
case UPDATE_MENU: // menu를 수정할 때 발생하는 action
return {
...state,
input: {
name: "",
price: "",
},
menuItems: state.menuItems.map((item, index) => {
return ({
...item,
name: state.selectedId === item.id? state.input.name: item.name,
price: state.selectedId === item.id? state.input.price: item.price,
isSelected: false,
})
}),
selectedId: -1,
isEditMode: false,
}
default:
return state
}
}
function MenuPageWithReducer (props: MenuPageWithReducerProps) {
const [ state, dispatch ] = useReducer(reducer, initialState);
const { menuItems, isEditMode, selectedId } = state;
const { name, price } = state.input;
const menuId = useRef(4);
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => { // 입력할 때 호출되는 함수
const { name, value } = event.target;
dispatch({
type: CHANGE_INPUT,
payload: {
name,
value,
}
});
}
const onCreate = (): void => { // menu를 생성할 때 호출되는 함수
if(name !== "" && price !== "") {
const newMenu = {
id: menuId.current,
name: name,
price: price,
isSelected: false,
}
dispatch({
type: CREATE_MENU,
payload: {
newMenu,
}
});
menuId.current += 1;
}
}
const onUpdate = (): void => { // menu를 수정할 때 호출되는 함수
if(name !== "" && price !== "") {
dispatch({
type: UPDATE_MENU,
})
}
}
const onSelect = useCallback((data: MenuItemInterface): void => { // menu를 선택할 때 호출되는 함수
if(selectedId === data.id) {
dispatch({
type: SELECT_EQUAL_MENU,
});
} else {
dispatch({
type: SELECT_DIFFERENT_MENU,
payload: {
menuItem: data,
}
});
}
}, [ selectedId ]);
const onDelete = useCallback((id: number): void => { // menu를 삭제할 떄 호출되는 함수
dispatch({
type: DELETE_MENU,
payload: {
menuItemId: id,
}
})
}, []);
return (
<>
<MenuInput name={name} price={price} isEditMode={isEditMode} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate}/>
<MenuList menuItems={menuItems} onSelect={onSelect} onDelete={onDelete}/>
</>
);
}
export default MenuPageWithReducer;
MenuInput.tsx
입력창을 통해 메뉴를 추가, 삭제할 수 있는 기능을 가진 component
import React from 'react';
interface MenuInputProps {
name: string,
price: number | string,
isEditMode: boolean,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
onCreate: () => void,
onUpdate: () => void,
}
function MenuInput(props: MenuInputProps) {
const { onCreate, onUpdate } = props;
const onPress = () => {
if(props.isEditMode) {
onUpdate();
} else {
onCreate();
}
}
return (
<>
<div>
<div>
<span>
메뉴 이름
</span>
<input placeholder="이름을 입력해주세요." name="name" value={props.name} onChange={props.onChange}/>
</div>
<div>
<span>
메뉴 가격
</span>
<input placeholder="가격을 입력해주세요." name="price" value={props.price} onChange={props.onChange}/>
</div>
<div>
<button onClick={onPress}>
{
props.isEditMode? "수정": "추가"
}
</button>
</div>
</div>
</>
)
}
export default MenuInput;
MenuList.tsx
저장된 메뉴 목록을 보여주는 컴포넌트
import React from 'react';
import { MenuItemInterface } from '../lib/interface';
import MenuItem from './MenuItem';
interface MenuListProps {
menuItems: MenuItemInterface[],
onSelect: (data: MenuItemInterface) => void,
onDelete: (id: number) => void,
}
function MenuList (props: MenuListProps) {
return (
<>
{
props.menuItems.map((item, index) => {
return(<MenuItem {...item} key={item.id} onSelect={props.onSelect} onDelete={props.onDelete}/>)
})
}
</>
)
}
export default React.memo(MenuList);
MenuItem.tsx
import React from 'react';
import { MenuItemInterface } from '../lib/interface';
interface MenuItemProps extends MenuItemInterface {
onSelect: (data: MenuItemInterface) => void,
onDelete: (id: number) => void,
}
function MenuItem(props: MenuItemProps) {
const { id, name, price, isSelected, onSelect, onDelete } = props;
return (
<div>
<span onClick={() => { onSelect({id, name, price, isSelected}); }}>
<b style={{ color: isSelected? "pink": "black" }}>{name}</b><span> - {price}</span>
</span>
<span>
{
isSelected?
<button onClick={() => { onDelete(id); }}>삭제</button>:
null
}
</span>
</div>
)
}
export default React.memo(MenuItem)
간단한 실행 예시는 아래와 같습니다.
- 참고 자료
https://reactjs.org/docs/hooks-reference.html#usereducer - React 공식문서(useReducer)
https://react.vlpt.us/basic/20-useReducer.html - 벨로퍼트와 함께하는 모던 리액트(useReducer를 사용하여 상태 업데이트 로직 분리하기)
읽어 주셔서 감사합니다 :)
틀린 부분이 있다면 댓글로 편히 알려주세요😊
'Programming > TS' 카테고리의 다른 글
[Typescript] React with Typescript 1 (0) | 2021.06.20 |
---|---|
[Typescript] Typescript Function, Interface, Object (0) | 2021.06.19 |
[Typescript] Typescript 타입 (0) | 2021.06.15 |
[Typescript] Typescript 파일 실행시키기 (0) | 2021.06.13 |