Skip to content

Contact 폼 유효성 검사 ​

handleFormSubmit은 event.preventDefault()로 기본 제출을 막은 뒤 validateContactForm()을 실행합니다.

필드검사
이름 / 메시지빈 값인지
이메일정규식 /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 으로 형식 확인

실패한 필드마다 setFieldError로 에러 메시지를 표시합니다. 이때 aria-invalid, aria-describedby 속성도 함께 바꿔서, 화면을 읽어 주는 스크린리더 사용자도 어느 칸이 왜 틀렸는지 알 수 있게 합니다.

검사를 통과하면: Formspree로 실제 전송 ​

검사를 통과하면 sendToFormspree()가 폼 내용을 Formspree(폼 내용을 이메일로 보내 주는 외부 서비스)에 POST 요청으로 보냅니다. 주소는 <form>의 data-formspree-endpoint 속성에 적혀 있습니다.

js
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(크로스 사이트 스크립팅) 라고 합니다.

js
// 위험: 입력값이 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