Prologue
useState는 참 편리한 훅입니다. 초기값 할당과 업데이트, 사용 방식이 매우 직관적이고 단순하죠. 하지만 한 컴포넌트 안에 state가 많이 존재한다면, 그리고 한 함수에서 여러 state를 업데이트해야 한다면 컴포넌트 규모는 금새 커지게 됩니다. 이번 글에서는 이 문제를 해결하기 위해 useReducer를 활용한 custom hook 제작 사례를 소개하려 합니다.
발단
대부분의 서비스에는 로그인, 회원가입과 같은 인증 페이지가 존재합니다. 그리고 그 페이지에는 아이디, 이름, 비밀번호 등 단순한 입력값뿐만 아니라 휴대폰 인증 여부, 비밀번호 양식 검증 등 유효성 검사를 관리하기 위해 많은 상태와 비즈니스 로직이 존재합니다. 제가 담당했던 서비스 또한 많은 상태로 인해 꽤 복잡해진 상태였습니다. 이로 인해 생긴 문제점은 다음과 같습니다.
- state hook이 많고 파편화되어 있다.
- useEffect 내 dependency가 n개만큼 늘어난다.
- 한 함수 내 상태 업데이트 시 개별로 지정해줘야 한다.
- 위 세 문제점만큼 코드도 n개씩 증가한다.
이러한 문제를 해결하고자 useForm이라는 커스텀 훅을 만들게 되었습니다.
useForm
먼저 구성은 다음과 같습니다.
import { useReducer } from 'react';
interface TypeUseForm {
<T>(initialForm: T): [T, (states: Partial<typeof initialForm>) => void];
}
const useForm: TypeUseForm = (initialForm) => {
const [form, dispatchForm] = useReducer((acc: typeof initialForm, cur: Partial<typeof initialForm>) => {
return { ...acc, ...cur };
}, initialForm);
return [form, dispatchForm];
};
export default useForm;
단계별로 해석하자면 다음과 같습니다.
타입 정의
먼저, TypeUseForm이라는 인터페이스가 정의되어 있습니다. 이 인터페이스는 제네릭 타입 <T>를 받습니다.
interface TypeUseForm {
<T>(initialForm: T): [T, (states: Partial<typeof initialForm>) => void];
}
이 인터페이스는 initialForm이라는 초기 폼 상태를 받아서 [폼 상태, 상태 업데이트 함수]를 반환하는 함수 타입을 정의합니다.
커스텀 훅 정의
그 다음, useForm이라는 커스텀 훅이 정의됩니다. 이 훅은 TypeUseForm 타입을 따릅니다.
const useForm: TypeUseForm = (initialForm) => {
const [form, dispatchForm] = useReducer((acc: typeof initialForm, cur: Partial<typeof initialForm>) => {
return { ...acc, ...cur };
}, initialForm);
return [form, dispatchForm];
};
useReducer를 사용하여 폼 상태를 관리합니다. useReducer는 두 가지 인자를 받습니다.
- 리듀서 함수: 현재 상태(acc)와 업데이트될 상태(cur)를 받아 새로운 상태를 반환합니다.
- 초기 상태: initialForm이 초기 상태로 사용됩니다.
이 함수는 현재 상태(acc)와 업데이트될 상태(cur)를 합쳐서 새로운 상태 객체를 반환합니다. 이를 통해 기존 상태에 새로운 상태를 병합할 수 있습니다.
- form: 현재 폼 상태
- dispatchForm: 상태를 업데이트하는 함수
사용 예시
다음은 useForm 훅을 사용하는 예시입니다.
import useForm from '@/hooks/useForm';
const initialForm = { name: '', email: '' };
const [form, dispatchForm] = useForm(initialForm);
// 업데이트 예시 1. 하나의 상태
dispatchForm({ name: 'John Doe' });
// 업데이트 예시 2. 복수 상태
dispatchForm({ name: 'John Doe', email: 'johndoe@gmail.com' });
// 값 사용 예시
console.log(form.name); // John Doe
여러 상태를 객체의 프로퍼티로 구성해 초기값을 useForm에 할당합니다. 그리고 dispatchForm에 업데이트가 필요한 키와 값을 추가하면 useState와 같이 상태를 업데이트할 수 있습니다.
또는 아래와 같이 객체 프로퍼티 형태로도 사용할 수 있습니다.
import { useEffect } from 'react';
import useForm from '@/hooks/useForm';
const MyComponent = () => {
const meta = { firstName: '', lastName: '' };
const initialForm = { name: '', email: '', meta };
const [form, dispatchForm] = useForm(initialForm);
// 업데이트 예시
useEffect(() => {
const { firstName, lastName } = form.meta;
const name = firstName + lastName;
dispatchForm({ name });
}, [form.meta]);
return (
<div>
<input
type="text"
value={form.meta.firstName}
onChange={(e) => dispatchForm({ meta: { ...form.meta, firstName: e.target.value } })}
/>
<input
type="text"
value={form.meta.lastName}
onChange={(e) => dispatchForm({ meta: { ...form.meta, lastName: e.target.value } })}
/>
<input type="text" value={form.name} readOnly />
</div>
);
};
export default MyComponent;
회고
그동안 useReducer 훅은 이론으로만 알고 있었지 실제로 사용해본 경험은 없었습니다. 또한 redux와 같이 action type을 전달하는 방식만 알고 있었는데, 위와 같이 next state를 전달하는 방식을 알게 되고 실제로 적용해볼 수 있어서 좋았던 경험이었습니다.
참고
'Programming > 3. Experience' 카테고리의 다른 글
safe triangle target area로 컨텍스트 메뉴 사용성 높이기 (0) | 2024.08.15 |
---|---|
emotion to scss module 도입기 (0) | 2024.07.25 |
서비스에 감정 불어넣기! @emotion 적용기 (0) | 2022.11.26 |
SCSS에서 Styled-Components로 변환하며 겪은 썰.txt (0) | 2022.05.16 |
[연습] Momentum 응용하기 (0) | 2021.06.23 |
댓글