폼 검증, 어디까지 해야 하나
Article
문의 폼 하나 만드는데 검증 로직을 어디까지 짜야 하나 한참 고민했다. 결론부터 말하면 클라이언트 검증과 서버 검증은 둘 다 필요하고, 둘은 하는 일이 다르다. 이걸 헷갈리면 한쪽에 몰빵하다가 구멍이 생긴다.
클라이언트 검증은 UX를 위한 것
브라우저에서 하는 검증은 사용자를 위한 거다. 제출 버튼 누르고 서버 다녀올 때까지 기다렸다가 "이메일 형식이 틀렸습니다" 하면 늦다. 입력하는 순간, 늦어도 필드를 벗어나는 순간 알려줘야 한다.
function validateEmail(value: string): string | null {
if (!value.trim()) return "이메일을 입력해줘.";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return "이메일 형식을 다시 확인해줘.";
}
return null;
}
핵심은 빠른 피드백이다. onBlur 시점에 검사하고, 한 번 에러가 뜬 필드는 onChange로 실시간 갱신해 사용자가 고치는 즉시 에러가 사라지게 한다. 처음부터 빨간 글씨를 도배하면 아직 입력도 안 했는데 혼나는 기분이 든다.
하지만 클라이언트 검증은 보안이 아니다. 자바스크립트는 사용자가 얼마든지 우회할 수 있다. 개발자 도구로 disabled 풀고, 아예 fetch로 직접 쏘면 클라이언트 코드는 없는 셈이 된다. 그러니 클라이언트 검증만 믿고 서버에 그대로 저장하면 그게 구멍이다.
서버 검증은 보안과 무결성을 위한 것
서버는 들어오는 모든 데이터를 불신하는 게 기본이다. 클라이언트에서 이미 검증했더라도 서버에서 다시 검증한다. 여기서 막는 건 UX가 아니라 잘못된 데이터, 악의적 입력, 스팸이다.
export async function POST(req: Request) {
const body = await req.json();
const errors: Record<string, string> = {};
if (typeof body.email !== "string" || !isEmail(body.email)) {
errors.email = "유효한 이메일이 아니다.";
}
if (typeof body.message !== "string" || body.message.length > 2000) {
errors.message = "메시지가 비었거나 너무 길다.";
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors }, { status: 400 });
}
// 통과한 데이터만 저장
}
타입까지 확인하는 이유는 클라이언트를 거치지 않은 요청은 email이 문자열이라는 보장조차 없기 때문이다. 길이 상한을 두는 건 DB와 이후 처리를 지키기 위해서다. 같은 "이메일 검증"이라도 클라이언트는 형식을 친절하게 안내하는 거고, 서버는 쓰레기 데이터를 걸러내는 거다.
에러 메시지도 UX 디자인이다
정작 오래 붙잡은 건 로직이 아니라 문구였다. "유효하지 않은 입력입니다" 같은 기본 문구는 틀린 건 아닌데 차갑고, 뭘 어떻게 고치라는 건지 안 알려준다. 좋은 에러 메시지는 세 가지를 담는다. 무엇이 문제인지, 왜 문제인지, 어떻게 고치는지.
"전화번호를 확인해주세요"보다 "전화번호는 숫자만 입력해줘 (예: 01012345678)"가 낫다. 예시 하나가 설명 열 줄을 줄인다. 문구 톤도 서비스 성격에 맞춘다. 디자인 전공을 거쳐서인지 이 부분이 제일 재밌다. 검증은 결국 사용자를 막는 게 아니라 제대로 통과하도록 돕는 일이라는 걸 문구를 다듬으면서 다시 느꼈다.
정리하면 클라이언트는 친절하게, 서버는 엄격하게. 그리고 에러 메시지는 코드만큼 신경 써서 쓴다.