맞춤법 검사를 원하는 단어 프로젝트를 진행하면서 고민했던 부분은 회원가입 폼에서 인풋에 값이 입력될 때 즉 handleChange에 유효성 검사를 해주어 입력할 때마다 검사를 해줄 것인지 혹은 값이 다 입력되고 입력 필드에서 포커스가 빠져나갈 때 유효성 검사를 해줄 것인지?
처음에는 회원가입 유효성 검사는 인풋에 값을 입력 받을 때 마다 했다.

하지만 사용자에게 아직 인풋이 다 입력되지 않았는데, 입력할 때마다 보이는 빨간 테두리와 에러 메시지는 사용자로 하여금 좋은 경험이 아니라고 생각된다.
handleBlur를 사용하여사용자가 해당 필드에서 포커스를 떼었을 때 입력값의 유효성을 검사하여 에러 메시지를 표시하게 변경하였습니다.

리팩토링 전 코드에는 반복되는 switch-case 구조로 인하여 가독성이 떨어지는 문제가 있습니다.
const validateField = (fieldName: keyof FormData, value: string) => {
let error = "";
switch (fieldName) {
case "userName":
if (!value) {
error = "";
} else if (value.length === 1 && /^[ㄱ-ㅎㅏ-ㅣ]+$/.test(value)) {
error = "올바르지 않은 이름 형식입니다.";
} else if (/[ㄱ-ㅎㅏ-ㅣ0-9!@#$%^&*(),.?":{}|<>]/.test(value)) {
error = "특수문자나 숫자, 초성은 사용할 수 없습니다.";
}
break;
case "phoneNumber":
if (!value) {
error = "";
} else if (!/^\d+$/.test(value) || value.length !== 11) {
error = "- 없이 11자리의 숫자를 입력해주세요.";
}
break;
case "email":
if (!value) {
error = "";
} else if (
!/^[A-Za-z0-9]([-_.]?[A-Za-z0-9_])*@[A-Za-z0-9]([-_.]?[A-Za-z0-9])*.[A-Za-z]{2,3}$/.test(
value
)
) {
error = "올바른 이메일 형식이 아닙니다.";
}
break;
case "password":
if (!value) {
error = "";
} else if (
!/(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}/.test(
value
)
) {
error =
"영문 대소문자와 숫자, 특수문자 중 2가지 이상 조합하여 8~16자여야 합니다.";
}
break;
case "checkPassword":
if (!value) {
error = "";
} else if (value !== formData.password) {
error = "비밀번호가 일치하지 않습니다.";
}
break;
case "detailAddress":
if (!value.trim()) {
error = "상세주소를 입력해주세요";
}
break;
default:
break;
}
setValidationErrors((prevValidationErrors) => ({
...prevValidationErrors,
[fieldName]: error,
}));
};
코드 가독성을 향상시키기 위해 변경하였습니다.
- 에러메세지 판단 함수 생성
- 유효성 검사를 하여 검사에 통과하는 것은 true,
부합하는 것은 false로 boolean 값을 뱉고,
true경우""를return
유효성 검사 실패시(false)errorMessage를 반환하는 함수- switch문 제거
리팩토링을 하면서 새롭게 써본 타입에 대해 간단하게 정리해보았다 💻
아래와 같은 예시에서
- "키"는 객체의 프로퍼티 이름 즉
userName이며,
() => {...}는 그 키에 해당하는 함수입니다.
💬 프로젝트 코드 예시
const validationRules: Record<string, () => string> = {
userName: () => {
if (!value) return "";
if (value.length === 1 && /^[ㄱ-ㅎㅏ-ㅣ]+$/.test(value)) {
return "올바르지 않은 이름 형식입니다.";
}
if (/[ㄱ-ㅎㅏ-ㅣ0-9!@#$%^&*(),.?":{}|<>]/.test(value)) {
return "특수문자나 숫자, 초성은 사용할 수 없습니다.";
}
return "";
},
phoneNumber: () => {
if (!value) return "";
return getErrorMessage(
() => /^\d+$/.test(value) && value.length === 11,
"- 없이 11자리의 숫자를 입력해주세요."
);
},
const validateField = (fieldName: keyof FormData, value: string) => {
const getErrorMessage = (
rule: (value: string) => boolean,
errorMessage: string
) => {
return rule(value) ? "" : errorMessage;
};
const validationRules: Record<string, () => string> = {
userName: () => {
if (!value) return "";
if (value.length === 1 && /^[ㄱ-ㅎㅏ-ㅣ]+$/.test(value)) {
return "올바르지 않은 이름 형식입니다.";
}
if (/[ㄱ-ㅎㅏ-ㅣ0-9!@#$%^&*(),.?":{}|<>]/.test(value)) {
return "특수문자나 숫자, 초성은 사용할 수 없습니다.";
}
return "";
},
phoneNumber: () => {
if (!value) return "";
return getErrorMessage(
() => /^\d+$/.test(value) && value.length === 11,
"- 없이 11자리의 숫자를 입력해주세요."
);
},
email: () => {
if (!value) return "";
return getErrorMessage(
() =>
/^[A-Za-z0-9]([-_.]?[A-Za-z0-9_])*@[A-Za-z0-9]([-_.]?[A-Za-z0-9])*.[A-Za-z]{2,3}$/.test(
value
),
"올바른 이메일 형식이 아닙니다."
);
},
password: () => {
if (!value) return "";
return getErrorMessage(
() =>
/(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}/.test(
value
),
"영문 대소문자와 숫자, 특수문자 중 2가지 이상 조합하여 8~16자여야 합니다."
);
},
checkPassword: () => {
if (!value) return "";
return getErrorMessage(
() => value === formData.password,
"비밀번호가 일치하지 않습니다."
);
},
detailAddress: () => {
return !value.trim() ? "상세주소를 입력해주세요" : "";
},
};
const error = validationRules[fieldName]();
setValidationErrors((prevValidationErrors) => ({
...prevValidationErrors,
[fieldName]: error,
}));
};
유익한 자료 감사합니다.