진행 예정일을 선택하지 않더라도, 아래의 '번쩍 개설하기' 버튼이 활성화되기를 바랐습니다.
해당 프로젝트에서는 react-hook-form 과 유효성 검사를 위한 zod 및 zodResolver를 사용하고 있기에, 정의해둔 스키마를 살펴봐야 했어요. 정의한 스키마는 아래와 같았습니다.
export const flashSchema = z.object({
title: z
.string()
.max(30, { message: '30자 까지 입력할 수 있습니다.' })
.min(1, { message: '모임 제목을 입력해주세요.' }),
desc: z
.string()
.min(1, {
message: '번쩍 설명을 입력해주세요.',
})
.max(500, { message: '500자 까지 입력 가능합니다.' }),
timeInfo: z
.object({
time: z.object({
label: z.string(),
value: z.string(),
}),
startDate: z
.string()
.min(10, { message: '번쩍 기간을 입력해주세요.' })
.max(10, { message: 'YYYY.MM.DD 형식으로 입력해주세요.' })
.optional()
.refine(datetime => dayjs(datetime, 'YYYY.MM.DD').isValid(), {
message: 'YYYY.MM.DD 형식으로 입력해주세요.',
}),
endDate: z
.string()
.optional()
.refine(datetime => (datetime ? dayjs(datetime, 'YYYY.MM.DD').isValid() : true), {
message: 'YYYY.MM.DD 형식으로 입력해주세요.',
}),
})
.refine(
data =>
data.time.label === '하루' ||
(data.time.label === '기간' && data.endDate && data.endDate.length === 10) ||
data.time.label === '협의 후 결정'
),
placeInfo: z
.object({
place: z.object({
label: z.string(),
value: z.string(),
}),
placeDetail: z.string().optional(),
})
.refine(data => {
if (data.place.label === '오프라인' || data.place.label === '온라인') {
return data.placeDetail && data.placeDetail.length > 0;
} else if (data.place.label === '협의 후 결정') {
return true;
}
return false;
}),
minCapacity: z
.number({
required_error: '모집 인원을 입력해주세요.',
invalid_type_error: '모집 인원을 입력해주세요.',
})
.gt(0, { message: '0보다 큰 값을 입력해주세요.' })
.lte(999, { message: '모집 인원을 다시 입력해주세요.' }),
maxCapacity: z
.number({
required_error: '모집 인원을 입력해주세요.',
invalid_type_error: '모집 인원을 입력해주세요.',
})
.gt(0, { message: '0보다 큰 값을 입력해주세요.' })
.lte(999, { message: '모집 인원을 다시 입력해주세요.' }),
files: z.array(z.string()),
welcomeTags: z
.array(
z
.object({
label: z.string(),
value: z.string(),
})
.optional()
.nullable()
)
.optional()
.nullable(),
});
export type FlashFormType = z.infer<typeof flashSchema>;
여기서, startDate의 값이 없어도 (혹은 빈 문자열이어도)
const formMethods = useForm<FlashFormType>({
mode: 'onChange',
resolver: zodResolver(flashSchema),
});
const { isValid, errors } = formMethods.formState;
와 같은 코드로 얻은 isValid 의 값이 true가 되도록 하고 싶었습니다.
그런데 이미 timeInfo.startDate 의 값은 .optional()로 필수가 아닌 값으로 정의되어 있었습니다. 여기서 저는 한참 해맸습니다.
.refine()이 정의되어 있으면 .optional()의 우선 순위가 밀려서 무시되기도 하는건가? 하는 고민도 해보고
애초에 .optional() 의 사용 방법이 잘못된건가? 하고 사용 방법을 찾아보기도 했습니다.
그 과정에서 얻은 지식은 다음과 같습니다.
zod에서 optional로 설정된 필드는 해당 필드가 undefined이거나 null일 때 유효성 검사를 통과한다.
하지만 refine 메서드를 사용하면, optional 필드라도 refine 내부에서 추가적인 검사를 수행할 수 있다. 이때 refine 내부에서 optional 필드에 대한 추가적인 조건을 검사하게 되면, 해당 조건을 만족하지 않으면 유효성 검사가 실패하게 된다.
아! 그럼 위의 코드에서는 .refine이 존재하니까 아래의 코드를
startDate: z
.string()
.min(10, { message: '번쩍 기간을 입력해주세요.' })
.max(10, { message: 'YYYY.MM.DD 형식으로 입력해주세요.' })
.optional()
.refine(datetime => dayjs(datetime, 'YYYY.MM.DD').isValid(), {
message: 'YYYY.MM.DD 형식으로 입력해주세요.',
}),
아래와 같이 바꾸면 되겠네요!
startDate: z
.string()
.min(10, { message: '번쩍 기간을 입력해주세요.' })
.max(10, { message: 'YYYY.MM.DD 형식으로 입력해주세요.' })
.optional()
.refine(datetime => (datetime ? dayjs(datetime, 'YYYY.MM.DD').isValid() : true), {
message: 'YYYY.MM.DD 형식으로 입력해주세요.',
}),
... 하지만 여전히 isValid는 false가 떴습니다. 저는 여기서 또 refine의 콜백 함수 내부에 console을 찍어서 return 하는 값이 true가 맞는지 확인해보았습니다. true가 맞았습니다. 원인을 찾았나 싶었는데, 여전히 또다른 원인이 있나보네요.
많은 시도를 해보면서, 결국 숨어있던 원인을 하나 더 찾았습니다.
빈 문자열을 refine 검사에서 true로 처리하고 있지만, 실제로는 Zod의 string() 메소드가 min(10)이나 max(10)을 검증할 때 ''을 허용하지 않기 때문에, ''이 전달되면 min과 max 검증을 거쳐서 결국 실패하게 됩니다.
한번 더 총체적으로 설명하자면,
.optional()은 해당 값이 null 이거나 undefined 면 유효성 검사를 통과시켜주는 메서드인데 '' 이므로 optional에 의해 통과되지 못하고, optional이 존재하든 말든 refine의 실행 결과에 따라 유효성 검사의 통과 여부가 결정됩니다...만, 그 전에 .string()과 .max 및 .min 체이닝 메서드로 유효성 검사를 먼저 하기 때문에 최종적으로는 유효성 검사 결과 false가 되어버리는 것이었습니다.
따라서 다음과 같은 코드가 되어야만 startDate값이 '' 일때도, null 일때도, undefined 일때도, 또한 값이 존재한다면 dayjs 형식에 맞을 때도 유효성 검사를 통과할 수있습니다.
startDate: z
.string()
.optional()
.refine(datetime => (datetime ? dayjs(datetime, 'YYYY.MM.DD').isValid() : true), {
message: 'YYYY.MM.DD 형식으로 입력해주세요.',
}),
알고 나면 참 간단한데, 원인을 찾고 해결 방법을 찾기 까지 꽤 오래 걸렸네요.
'Develop > Frontend' 카테고리의 다른 글
클로저 (Closure) (0) | 2025.02.06 |
---|---|
실행 컨텍스트 (Execution Context) (0) | 2025.02.05 |
useCallback은 도대체 언제 사용하는게 좋을까? (0) | 2025.01.09 |
pnpm 도입기 (왜 yarn berry가 아닌가?) (0) | 2025.01.03 |
Web Socket을 도입하며 겪었던 트러블 슈팅들 (0) | 2024.12.31 |