
회원가입은 물론 여러가지 형태의 음료 등을 등록해야했던 만큼 다양한 형태의 확장된 FormData를 제출해야했다.
처음에는 정말 아래와 같이 무작정 삼항연산자로 조건을 작성하고 에러 전체를 error 상태에 저장을 했다
const [error, setError] = useState(initialErrorState);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
...
const errors: ErrorState = {
nickname:
signUpFormData.nickname.trim() === "" ? "닉네임을 입력해주세요." : "",
birth: signUpFormData.birth.trim() === "" ? "나이를 입력해주세요." : "",
gender: signUpFormData.gender.trim() === "" ? "성별을 선택해주세요." : "",
activityLevel:
signUpFormData.activityLevel.trim() === ""
? "활동 수준을 선택해주세요."
: "",
height: signUpFormData.height === "" ? "키를 입력해주세요" : "",
weight: signUpFormData.height === "" ? "몸무게를 입력해주세요" : "",
};
const checkError = Object.values(errors).some((e) => e !== "");
if (checkError) {
setError(errors);
return;
}

하지만
1. 여러가지 form마다 if문이나 삼항연산자를 쭉 나열하듯 다 작성할 순 없었고,
2. UI상 당장 모든 input에서 발생하는 에러를 다 저장해서 보여줄 필요도 없었다.(상단에서부터 하나만 반환해도 충분)
유효성 검사를 위한 config를 따로 작성하고, ValidationConfigType또한 따로 작성해준다(추후 확장에 용이)
const signUpValidationConfig: Record<string, ValidationConfigType> = {
nickname: {
required: true,
maxLength: 8,
emptyMessage: "닉네임을 입력해주세요",
errorMessage: "닉네임은 최대 8자까지만 입력할 수 있습니다.",
},
height: {
required: true,
emptyMessage: "키를 입력해주세요.",
},
weight: {
required: true,
emptyMessage: "몸무게를 입력해주세요.",
},
...
};
export type ErrorStateType = {
[key: string]: string;
};
export type ValidationConfigType = {
required: boolean;
// 조건
maxLength?: number;
minValue?: number;
// 메시지
errorMessage?: string;
emptyMessage?: string;
};
form의 유효성 검사를 수행하려는 컴포넌트의 submit로직에 해당 에러상태에 맞는 key에 error를, 그리고 error메시지를 반영하는 로직을 넣어준다
const [error, setError] = useState<ErrorStateType>(initialErrorState);
const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const checkErrorOrder: string[] = [
"nickname",
"height",
"weight",
"birth",
"gender",
"activityLevel",
];
// error를 검증한 후
const formError: ErrorStateType = validateFormData(
signUpValidationConfig,
signUpFormData,
checkErrorOrder
);
// error가 존재할 경우 해당 error와 error메시지를 반환
if (Object.keys(formError).length > 0) {
const [firstErrorKey, firstError] = Object.entries(formError)[0];
setError((prev) => ({
...prev,
[firstErrorKey]: firstError,
}));
setErrorMessage(firstError);
return;
}
// API 요청
...
};
검증config, data, 검증순서를 담은 배열을 받아 순서대로 에러를 하나씩 반환하는 함수
import { ErrorStateType, ValidationConfigType } from "@/types/validationTypes";
export default function validateFormData<T>(
// 검증config
validationConfig: Record<string, ValidationConfigType>,
// data
formdata: T,
// 검증의 순서
checkErrorOrder: string[]
) {
const errors: ErrorStateType = {};
// 배열로서 검증 순서를 지킴
for (const key of checkErrorOrder) {
const config = validationConfig[key];
let value: unknown = formdata[key as keyof T];
if (typeof value === "string") {
value = value.trim();
}
// 바로 return함으로써 오류 하나만 반영(삭제시 여러개 반영 가능)
if (config.required && value === "") {
// 1. 비어있는 경우 판단
errors[key] = config.emptyMessage || "";
return errors;
} else if (
// 2. maxLength판단
typeof value === "string" &&
config.maxLength !== undefined &&
value.length > config.maxLength
) {
errors[key] = config.errorMessage || "";
return errors;
} else if (
// 3. minValue 초과 입력 판단
typeof value === "number" &&
config.minValue !== undefined &&
value <= config.minValue
) {
errors[key] = config.errorMessage || "";
return errors;
}
}
return errors;
}

상단에서부터 1개씩 해당 input의 error상태(빨간색) 하단에 error메시지를 보여준다
Record<K,T>
기본적으로 객체로서 두가지 타입 인자를 받아 전자는 Key로, T는 value로서의 타입을 담당한다.
즉, 객체의 모든 속성이 같은 타입을 갖도록 만들 수 있어 각 상태의 타입을 명시적으로 정의하는 데 유용하며 type의 일관성을 보장받을 수 있다.
사실, checkErrorOrder가 들어가는 로직이 없어도 Config 순서대로도 위에서부터 검증이 작동하는 과정을 확인했다.
하지만...Object는 순서가 보장되지 않는 것로 아는데 왜 순서가 보장이 된걸까?
이에 대해 찾아보니 같은 의문을 가졌던 블로그글이 나왔는데
Es2015(es6)기준으로
1. string 타입의 키 값은 넣는 순서를 보장
2. 정수형 키 값은 오름차순으로 정렬되며 순회 시 string형 보다 먼저 접근됨
3. Symbol 타입 또한 넣는 순서 보장
이라고 한다.
그 외 대체가능한 순서 보장 방법으로 다음과 같이 방법이 있는데
구형브라우저에서 이를 보장하지 않는다는 점도 있고, Config파일(너무 길다)이 따로 분리되거나 최상단에 위치하여 순서를 확인하기 힘든 점을 고려했을 때, 위 3가지 선택지 중 배열로서 따로 명시하는것을 선택했다.
이게 무조건 맞는 선택이라고는 할 수 없지만 나름의 이유는 이렇다..
그 외, 제네릭 타입을 이론적으로 배우거나.. 가끔 라이브러리 props나 타입을 찾아들어가보면서 봐왔는데, 이번에 직접 기능을 제작하는데 쓰였다는 점이 굉장히 좋았다.
formdata: T)을 활용함으로서 각자의 특정한 데이터 타입에 의존하지 않고 사용할 수 있었다.(재사용성과 유연성이 높아짐)+)사실 React-Hook-Form이라는 라이브러리(비제어 컴포넌트로서 리렌더링을 최소화시킴 등)가 있다는 걸 중간에 알았지만 일단 끝까지 구현해보고 싶어 이렇게 해 보았다. 하지만 에러 유효성 검사를 하며 느낀 렌더링 횟수를 보니 사용을 앞으로 사용안할 이유가 없어보인다...
다음 프로젝트 form 관련 로직이 필요하다면 위 라이브러리를 사용해볼 수 있지 않을까?
https://developer-talk.tistory.com/296
https://velog.io/@hkh9601/JS-객체의-속성-순서