폼(form)을 다루면서 생긴 문제

Hunter Joe·2025년 7월 6일
0

기존 코드는 동작을 위한 form 코드, useState로 상태를 관리했었다.

export default function AddInfoForm() {
  const { userInfo } = useGetUserInfo();
  // form state
  const [interests, setInterests] = useState<string[]>([]);
  const [name, setName] = useState<string>('');
  const [email, setEmail] = useState<string>('');
  const [phone, setPhone] = useState<string>('');
  const [role, setRole] = useState<string>('');

해당 코드를 하나의 state로 관리하면 어떨까 하는 생각에
아래와 같이 리팩토링을 진행했음

useForm.tsx (hook)

import { useReducer } from 'react';

const reducer = (state: any, action: any) => {
  switch (action.type) {
    case 'TOGGLE_ARRAY_VALUE':
      const arr = state[action.payload.name] as string[];
      const value = action.payload.value;
      return {
        ...state,
        [action.payload.name]: arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value],
      };
    case 'CHANGE_VALUE':
      return { ...state, [action.payload.name]: action.payload.value };
    default:
      return state;
  }
};

export const useForm = (initialState: { [key: string]: string | string[] }) => {
  const [formValues, dispatch] = useReducer(reducer, initialState);
  console.log(formValues);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => {
    const { name, value } = e.target;
    // 배열 필드라면 TOGGLE_ARRAY_VALUE 사용
    if (Array.isArray(formValues[name])) {
      dispatch({ type: 'TOGGLE_ARRAY_VALUE', payload: { name, value } });
    } else {
      dispatch({ type: 'CHANGE_VALUE', payload: { name, value } });
    }
  };

  return { formValues, handleChange };
};

addInfoForm.tsx

import Button from '@/components/common/button';
import Chip from '@/components/common/chip';
import Input from '@/components/common/input';
import { LABELS } from '@/components/common/input/labels';
import { categories } from '@/configs/category';
import { roles } from '@/configs/roles';
import { Select, SelectItem } from '@heroui/react';
import { useForm } from '../hooks/useForm';

export default function Form() {
  const { formValues, handleChange } = useForm({
    name: '',
    email: '',
    phone: '',
    role: '',
    interest: [],
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
  };

  return (
    <>
      <form
        className="mx-auto w-1/2 flex-1 flex-col gap-6 space-y-6 rounded-3xl border border-white/20 bg-white/10 p-10 shadow-2xl backdrop-blur-2xl"
        onSubmit={handleSubmit}
      >
        <h2 className="mb-2 text-3xl font-extrabold text-white drop-shadow">회원 정보 입력</h2>
        <Input
          name="name"
          label={LABELS.NAME}
          type="text"
          placeholder="사용하실 이름을 입력해주세요."
          description="이름은 최대 10자까지 입력할 수 있습니다."
          // isInvalid={!nameSchema.safeParse(name).success}
          // errorMessage={nameSchema.safeParse(name).error?.issues[0]?.message}
          value={formValues.name}
          onChange={handleChange}
        />
        <Input
          name="email"
          label={LABELS.EMAIL}
          type="email"
          placeholder="example@example.com"
          value={formValues.email}
          onChange={handleChange}
        />
        <Input
          name="phone"
          label={LABELS.PHONE}
          type="tel"
          placeholder="010-1234-5678"
          value={formValues.phone ?? ''}
          onChange={handleChange}
        />
        <Select
          name="role"
          items={roles}
          label="역할"
          placeholder="참여하실 역할을 선택해주세요."
          value={formValues.role}
          color="primary"
          onChange={handleChange}
          variant="underlined"
          classNames={{
            label: 'text-black dark:text-white/90',
            listbox: 'text-black dark:text-white/90',
          }}
        >
          {(role) => <SelectItem>{role.label}</SelectItem>}
        </Select>

        <div>
          <label className="mb-1 block">관심사</label>
          <div className="mb-1 flex flex-wrap gap-2">
            {categories.map((category) => (
              // NOTE: 임시 관심사 리스트임
              <Chip
                name="interest"
                key={category}
                onClick={() =>
                  handleChange({
                    target: {
                      name: 'interest',
                      value: category,
                    },
                  } as React.ChangeEvent<HTMLInputElement>)
                }
                color="danger"
                radius="md"
                className="w-fit cursor-pointer border border-white/20 shadow backdrop-blur-md"
                variant={formValues.interest.includes(category) ? 'flat' : 'bordered'}
              >
                {category}
              </Chip>
            ))}
          </div>
          <p className="text-xs text-black/80 dark:text-white/80">관심있는 팝업을 선택해주세요.</p>
        </div>
        <Button
          type="submit"
          className="w-full rounded-full bg-gradient-to-r from-pink-400 to-blue-400 font-semibold text-white shadow-lg transition hover:scale-105"
        >
          완료
        </Button>
      </form>
    </>
  );
}

useReducer를 활용해서 작성했는데 처음 input의 e.target.value만을 다룰 때
즉, 코드가 단순할 때는 효율적이였지만
상태관리, 유효성검사, 에러상태처리 등 여러 상태처리를 같이 하려고보니 코드 가독성, 유지보수성 모든 면에서 기존 코드보다 나은점이 없었음 또한 단일원칙또한 지켜지지 않았음

오히려 더 복잡해졌고 코드 쓰레기장이 되어버린 결과가 생김

이를 위해서 어떻게 리팩토링을 할지 생각중에 있다. 한번 더 시도해보고 좋은 결과가 생기면 아래에 추가할 예정

내가 해결한 방법

state와 action을 zustand를 사용해 store를 만들어줌

import { create } from 'zustand';

interface AddInfoFormState {
  name: string;
  email: string;
  phone: string;
  role: string;
  interests: string[];
  nameValid: boolean;
  emailValid: boolean;
  phoneValid: boolean;
}

interface AddInfoFormActions {
  setValue: (key: string, value: string) => void;
  setInterests: (interests: string[]) => void;
  setIsValid: (key: string, isValid: boolean) => void;
}

export const useAddInfoFormStore = create<AddInfoFormState & AddInfoFormActions>((set) => ({
  name: '',
  email: '',
  phone: '',
  role: '',
  interests: [],
  nameValid: false,
  emailValid: false,
  phoneValid: false,
  setValue: (key, value) => set({ [key]: value }),
  setIsValid: (key, isValid) => set({ [`${key}Valid`]: isValid }),
  setInterests: (interests: string[]) => set({ interests: [...interests] }),
}));

그리고 각 필드를 컴포넌트화 해서
필요한 상태와 action을 import해서 각 input에 해당하는 컴포넌트에 사용해서 해결

이렇게 하니깐 전 보다는 최적화(렌더링)에서는 좋은데 문제는 이제 코드가 생각보다 깔끔하지는 않음
추상화 팩토리 패턴을 도입해서 각 Input을 재사용하는 방향이 필요할거같음

profile
Async FE 취업 준비중.. Await .. (취업완료 대기중) ..

0개의 댓글