본문 바로가기
Programming/3. Experience

useReducer + custom hook으로 state 관리하기

by @sangseophwang 2024. 7. 26.

 

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는 두 가지 인자를 받습니다.

  1. 리듀서 함수: 현재 상태(acc)와 업데이트될 상태(cur)를 받아 새로운 상태를 반환합니다.
  2. 초기 상태: 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를 전달하는 방식을 알게 되고 실제로 적용해볼 수 있어서 좋았던 경험이었습니다.
 

참고

react: comparing useState and useReducer

반응형

댓글