반응형
Next.js에서 사용자가 입력한 데이터를 Google Form으로 바로 POST해서 응답을 수집하는 과정을 정리했습니다.
엔드포인트 구조, entry 파라미터 찾는 법 그리고 작업하면서 겪은 삽질(?)까지 기록해봐슨..

왜 Google Form을 썼을까?
- 간단한 설문/문의는 DB 따로 안 만들어도 바로 구글 시트로 들어감
- 관리자는 Google Sheet에서 필터링/검색만 해도 바로 관리 가능
- 배포/운영 비용도 확 줄어듦
👉 당시엔 빠르게 결과를 내야 했기 때문에 이 선택이 꽤 적절했던 것 같아요!
전체 흐름
- Google Form 생성
- 각 문항의 entry.<숫자> 파라미터 확인
- Form 제출 엔드포인트(formResponse)로 POST
1. Google Form 엔드포인트와 entry 파라미터 찾기
Google Form 제출 URL 기본 형태는 이렇습니다:
<https://docs.google.com/forms/d/e/>/formResponse
각 질문은 entry.<숫자> 이름으로 매핑됩니다.
찾는 방법은 두 가지:
- 페이지 소스 보기: “양식 보내기” → 링크 열기 → entry. 검색
- 양식 미리 작성하기 url 사용 : 양식 미리 작성하기에 아무거나 입력 후 링크 복사시 entry + 답변이 나옴
저는 두 개 다 해봤는데… 두 번째 방법이 훨씬 편하고 간단합니다 ((괜히 고생하지 않기...~)) 😅
제가 실제로 썼던 폼은 대략 이런 매핑이었습니다:
이름 -> entry.123456789
이메일 -> entry.987654321
전화번호 -> entry.135792468
실제 코드에선 .env 파일로 entry 키를 관리했습니다.
폼 항목이 늘어날 수 있다면 이 방식이 훨씬 깔끔해요.
// field map (env로 관리 추천)
// name: entry.123456789
// email: entry.987654321
// phoneNumber: entry.135792468
2. 서버 프록시 구현 예시
app/api/form/route.ts (Next.js App Router 기준)
import { NextResponse } from 'next/server';
const formActionUrl = '<https://docs.google.com/forms/d/e/>/formResponse'
export async function POST(req: Request) {
try {
const { name, email, phoneNumber } = await req.json();
// 1) 서버 쪽 검증 (필수/형식)
if (!name || !email || !phoneNumber) {
return NextResponse.json({ ok: false, error: 'Missing fields' }, { status: 400 });
}
// 2) Google Form에 맞는 entry.* 로 인코딩
const formData = new URLSearchParams({
'entry.123456789': name,
'entry.987654321': email,
'entry.135792468': message,
});
// 3) Google Form 제출
const resp = await fetch(formActionUrl, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
const ok = resp.status >= 200 && resp.status < 400;
return NextResponse.json({ ok });
} catch (e) {
console.error(e);
return NextResponse.json({ ok: false, error: 'Internal error' }, { status: 500 });
}
}
클라이언트 폼 제출
'use client';
import { useState } from 'react';
export default function ContactPage() {
const [loading, setLoading] = useState(false);
const [ok, setOk] = useState<boolean | null>(null);
const [form, setForm] = useState({ name: '', email: '', phoneNumber: '' });
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setOk(null);
try {
const res = await fetch('/api/form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
const data = await res.json();
setOk(data.ok);
} catch {
setOk(false);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={onSubmit} className="max-w-md space-y-3">
<input
className="w-full border p-2 rounded"
placeholder="이름"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
/>
...
</form>
);
}
4. 제가 겪은 이슈와 해결
- entry 키 불일치
- 오타나 누락 있으면 시트에 데이터가 안 들어가도 응답은 200이 옵니다.
- → 네트워크 탭 + 시트 반영 둘 다 꼭 확인하세요 !!
- 권한 문제
- 구글폼이 “로그인 사용자만 응답”으로 되어 있으면 응답은 성공처럼 보여도 시트엔 저장이 안 됩니다.
- → 익명도 응답 가능하게 설정을 열어야 합니다. 그럼 해결 ~ !
🍯 꿀팁
구글폼 자체를 복제해서 사본으로 만들어서 새로 제작해도 질문 당 entry ID 가 변하지 않습니다 !!
변하는건 구글폼 ID 만 바뀌는거라 개발용으로 사본을 제작해주시고 .env.local 에는 개발용 구글폼으로 연결해서 사용하면 간단합니다 !
결론
- 단순하게 제출/수집만 하고 싶다면, Google Form + Next.js 프록시 조합이 가장 빠르고 단순했습니다.
- 서버에서 검증을 조금 해주고 entry 매핑만 제대로 맞추면 운영이 편해요 ~ !!
반응형
'Frontend' 카테고리의 다른 글
| 버저닝(Versioning)과 캐럿(Caret), 그리고 의존성 버저닝의 함정 (2) | 2025.09.28 |
|---|