Contact 폼 유효성 검사
handleFormSubmit은 event.preventDefault()로 기본 제출을 막은 뒤 validateContactForm()을 실행합니다.
| 필드 | 검사 |
|---|---|
| 이름 / 메시지 | 빈 값인지 |
| 이메일 | 정규식 /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 으로 형식 확인 |
실패한 필드마다 setFieldError로 에러 메시지를 표시합니다. 이때 aria-invalid, aria-describedby 속성도 함께 바꿔서, 화면을 읽어 주는 스크린리더 사용자도 어느 칸이 왜 틀렸는지 알 수 있게 합니다.
검사를 통과하면: Formspree로 실제 전송
검사를 통과하면 sendToFormspree()가 폼 내용을 Formspree(폼 내용을 이메일로 보내 주는 외부 서비스)에 POST 요청으로 보냅니다. 주소는 <form>의 data-formspree-endpoint 속성에 적혀 있습니다.
const sendToFormspree = async (formData) => {
const endpoint = selectors.contactForm.dataset.formspreeEndpoint;
const response = await fetch(endpoint, {
method: 'POST',
headers: { Accept: 'application/json' },
body: formData,
});
if (!response.ok) {
throw new Error('전송에 실패했습니다. 잠시 후 다시 시도해 주세요.');
}
};| 부분 | 의미 |
|---|---|
method: 'POST' | 서버에 새 데이터(메시지)를 보내는 요청 |
body: formData | 폼 칸들을 FormData로 그대로 담아 보냄 |
Accept: application/json | 응답을 HTML 페이지가 아니라 JSON으로 달라고 요청 (페이지 이동 없이 결과만 받기 위해) |
if (!response.ok) | 상태 코드가 200~299가 아니면 직접 에러를 던짐 |
마지막 줄이 중요합니다. fetch()는 서버가 400이나 500을 돌려줘도 에러를 던지지 않고 정상으로 끝나기 때문에, 상태 코드를 직접 확인하지 않으면 전송이 실패해도 "성공" 메시지가 뜰 수 있습니다. 자세한 내용은 GitHub API 연동 글의 "fetch는 404나 500에서 에러를 던지지 않는다"를 참고하세요.
전송하는 동안에는 setSubmitting(true)가 버튼을 disabled로 막고 문구를 "전송 중..."으로 바꿉니다. 같은 메시지가 두 번 전송되는 것을 막는 장치이고, finally 블록에서 성공이든 실패든 버튼을 다시 풀어 줍니다.
브라우저 검사만으로는 부족한 이유
validateContactForm()은 사용자 편의를 위한 검사입니다. 잘못 입력한 칸을 서버까지 갔다 오지 않고 바로 알려 주니 빠르고 친절하지만, 보안 장치는 아닙니다.
- 브라우저에서 돌아가는 JS는 사용자가 개발자 도구로 끄거나 고칠 수 있습니다.
- 아예 브라우저를 거치지 않고
curl같은 도구로 전송 주소에 바로 요청을 보낼 수도 있습니다.
[사용자] ──▶ [브라우저 검사] ──▶ [서버 검사] ──▶ 저장 / 메일 전송
(편의) (보안)
[curl 등] ─────────────────────▶ [서버 검사] ← 브라우저 검사를 건너뜀비유하면 브라우저 검사는 매장 입구의 안내 직원이고, 서버 검사는 금고 앞의 경비원입니다. 안내 직원이 있어도 경비원은 빠질 수 없습니다. 이 프로젝트에서는 서버 역할을 Formspree가 맡고 있으므로, 받는 쪽의 스팸 필터와 필수 칸 검사는 Formspree 설정에 맡기는 구조입니다. 직접 서버를 만든다면 같은 규칙을 서버에서도 한 번 더 검사해야 합니다.
입력값을 화면에 다시 보여줄 때 주의할 점
사용자가 입력한 값을 화면에 다시 넣을 때는 innerHTML 대신 textContent를 써야 합니다. 이름 칸에 HTML이나 스크립트 조각을 넣으면 innerHTML은 그것을 진짜 코드로 해석해서 실행할 수 있기 때문입니다. 이런 공격을 XSS(크로스 사이트 스크립팅) 라고 합니다.
// 위험: 입력값이 HTML로 해석됨
statusEl.innerHTML = `${name}님, 메시지가 전송되었습니다`;
// 안전: 입력값이 글자 그대로 표시됨
statusEl.textContent = `${name}님, 메시지가 전송되었습니다`;이 프로젝트는 에러 메시지, 성공 메시지, 프로젝트 카드까지 모두 textContent와 createElement로 화면을 만들고 innerHTML은 쓰지 않습니다. 코드 주석에도 "innerHTML을 쓰지 않음 (XSS 방지)"라고 적혀 있습니다. 폼과 관련된 또 다른 공격인 CSRF와 함께 CSRF와 XSS 글에서 자세히 다룹니다.
아직 없는 것
- 현재는 제출 시점에만 검사가 실행되며, 입력 중 실시간 검사(예: 칸을 벗어날 때
blur이벤트로 검사)는 구현되어 있지 않습니다. - 전송 실패 시 상태 코드와 상관없이 같은 문구를 보여 줍니다. 4xx(입력 문제)와 5xx(서버 문제)를 나눠 "입력을 확인해 주세요"와 "잠시 후 다시 시도해 주세요"로 안내하면 사용자가 무엇을 해야 할지 더 분명해집니다.
정리
- 제출 시 브라우저에서 이름·이메일·메시지를 검사하고, 틀린 칸에 에러와 접근성 속성을 표시
- 통과하면
fetch로 Formspree에 POST 전송하고,response.ok로 실패를 직접 확인 - 전송 중에는 버튼을 막아 중복 전송을 방지
- 브라우저 검사는 편의 기능이고, 보안 검사는 받는 쪽(서버, 여기서는 Formspree)이 담당
- 화면에 글자를 넣을 때는
textContent만 써서 XSS를 막음
출처: Codyssey-B1/B1-1
