홍풍 프로젝트를 하면서 폼 검증이 항상 골칫거리였다. 특히 BasicInput 컴포넌트를 사용할 때마다 각각의 입력 필드에 대한 검증 로직을 일일이 작성해야 하는 번거로움이 있었다.

기존에는 단순히 useState로 입력값을 관리하고, onBluronSubmit 시점에 검증 함수를 호출하는 방식이었다. 하지만 이 방식은 몇 가지 문제가 있었다:

  1. 검증 로직의 중복: 비슷한 검증 로직을 여러 컴포넌트에서 반복 작성
  2. 타입 안전성 부족: 런타임에서만 에러를 발견할 수 있음
  3. 상태 관리의 복잡성: 검증 상태와 입력 상태를 별도로 관리해야 함
  4. 일관성 부족: 각 컴포넌트마다 다른 검증 방식 사용

이런 문제들을 해결하기 위해 Zod를 활용한 타입 안전한 폼 검증 훅을 만들어보기로 했다.


기존 BasicInput 컴포넌트의 문제점

현재 BasicInput 컴포넌트는 다음과 같은 구조로 되어 있다:

type InputProps = {
  label: string;
  inputValue: string;
  setInputValue: (text: string) => void;
  validationCondition: ValidationState;
  onBlur?: () => void;
  onFocus?: () => void;
  // ... 기타 props
};

export const BasicInput = forwardRef<TextInput, InputProps>(
  ({
    label,
    inputValue,
    setInputValue,
    validationCondition,
    onBlur,
    onFocus,
    // ... 기타 props
  }, ref) => {
    // ... 컴포넌트 로직

    return (
      <View style={[styles.inputGroup]}>
        <Animated.Text style={[styles.labelText, labelStyle]}>
          {label}
          {requireMark && <Text style={{ color: "red" }}>*</Text>}
        </Animated.Text>
        <TextInput
          ref={ref}
          style={[
            styles.InputBox,
            {
              borderBottomColor:
                validationCondition?.state !== "ERROR"
                  ? underlineColor
                  : Color["red500"],
            },
          ]}
          value={inputValue}
          onChangeText={setInputValue}
          onFocus={onFocus}
          onBlur={onBlur}
          // ... 기타 props
        />
        {validationCondition?.state === "ERROR" &&
          validationCondition.errorText.length > 0 && (
            <Text style={styles.errorText}>
              {validationCondition?.errorText}
            </Text>
          )}
      </View>
    );
  },
);

이 컴포넌트는 validationCondition을 외부에서 받아서 에러 상태를 표시하는 구조다. 하지만 이 방식은 각 사용처에서 검증 로직을 일일이 작성해야 한다는 문제가 있다.

ValidationState 타입 정의

먼저 검증 상태를 관리하기 위한 타입을 정의했다:

export type ValidationState =
  | {
      state: "PENDING" | "BEFORE" | "VALID";
    }
  | {
      state: "ERROR";
      errorText: string;
    };

이 타입은 검증의 4가지 상태를 표현한다:

특히 PENDING 상태는 두 가지 경우에 사용된다:

  1. 사용자 입력 중: 사용자가 타이핑하고 있을 때 일시적으로 검증을 보류
  2. 비동기 검증 중: 서버에 검증 요청을 보내고 응답을 기다리는 중

useValidatedForm 훅 구현