정적 사이트의 문의 폼

정적 사이트의 문의 폼4분 분량

Vercel에 호스팅한 문의 폼의 405 오류 해결하기

Vercel의 정적 페이지는 폼 POST에 405 Method Not Allowed로 응답합니다. 그 이유와 폼을 백엔드로 보내는 방법을 알아보세요.

Preet SutharIkiform 창업자

Vercel은 정적 빌드를 파일로 제공하며, 해당 경로를 처리하는 함수가 없으므로 파일로 보낸 POST는 빈 본문의 405를 받습니다. 대신 폼의 action을 Ikiform 엔드포인트로 지정하거나, POST를 받는 Vercel Function을 작성하세요. Ikiform을 쓴다면 Vercel 환경마다 폼 ID를 따로 설정해 프리뷰는 테스트 폼으로, 프로덕션은 실제 폼으로 POST하게 하세요.

Vercel이 405로 응답하는 이유

Vercel은 각 요청을 정적 파일이나 함수로 라우팅합니다. 정적 사이트, Vite 빌드, output: "export"를 쓴 Next.js 앱에는 파일만 있습니다. /contact에 대한 GET은 파일을 돌려주지만, 같은 경로로 보낸 POST는 갈 곳이 없으므로 여러분의 코드가 실행되기도 전에 Vercel이 거부합니다.

터미널
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contact
응답
HTTP/2 405
server: Vercel
content-disposition: inline
x-vercel-cache: HIT
content-length: 0

응답에 본문이 없으므로 브라우저는 빈 페이지나 자체 오류 화면을 보여 줍니다. 다음 세 가지 구성이 이 상황으로 이어집니다.

여러분의 코드Vercel에서 405가 나는 이유
action이 없는 <form method="post">폼이 자신이 있는 페이지, 즉 정적 파일로 POST합니다
Vite나 일반 HTML 프로젝트의 action="/api/contact"프로젝트 루트의 api/ 폴더에 파일이 없으므로 그 경로를 처리하는 함수가 없습니다
정적 내보내기의 Next.js Server Action 또는 Route Handleroutput: "export"는 Server Actions나 요청을 읽는 Route Handlers를 지원하지 않습니다. next dev가 오류를 알려 주며, 빌드에는 이를 실행할 서버가 없습니다
Vercel이 폼 POST를 처리하는 방식action이 Vercel 사이트를 가리킴action이 Ikiform을 가리킴POST/contact폼에서Vercel 라우팅파일인가 함수인가?여기에 함수가?여러분의 함수직접 작성했다면405, 빈 본문정적 파일ikiform.com으로POST같은 폼Ikiform저장, 이메일 알림감사 페이지Vercel의 정적 파일예아니요303Vercel이 폼 POST를 처리하는 방식action이 Vercel 사이트를 가리킴action이 Ikiform을 가리킴POST /contact폼에서Vercel 라우팅파일인가 함수인가?여기에 함수가?여러분의 함수직접 작성했다면405, 빈본문정적 파일ikiform.com으로POST같은 폼Ikiform저장, 이메일 알림감사 페이지Vercel의 정적 파일예303아니요
  1. 폼이 Vercel 배포의 경로로 POST합니다.
  2. Vercel이 그 경로를 처리하는 함수를 찾습니다.
  3. 직접 작성한 함수가 있으면 실행됩니다. 정적 빌드에는 없으므로 Vercel은 빈 본문의 405로 응답합니다.
  4. action이 Ikiform을 가리키면 POST는 Vercel에 닿지 않습니다.
  5. Ikiform이 메시지를 저장하고, 이메일로 알리고, 브라우저를 Vercel 사이트의 정적 페이지로 돌려보냅니다.
Vercel이 고장 난 것이 아닙니다. 정적 배포에는 POST를 처리할 코드가 없고, Vercel 쪽에서 코드를 추가하는 방법은 함수뿐입니다.

그러니 함수를 추가하거나 POST를 다른 곳으로 보내야 합니다. api/contact.ts에 Vercel Function을 두면 되지만, 그러면 이메일 API, 스팸 필터링, 레이트 리밋이 필요하고 이를 직접 유지해야 합니다. POST를 Ikiform으로 보내면 배포는 정적으로 유지됩니다. HTML에서 고칠 것은 속성 하나입니다.

수정 전
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
수정 후
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">

함수를 직접 작성하고 싶다면

메시지가 여러분의 코드만 닿을 수 있는 곳으로 가야 한다면 함수도 괜찮은 선택입니다. 시작하기 전에 필요한 것을 따져 보세요.

  • 이메일 제공업체 계정, 인증된 발신 도메인, Vercel 환경 변수에 넣을 API 키.
  • 서버 측 검증. 브라우저의 required 속성은 편의 기능일 뿐 검사가 아닙니다.
  • 스팸 필터링과 레이트 리밋. 없으면 인터넷의 모든 봇이 문의 폼을 통해 메일을 보냅니다.
  • 이메일 제공업체가 다운되었을 때 메시지를 보관할 곳과 다시 보낼 방법.

폼 백엔드가 이 목록을 대신 처리합니다. 그래도 여러분의 코드를 거치게 하고 싶다면, 폼은 Ikiform으로 POST하게 두고 함수에서 서명된 웹훅을 받으세요. 핸들러는 직접 운영하는 서버에 있습니다.

Vercel 환경별 폼 ID

Vercel에서는 풀 리퀘스트마다 프리뷰 배포가 만들어집니다. 프리뷰가 프로덕션 폼으로 POST하면 모든 테스트 메시지가 실제 메시지와 함께 받은편지함에 쌓입니다. 프리뷰용 Ikiform 폼을 하나 더 만들고, Vercel 환경 변수가 고르게 하세요. Settings › Environment Variables에서 같은 이름을 두 번 추가하세요.

Vercel 환경 변수
VITE_IKIFORM_FORM_ID   Production   frm_xxxxxxxxxxxxxxxx
VITE_IKIFORM_FORM_ID   Preview      frm_yyyyyyyyyyyyyyyy

Vite는 빌드 시점에 index.html의 %NAME% 플레이스홀더를 VITE_ 변수로 바꾸므로, 일반 HTML 폼도 JavaScript 없이 이를 쓸 수 있습니다. VITE_VERCEL_ENV는 Vercel이 Vite 프로젝트에 설정하며, 메시지가 어느 배포에서 왔는지 기록합니다.

index.html
<form id="contact" action="https://ikiform.com/f/%VITE_IKIFORM_FORM_ID%" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="build" value="%VITE_VERCEL_ENV%" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
</form>
  • 정적 사이트에서 Vercel은 빌드 시점에 환경 변수를 읽습니다. 값을 바꾼 뒤에는 다시 배포하세요. 그렇지 않으면 HTML에 이전 값이 남습니다.
  • Vite는 변수가 없으면 플레이스홀더를 그대로 두므로, ID가 빠지면 %VITE_IKIFORM_FORM_ID%가 그대로 배포되고 모든 POST가 폼에 닿기도 전에 400 Bad Request로 실패합니다. 아래의 가드가 이를 잡아 줍니다.
  • 폼 ID는 어느 쪽이든 공개됩니다. 브라우저에 전달되는 것은 VITE_ 변수뿐이며, 그래서 여기에는 비밀을 넣으면 안 됩니다.
src/contact.js
const form = document.querySelector("#contact");

if (form && form.action.includes("%VITE_")) {
  console.error("VITE_IKIFORM_FORM_ID is not set for this Vercel environment");
  form.querySelector("button").disabled = true;
}

output: "export"로 Next.js를 쓰나요? 대신 클라이언트 컴포넌트에서 process.env.NEXT_PUBLIC_IKIFORM_FORM_ID를 읽으세요. 필라 가이드에 정적 내보내기용 폼이 있습니다. Next.js 앱이 Vercel 서버에서 실제로 실행된다면 Server Action이 시크릿 키로 POST할 수 있습니다. Next.js 가이드를 참고하세요.

헤더와 깔끔한 URL

사이트가 Content-Security-Policy를 설정한다면 vercel.json에서 설정하고 Ikiform을 포함하세요. cleanUrls는 thanks.html을 /thanks로 제공하므로 리디렉션 대상이 더 깔끔해집니다.

vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
        }
      ]
    }
  ]
}

프리뷰와 허용 도메인

your-project-git-branch-team.vercel.app 같은 프리뷰 URL은 your-project.vercel.app의 하위 도메인이 아니므로 Allowed domains에서 일치하지 않습니다. 여기서 보낸 POST는 origin_mismatch와 함께 스팸으로 분류됩니다. 프리뷰용 폼을 따로 두어야 하는 또 다른 이유입니다. 그 폼의 허용 도메인은 비워 두거나, 테스트 중인 프리뷰 호스트를 추가하세요.

Vercel Authentication을 켜고 프로덕션 도메인을 제외한 모든 배포를 포함하는 Standard Protection 범위를 쓰면, 프리뷰 URL은 Vercel 로그인 뒤에 있습니다. 여러분의 팀만 접근할 수 있으므로 허용 도메인이 없는 프리뷰 폼도 공개되지 않습니다. 이에 의존하기 전에 Settings › Deployment Protection을 확인하세요. 프리뷰에서 보낸 POST는 localhost 테스트가 아니므로 다른 실제 제출처럼 플랜 사용량에 포함됩니다.

작동 확인하기

  1. 수정하기 전에 프로덕션 URL로 위의 curl을 실행해 405를 확인하세요. 수정한 뒤에는 배포된 HTML에서 폼의 action이 https://ikiform.com/f/로 시작해야 합니다.
  2. 프리뷰 배포를 열어 제출하고, 메시지가 프로덕션 폼이 아니라 프리뷰 폼에 도착하는지 확인하세요.
  3. 프로덕션 도메인에서 제출하고 build 필드가 production인지 확인하세요.
  4. 알림 수신자마다 받은편지함을 확인하세요. 각자 필드가 담긴 이메일을 받고, 폼에 email 필드가 있으면 Reply가 제출한 사람에게 갑니다.
  5. 아무것도 오지 않았나요? 대시보드의 상태 필터를 Spam으로 바꿔 보세요. 스팸은 실제 제출과 같은 성공 응답을 받습니다.

주의할 점

문제해결 방법
폼을 바꾼 뒤에도 여전히 405테스트 중인 배포가 변경 전에 빌드되었거나, JavaScript 핸들러가 fetch로 여러분의 경로에 POST합니다. 빌드된 파일에서 /api/contact를 검색하세요.
모든 POST에서 400 Bad Request, 또는 Ikiform에서 404 not_found폼 ID가 빌드에 들어가지 않았거나, 그 환경에 맞지 않는 ID입니다. 해당 환경에 변수가 있는지 확인하고 다시 배포하세요.
프리뷰는 되는데 프로덕션 POST가 스팸으로 분류됨프로덕션 폼의 Allowed domains에 프로덕션 도메인이 빠져 있습니다. your-project.vercel.app과 함께 커스텀 도메인도 추가하세요.
브라우저가 여러분의 페이지 대신 Ikiform의 감사 페이지로 이동함_redirect는 도메인이 Allowed domains에 있고 https: URL일 때만 따릅니다. 도메인을 추가하거나, 대신 폼 설정에서 Redirect after submit을 설정하세요.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.