관리 메뉴

즐겁게, 코드

vee-validate에 타입스크립트 적용하기 (@vee-validate/yup) 본문

🎨 프론트엔드/Vue.js

vee-validate에 타입스크립트 적용하기 (@vee-validate/yup)

Chamming2 2024. 12. 19. 15:25

공식 문서에 있는 간단한 내용이긴 하지만 기록해두면 좋을 것 같아 정리해 보았다.

React-Hook-Form (RHF)의 register 함수와는 달리, vee-validate의 defineField 함수는 기본적으로 타입 추론을 제시하지 않는다.

 

이것이 무슨 말인가 하면...

const { values, errors, defineField } = useForm({
  validationSchema,
});

const validationSchema = yup.object({
  startDate: yup
    .date()
    .required('날짜를 입력해 주세요')
    .max(yup.ref('endDate'), '시작일을 종료일 이전으로 입력해 주세요'),
  endDate: yup.date().required('날짜를 입력해 주세요'),
});

// ❌ 별도의 설정이 없다면 '<스키마의 키 값>' 은 타입스크립트에 의해 제안되지 않는다.
const [startDate] = defineField('<스키마의 키 값>');

trigger suggestion을 실행해도 스키마의 키 값이 추론되지 않는다.

vee-validate가 키 값을 추론하기 위해서는 yup이 타입스크립트 지원을 받을 수 있도록 @vee-validate/yup 패키지를 추가로 설치할 수 있다.

# @vee-validate/yup 패키지 설치
npm install @vee-validate/yup
// toTypedSchema 는 yup이 vee-validate에 타입 추론을 제공할 수 있도록 도와준다.
import { toTypedSchema } from '@vee-validate/yup';

const validationSchema = toTypedSchema(yup.object({
  startDate: yup
      .date()
      .required('날짜를 입력해 주세요')
      .max(yup.ref('endDate'), '시작일을 종료일 이전으로 입력해 주세요'),
  endDate: yup.date().required('날짜를 입력해 주세요'),
}));

const [startDate] = defineField(''); // ✅ defineField 함수 인자의 타입이 추론된다.

defineField 함수 인자에 타입 추론이 가능해진 모습

만약 yup이나 zod 등의 검증 라이브러리를 사용하지 않거나, 라이브러리를 추가로 설치하는 것이 마음에 들지 않는다면 타입을 직접 제네릭으로 전달할 수도 있다.

const validationSchema = yup.object({
  startDate: yup
      .date()
      .required('날짜를 입력해 주세요')
      .max(yup.ref('endDate'), '시작일을 종료일 이전으로 입력해 주세요'),
  endDate: yup.date().required('날짜를 입력해 주세요'),
});

// toTypedSchema를 사용하지 않고, useForm에 직접 제네릭 타입을 정의할 수도 있다.
const { values, errors, defineField } = useForm<{
  startDate: string;
  endDate: string;
}>({
  validationSchema,
});

const [startDate] = defineField(''); // ✅ defineField 함수 타입이 추론된다.

의도치 않게(?) Vue 개발을 주로 하게 되면서 React의 RHF을 사용하지 못한다는 점이 크게 아쉬웠는데, 생각보다 vee-validate를 사용한 개발 경험에 만족하는 중이다.

번외

defineField("키") 함수는 Path<TValues> 타입을 "키" 의 타입으로 기대한다.

그런데 useForm이 제네릭으로 받는 TValues 타입이 어떤 마법을 거쳐 Path<TValues> 타입으로 전환되어 전달될지가 궁금했다.

// useForm()
// TValues 타입을 제네릭 첫 번째 인자로 받는다.
declare function useForm<TValues extends GenericObject = GenericObject, TOutput extends GenericObject = TValues, TSchema extends FormSchema<TValues> | TypedSchema<TValues, TOutput> = FormSchema<TValues> | TypedSchema<TValues, TOutput>>(opts?: FormOptions<TValues, TOutput, TSchema>): FormContext<TValues, TOutput>;

// defineField()
// Path<TValues> 타입을 제네릭 첫 번째 인자로 받는다.
defineField<TPath extends Path<TValues>, TValue = PathValue<TValues, TPath>, TExtras extends GenericObject = GenericObject>(path: MaybeRefOrGetter<TPath>, config?: Partial<InputBindsConfig<TValue, TExtras>> | LazyInputBindsConfig<TValue, TExtras>): [Ref<TValue>, Ref<BaseFieldProps & TExtras>];

// 둘의 타입이 다른데, 어떻게 TValues 타입이 Path<TValues> 타입으로 전환되는 걸까?

아직 긴 코드를 분석하는 능력은 부족해 GPT에 질의했더니 FormContext의 존재를 알게 되었다.

📄 질문 링크 : https://chatgpt.com/share/6763b59d-dd20-8008-89ca-6783e29d4c8d

toTypedSchema를 살펴보면 스키마를 인자로 받아 TypedSchema로 캐스팅해 리턴한다.

toTypedSchema<TSchema extends Schema, TOutput = InferType<TSchema>, TInput = PartialDeep<TOutput>>(
  yupSchema: TSchema,
  opts: ValidateOptions = { abortEarly: false },
): TypedSchema<TInput, TOutput>

toTypedSchema 함수를 통해 validationSchemaTypedSchema 타입을 전달할 수 있게 되면 validationSchemaTypedSchema<TValues, TOutput>이 되고, 제네릭에 전달된 TValues 타입이 defineFields에 전달되는 것으로 보인다.

// useForm 컴포저블의 FormOptions 중 
validationSchema?: MaybeRef<TSchema extends TypedSchema ? TypedSchema<TValues, TOutput> : any>;

// * TValues는 기본적으로 Record<string, any> 타입이다.

Yup.object()로 정의한 스키마 타입 (ObjectSchema ⊂ YupSchema)
toTypedSchema로 래핑된 스키마 타입

 

오픈소스를 사용할 때 원리를 알고 쓰려 노력하는 중인데, 타입스크립트 관문을 통과해야 하는게 쉽지 않은 것 같아 더 공부해야겠다. 🙇‍♂️

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆