선언적 폼 바인딩

선언적 폼 바인딩

1. 서론

웹 애플리케이션 개발에서 다이얼로그(Dialog)나 모달 화면을 통한 데이터 수정 폼(Form)을 구현하는 것은 가장 흔하면서도 복잡도가 높은 작업 중 하나입니다. 특히 관리자 페이지나 대시보드의 '콘텐츠 상세 정보 수정 화면'과 같이, 화면이 열리는 시점에 백엔드 API로부터 비동기로 상세 데이터를 조회하여 폼의 초기값으로 바인딩해야 하는 구조에서는 더욱 세밀한 상태 제어가 요구됩니다.

초기 컴포넌트 설계 시에는 가장 직관적인 방식인 useEffect 훅을 활용하여 외부 Props로 전달된 상세 데이터의 변경을 감지하고, 폼 관리 상태에 수동으로 값을 주입하는 명령형(Imperative) 접근 방식을 채택하였습니다. 그러나 이 방식은 고도화 단계에서 예기치 못한 동시성 이슈를 발생시켰습니다. 사용자가 다이얼로그를 빠르게 닫았다가 다른 아이템을 선택해 다시 열 때, 비동기 데이터 패칭 타이밍과 맞물려 이전 데이터의 찌꺼기가 입력 필드에 잠시 남아있거나, 훅의 호출 순서와 컴포넌트 렌더링 라이프사이클이 어긋나면서 상태가 유실되는 버그가 빈번하게 관찰되었습니다.

UI 레이어의 가독성을 저하시키고 데이터 정합성을 위협하는 이 고질적인 문제를 해결하기 위해, 절차지향적인 useEffect를 과감히 걷어내고 React Hook Form 라이브러리의 핵심 아키텍처를 활용하여 선언적(Declarative) 상태 동기화를 달성한 실무 리팩토링 사례를 공유하고자 합니다.

2. 기술 선택 배경

React에서 폼 상태를 제어할 때 흔히 범하는 안티 패턴 중 하나는 데이터의 흐름을 useEffect 내에서 제어하려는 시도입니다. Props로 받아온 데이터가 변경될 때마다 특정 상태를 수동으로 동기화하는 구조는 다음과 같은 한계를 가집니다.

  1. 상태 동기화 타이밍의 불일치: React의 상태 업데이트는 비동기적으로 배치(Batch) 처리되므로, useEffect가 실행되어 폼 값을 업데이트하기 전에 브라우저가 이전 상태를 기반으로 화면을 한 번 그려내면서 시각적 잔상이나 유효성 검사 오류를 유발합니다.

  2. 명령형 코드의 비대화: 입력 필드가 많아질수록 각 필드의 초기화 로직과 조건문들이 컴포넌트 내부 곳곳에 흩어지게 되어, 1,000줄이 넘어가는 비대하고 읽기 힘든 스파게티 컴포넌트를 양산하게 됩니다.

React Hook Form은 이러한 한계를 극복할 수 있는 선언적 속성인 values 옵션을 제공합니다. 컴포넌트 내부에 상태를 리셋하는 명령을 직접 기술하는 대신, 폼이 바라보는 데이터 소스를 객체 형태로 명시적으로 선언해 두면 라이브러리가 리액트의 렌더링 파이프라인과 동기화하여 상태 찌꺼기를 안전하게 털어내고 새로운 데이터 인스턴스를 유지해 줍니다.

따라서 컴포넌트 라이프사이클에 안전하게 안착하면서도 코드 가독성과 개발자 경험을 극대화할 수 있는 솔루션으로 선언적 바인딩 패턴을 도입하게 되었습니다.

3. 실제 적용 과정 및 트러블슈팅

본 프로젝트의 실제 상세 정보 수정 다이얼로그 설계 사상을 바탕으로, 버그를 유발하던 절차적 구조를 선언적 구조로 리팩토링한 과정을 구체적인 코드 블록과 함께 설명합니다.

3.1 useEffect 기반 명령형 바인딩 구조 (Before)

기존에 작성된 구조에서는 외부에서 API 요청의 결과물인 itemDetail 객체가 유입될 때마다, 하위 다이얼로그 내부에서 useEffect 훅이 매번 실행되며 setValue 메서드를 통해 폼의 개별 필드를 수동으로 채워 넣었습니다.

export const ModifyItemDialogBefore = ({ itemDetail, onClose }) => {
  const { register, handleSubmit, setValue } = useForm({
    defaultValues: {
      title: '',
      category: '',
      description: '',
    },
  });

  // 외부 데이터가 바뀔 때마다 수동으로 상태를 주입하는 안티 패턴
  useEffect(() => {
    if (itemDetail) {
      setValue('title', itemDetail.title ?? '');
      setValue('category', itemDetail.category ?? '');
      setValue('description', itemDetail.description ?? '');
    }
  }, [itemDetail, setValue]);

  const onSubmit = (data) => {
    console.log('서버 전송 데이터:', data);
  };

  return (
    <Dialog open={true} onClose={onClose}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register('title')} placeholder="제목" />
        <button type="submit"> 수정 </button>
      </form>
    </Dialog>
  );
};

위의 구조는 비동기 패칭 속도가 지연되거나 사용자가 다이얼로그를 빠르게 껐다 켤 때, itemDetail이 미처 도달하지 못한 찰나의 순간 동안 이전 컴포넌트의 이전 데이터가 폼에 그대로 남아 표출되는 데이터 오염 현상을 유발했습니다.

3.2 values 속성을 활용한 선언적 바인딩 구조 (After)

이를 해결하기 위해 useForm 내부의 values 리액티브 속성을 도입하고 인터페이스를 ModifyItemFormValue 구조로 단일화하여 폼 스스로 데이터 변경에 대응하도록 리팩토링했습니다.

interface ModifyItemFormValue {
  title: string;
  category: string;
  date: Dayjs | null;
  description: string;
  details: string;
}

export const ModifyItemDialogAfter = ({ itemDetail, onClose }) => {
  // values 옵션을 선언해 두면, useEffect 없이도 데이터 변경 시 내부 상태가 자동 동기화됨
  const methods = useForm<ModifyItemFormValue>({
    values: {
      date: itemDetail?.updatedAt ? dayjs(itemDetail.updatedAt) : dayjs(),
      title: itemDetail?.title ?? '',
      category: itemDetail?.category ?? '',
      description: itemDetail?.description ?? '',
      details: itemDetail?.details ?? '',
    },
  });

  const { handleSubmit, register } = methods;

  const onSubmit = handleSubmit((data: ModifyItemFormValue) => {
    const baseDate = dayjs(data.date).startOf('day').valueOf();
    console.log('정합성이 보장된 타임스탬프 데이터 전송:', baseDate);
  });

  return (
    <Dialog open={true} onClose={onClose}>
      <Form methods={methods} onSubmit={onSubmit}>
        <div className="flexColumn gap16">
          <Field.Text name="title" placeholder="제목" />
          <Field.Text name="details" placeholder="상세 내용" multiline rows={2} />
        </div>
        <button type="submit">수정 완료</button>
      </Form>
    </Dialog>
  );
};

이렇게 리팩토링을 거치면, React Hook Form 라이브러리가 리액트의 가상 DOM 갱신 타이밍에 맞춰 데이터 세트를 원자적으로(Atomically) 교체하므로, 화면에 이전 데이터 찌꺼기가 노출되거나 폼 상태가 꼬이는 레이스 컨디션 버그가 근본적으로 완전 차단됩니다.

4. 적용 결과 및 성과

부재 정보 수정 화면을 포함하여 사내 주요 입력 폼 레이어에 React Hook Form의 values 선언적 바인딩 아키텍처를 전면 확산 적용한 결과, 다음과 같은 성과를 달성하였습니다.

  1. 상태 꼬임 현상 및 UI 잔상 버그 제로화: 다이얼로그 호출 속도나 비동기 통신 가용성과 무관하게 데이터 찌꺼기 노출 현상이 완벽하게 해결되었으며, 이로 인한 오작동 문의 건수가 기존 대비 전무하게 개선되었습니다.

  2. 코드 베이스 가독성 및 생산성 향상: 무분별하게 선언되어 가독성을 저해하던 컴포넌트 내부의 useEffect 구문들이 제거되면서 모달 관련 전체 소스 코드 라인 수가 평균 25% 이상 감소하는 슬림화를 이루었습니다.

결합도 완화 및 데이터 안정성 확보: 비즈니스 폼 데이터 가공(ModifyItemFormValue) 로직이 뷰 레이어와 철저히 단방향 흐름으로 디커플링(Decoupling)되어, 이전 상태의 부사상 효과에 영향을 받지 않는 순수한 컴포넌트 격리성을 확보했습니다.

5. 결론

이번 프로젝트 수행 과정에서 맞닥뜨린 상태 꼬임 버그와 리팩토링 경험은, 프런트엔드 아키텍처 설계 시 상태의 변화 흐름을 개발자가 부수 효과(Side Effect)를 통해 억지로 명령하고 추적하기보다, 프레임워크와 라이브러리가 제공하는 선언적 사상에 맞추어 의존성을 명확히 선언해 두는 것이 얼마나 강력한지 증명하는 좋은 계기였습니다.

React Hook Form의 values 속성을 활용한 라이프사이클 매핑 기법은, 단순한 버그 패치를 넘어 시스템의 아키텍처적 견고함을 한 단계 끌어올리는 자산이 되었습니다. 대규모 데이터를 다루고 입력 필드가 복잡하게 얽히는 엔터프라이즈 환경일수록 이와 같은 데이터 정합성 보장 패턴은 필수적이며, 이번에 정립한 선언형 폼 아키텍처 튜닝 경험을 사내 기술 컴포넌트 표준 가이드라인으로 자산화하여 전사 개발 생태계의 복잡도를 낮추는 데 적극적으로 기여하고자 합니다.

* Reference

- React Hook Form - useForm API Document

- React Official Document - Synchronizing with Effects

- React Official Document - You Might Not Need an Effect

D.Hyeok

Site footer