Skip to content

RHF 도입기: 온보딩 프로필 폼 구조 전환 준비

FE_juncci edited this page Apr 2, 2026 · 1 revision

배경: 왜 RHF를 도입했는가

기존 온보딩 프로필 폼은 useState 기반으로 상태를 관리하고 있었고, 폼이 커지면서 다음 문제가 명확해졌다.

  • 필드 상태가 각각 분산 (selectedJob, nickname, 약관 등)
  • 제출 시점에 수동 검증 로직 작성
  • 버튼 활성화 조건과 실제 검증 로직이 이중화

👉 폼 상태 / 검증 / 제출 흐름이 하나의 시스템으로 묶여 있지 않았다

특히 설문조사 폼 확장을 고려하면 이 구조는 유지보수 비용이 급격히 증가할 가능성이 높았다.

그래서 폼을 하나의 컨텍스트로 통합 관리하기 위해 RHF를 도입했다.


RHF 도입 목표

이번 작업의 핵심 목표는 단순 라이브러리 도입이 아니라 다음이다.

  • 폼 상태를 단일 소스로 통합
  • 검증 흐름을 선언적으로 정리
  • 제출 흐름의 진입점을 명확히 통제
  • 기존 UI 영향 최소화

주요 변경 사항


폼 상태를 RHF 기반으로 전환

대상 파일

  • useOnboardingFormState.client.ts

변경 전

  • 모든 필드를 useState로 개별 관리
const [selectedJob, setSelectedJob] = useState(null);
const [selectedCareer, setSelectedCareer] = useState(null);
const [nickname, setNickname] = useState('');

👉 문제

  • 상태 분산
  • 필드 추가 시 관리 포인트 증가

변경 후 (핵심)

👉 폼 상태 저장소를 RHF로 이동

const { control, setValue, getValues } = useForm<OnboardingFormValues>({
  defaultValues: {
    selectedJob: null,
    selectedCareer: null,
    selectedTech: [],
    nickname: '',
    introduction: '',
    termsAgreed: false,
    privacyAgreed: false,
    pledgeAgreed: false,
  },
});

값 조회 → useWatch로 통일

const selectedJob = useWatch({ control, name: 'selectedJob' });
const nickname = useWatch({ control, name: 'nickname' }) ?? '';

👉 의미

상태를 직접 들고 있는 게 아니라 “폼 상태를 구독”하는 구조


값 변경 → setValue로 통일

const setNickname = (value: string) => {
  setValue('nickname', value, { shouldDirty: true, shouldValidate: true });
};

👉 포인트

  • dirty 상태 자동 반영
  • validation 트리거 통합

복합 상태 처리도 RHF 기준으로 변경

const handleTechToggle = (skill: Skill) => {
  const currentTech = getValues('selectedTech');

  const exists = currentTech.some((item) => item.id === skill.id);
  const next = exists
    ? currentTech.filter((item) => item.id !== skill.id)
    : [...currentTech, skill];

  if (!exists && next.length > 5) {
    setTechLimitMessage('기술 스택은 최대 5개까지 선택할 수 있어요.');
    return;
  }

  setSelectedTech(next);
};

👉 핵심 변화

기존: 로컬 상태 기반 변경: RHF 상태를 기준으로 로직 수행


중요한 설계 포인트

return {
  selectedJob,
  setSelectedJob,
  nickname,
  setNickname,
};

👉 외부 API 유지

UI 레이어는 그대로 두고 내부 구현만 RHF로 교체


제출 흐름 정리 (검증 진입점 통합)

대상 파일

  • useOnboardingProfileForm.client.ts

변경 내용

👉 제출 전에 상위 훅에서 선 검증 수행

const handleSubmit = async () => {
  const trimmedNickname = form.nickname.trim();

  if (!form.selectedJob || !form.selectedCareer) {
    submit.setSubmitError('직무와 경력을 선택해 주세요.');
    return;
  }

  if (!isExpert && form.selectedTech.length === 0) {
    submit.setSubmitError('기술 스택을 선택해 주세요.');
    return;
  }

  if (!trimmedNickname) {
    submit.setSubmitError('닉네임을 입력해 주세요.');
    return;
  }

  if (!allRequiredAgreed) {
    submit.setSubmitError('필수 약관에 동의해 주세요.');
    return;
  }

  await submit.handleSubmit(nicknameLimit);
};

👉


설계 의도

  • 검증 책임을 submit 훅에서 분리
  • “폼 레벨 검증”을 명확히 드러냄

👉 결과

검증 흐름의 진입점이 한 곳으로 정리됨


3-3. submit 훅 인터페이스 확장

대상 파일

  • useOnboardingSubmit.client.ts

변경 내용

return { isSubmitting, submitError, setSubmitError, handleSubmit };

핵심 포인트

👉 setSubmitError를 외부에 노출


왜 필요한가

이전에는

  • submit 내부에서만 에러 처리 가능

변경 후:

  • 상위 훅에서 선 검증 → 동일 에러 채널 사용

결과

에러 흐름이 하나로 통합됨

  • UI 에러 표시
  • 서버 에러
  • 사전 검증 에러

👉 전부 submitError로 수렴


전체 구조 변화 요약

Before

  • 상태: useState 분산
  • 검증: submit 내부 수동 처리
  • UI 조건 vs 실제 검증 분리

After

  • 상태: RHF 단일 컨텍스트
  • 검증: 상위 훅 + submit 훅 분리
  • 에러: 단일 채널 (submitError)

이번 작업에서 중요한 설계 판단

1) “RHF를 전부로 쓰지 않았다”

  • 서버 로직 (닉네임, 이메일) → 별도 훅 유지
  • RHF는 폼 상태 + 검증까지만 담당

👉 역할 분리 유지


2) UI API는 유지했다

setNickname()
setSelectedJob()

👉 이유

리팩토링 범위를 최소화하기 위해


RHF를 “싱글 소스”로 설정

  • useWatch
  • setValue
  • getValues

👉 모든 상태 흐름이 여기 기준으로 동작


이번 작업에서 얻은 인사이트

1) 폼은 단순 상태 묶음이 아니다

폼 = 상태 + 검증 + 제출 흐름


진짜 중요한 건 “검증 진입점”

RHF보다 더 중요한 변화는

👉 검증이 어디서 시작되는지 명확해진 것


3) 이건 온보딩만의 문제가 아니다

이번 구조는

앞으로 모든 폼의 기본 패턴이 될 수 있음


Clone this wiki locally