Receipto는 총무가 모임에서의 결제 내역을 체계적으로 정리하고 공유할 수 있도록 도와주는 웹 애플리케이션입니다. 이번 글에서는 Receipto에 필요한 상태를 React Hooks를 통해 관리하는 방법에 대해 작성해보았습니다.
Receipto의 요구사항은 다음과 같습니다:
이해를 돕기 위해 현재 구현된 UI 스크린샷을 첨부합니다.
위 요구사항을 만족하기 위해 다음과 같은 상태가 필요하겠다고 생각했습니다:
interface PaymentHistory {
id: string;
content: string;
amount: number;
}
interface Receipt {
title: string;
date: Date | undefined;
histories: PaymentHistory[];
peopleCount: number;
}
useState
는 React 함수형 컴포넌트에서 가장 기본적으로 사용하는 상태 관리 Hook입니다. 컴포넌트 내부에서 간단하게 상태 값을 선언하고, 그 값을 변경할 수 있는 setter 함수를 제공합니다.
useState는 배열을 반환하며, 첫 번째 요소는 현재 상태 값, 두 번째 요소는 상태를 변경하는 함수(set
함수)입니다. 상태 변경 시 set
함수를 호출하면 React가 상태를 갱신하고 컴포넌트를 다시 렌더링합니다.
import { useState } from 'react';
export default function CounterComponent() {
const [count, setCount] = useState(0);
// ...
}
useReducer
는 useState
와 유사하게 상태를 관리하지만, 상태 변경 로직을 컴포넌트 외부의 reducer
함수로 분리할 수 있습니다. 복잡한 상태(여러 하위 값을 포함하거나, 상태 변경 로직이 복잡한 경우) 관리에 적합합니다.
useReducer
는 [state, dispatch]
형태의 배열을 반환합니다. 상태를 변경하려면 dispatch 함수에 action 객체를 전달합니다. 이 action은 reducer 함수에서 처리되어 새로운 상태를 반환합니다.
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
}
구분 | useState | useReducer |
---|---|---|
사용 목적 | 단순한 상태(숫자, 문자열 등) 관리 | 복잡한 상태(여러 값, 복잡한 변경 로직) 관리 |
상태 변경 방식 | set 함수로 직접 변경 | dispatch로 action을 전달, reducer 함수에서 처리 |
로직 위치 | 컴포넌트 내부에 상태 변경 로직이 위치 | 상태 변경 로직을 reducer 함수로 분리 가능 |
코드 구조 | 간단하고 직관적임 | 구조적이고 일관성 있게 복잡한 로직 관리 가능 |
useState
와 useReducer
훅의 정의 및 차이를 공부한 결과, Receipto에서는 단순한 상태보다 복잡한 상태(Receipto에서는 여러 값을 사용하여 복잡한 상태라고 생각)를 관리해야 하므로 useReducer
를 선택하는 것이 적합하다고 판단했습니다.
먼저, 상태의 업데이트를 처리하는 reducer
함수를 정의합니다. 각 액션 타입에 따라 상태를 어떻게 변화시킬지 설정합니다.
function reducer(state: Receipt, action: PaymentAction): Payment {
const { type, payment } = action;
switch (type) {
case 'CHANGED_TITLE':
return { ...state, title: payment.title };
case 'CHANGED_DATE':
return { ...state, date: payment.date };
case 'ADD_PAYMENT_HISTORY':
return { ...state, histories: [...state.histories, payment.history] };
case 'DELETE_PAYMENT_HISTORY':
return {
...state,
histories: state.histories.filter(
(history) => history.id !== payment.history.id,
),
};
case 'CHANGED_PEOPLE_COUNT':
return { ...state, peopleCount: payment.peopleCount };
default:
return state;
}
}
useReducer
를 통해 초기 상태를 설정하고, 상태 변경을 위한 dispatch
함수를 얻습니다.
const [payment, dispatch] = useReducer(reducer, {
title: '',
date: undefined,
histories: [],
peopleCount: 0,
});
컴포넌트 내에서 dispatch를 사용하여 상태를 업데이트합니다.
예를 들어, 제목이 변경될 때 dispatch를 호출하여 변경된 상태를 반영합니다.
<Input
id="title"
value={title}
onChange={(e) =>
dispatch({
type: 'CHANGED_TITLE',
payment: { title: e.target.value },
})
}
/>
이번 글을 통해 React Hooks 중 상태를 관리할 때 사용하는 useState
와 useReducer
의 차이에 대해 공부할 수 있었습니다. useState
는 단순한 상태(숫자, 문자열 등)를 관리할 때 유용하며, useReducer
는 여러 값이나 복잡한 변경 로직이 필요한 복잡한 상태를 관리하기에 적합하다는 것을 배웠습니다.
useState
가 익숙했던 터라 useReducer
가 복잡할 것이라 생각했지만, 실제로 사용해 보니 상대적으로 코드가 복잡하지만 상태를 오히려 더 깔끔하게 관리할 수 있는 훅이라고 생각했습니다. useReducer
를 통해 액션 타입으로 상태 업데이트 로직을 명확하게 표현할 수 있어 어떤 상태의 값이 바뀌는가를 쉽게 파악할 수 있었습니다.
다음에는 Receipto의 UX 개선과 코드 리팩토링, 그리고 최적화 작업에도 도전해 보고자 합니다.