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

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

2026-10-25 게시 · https://ikiform.com/ko/guides/contact-form-static-site/vercel

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

## Vercel이 405로 응답하는 이유

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

`터미널`:

```bash
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contact
```

`응답`:

```http
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 Handler | `output: "export"`는 Server Actions나 요청을 읽는 Route Handlers를 지원하지 않습니다. `next dev`가 오류를 알려 주며, 빌드에는 이를 실행할 서버가 없습니다 |

다이어그램: Vercel이 폼 POST를 처리하는 방식

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에서 고칠 것은 속성 하나입니다.

`수정 전`:

```html
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
```

`수정 후`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
```

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

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

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

폼 백엔드가 이 목록을 대신 처리합니다. 그래도 여러분의 코드를 거치게 하고 싶다면, 폼은 Ikiform으로 POST하게 두고 함수에서 서명된 웹훅을 받으세요. 핸들러는 [직접 운영하는 서버](https://ikiform.com/blog/form-submissions-to-your-own-server.md)에 있습니다.

## Vercel 환경별 폼 ID

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

`Vercel 환경 변수`:

```text
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`:

```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`:

```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`를 읽으세요. [필라 가이드](https://ikiform.com/ko/guides/contact-form-static-site.md#form)에 정적 내보내기용 폼이 있습니다. Next.js 앱이 Vercel 서버에서 실제로 실행된다면 Server Action이 시크릿 키로 POST할 수 있습니다. [Next.js 가이드](https://ikiform.com/ko/docs/guides/nextjs.md#server-action)를 참고하세요.

## 헤더와 깔끔한 URL

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

`vercel.json`:

```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**을 설정하세요. |

## 질문

### Vercel에서 폼을 제출하면 왜 405 Method Not Allowed가 나오나요?

폼이 정적 파일만 있는 경로로 POST하기 때문입니다. Vercel은 GET 요청에 파일을 제공하고, 그 경로를 처리하는 함수가 없으면 POST를 405로 거부합니다. 폼을 폼 백엔드로 POST하거나 Vercel Function을 추가하세요.

### 정적 내보내기에서 Next.js Server Action을 쓸 수 있나요?

아니요. output: export에서는 실행할 서버가 없으므로 Server Actions를 지원하지 않습니다. 클라이언트 컴포넌트에서 폼 백엔드로 POST하거나, 정적 내보내기 없이 배포해 Vercel이 액션을 실행하게 하세요.

### 프리뷰 배포가 프로덕션과 같은 폼을 써도 되나요?

권하지 않습니다. Preview 환경 변수로 프리뷰에 별도의 폼 ID를 주어 테스트 메시지가 받은편지함이나 웹훅에 닿지 않게 하세요.

### 문의 폼에 vercel.json이 필요한가요?

아니요. 폼은 vercel.json 없이도 작동합니다. ikiform.com을 허용하는 Content-Security-Policy를 설정하거나, 감사 페이지 URL을 깔끔하게 하려고 cleanUrls를 켤 때만 추가하세요.

### Ikiform은 무료인가요?

아니요, 무료 플랜은 없습니다. 테스트는 무료입니다. 결제하기 전에 localhost에서 한 달에 50건의 테스트 제출을 보낼 수 있습니다. 실제로 운영하려면 Pro가 제출 500건에 월 $6부터이며, 한 번만 결제해 제출 10,000건을 $49에, 50,000건을 $149에 살 수도 있습니다. 크레딧은 만료되지 않습니다.

## 가이드 더 보기

- [정적 사이트의 문의 폼](https://ikiform.com/ko/guides/contact-form-static-site.md): 서버 없이 정적 사이트에 작동하는 문의 폼을 추가하세요. POST가 가는 곳, 스팸, 이메일 답장, 호스트와 프레임워크별 코드까지 다룹니다.
- [Netlify](https://ikiform.com/ko/guides/contact-form-static-site/netlify.md): Netlify Forms 없이 Netlify 사이트에서 문의 폼을 운영하세요. 빌드 시점 감지 없이 React에서 JSON을 POST하고, 배포 프리뷰용 폼을 따로 둡니다.
- [Cloudflare Pages](https://ikiform.com/ko/guides/contact-form-static-site/cloudflare-pages.md): Function을 작성하지 않고 Cloudflare Pages에 문의 폼을 추가하세요. 폼 백엔드로 POST하고, _headers에서 헤더를 설정하고, 직접 가진 Turnstile 키를 씁니다.
- [GitHub Pages](https://ikiform.com/ko/guides/contact-form-static-site/github-pages.md): GitHub Pages는 서버 코드를 실행할 수 없어 폼 POST가 405 Not Allowed를 받습니다. 대신 폼 백엔드로 POST하고 프로젝트 사이트의 리디렉션을 올바르게 설정하세요.
- [Amazon S3](https://ikiform.com/ko/guides/contact-form-static-site/aws-s3.md): S3 웹사이트 엔드포인트는 일반 HTTP로 GET과 HEAD만 받으므로, 문의 폼은 폼 백엔드로 POST하고 CloudFront로 리디렉션을 올바르게 설정하세요.
- [포트폴리오 웹사이트](https://ikiform.com/ko/guides/contact-form-static-site/portfolio-website.md): 첫 통화에서 물어볼 프로젝트, 예산, 일정, 링크를 미리 묻는 포트폴리오 문의 폼입니다. 서버가 필요 없고 받은편지함에서 답장합니다.
