정적인 페이지에서 한 걸음 더 나아가기
지금까지 만든 페이지는 주로 방문자에게 정보를 보여 주는 역할을 했습니다. 회사 소개, 서비스 설명, 이미지, 공지사항처럼 사이트 주인이 준비한 내용을 방문자가 읽는 구조였습니다.
이번 장에서는 반대로 방문자가 우리에게 무언가를 남기는 기능을 다룹니다.
대표적인 예는 다음과 같습니다.
문의 폼에 이름, 이메일, 문의 내용을 남긴다.
뉴스레터 신청 칸에 이메일을 입력한다.
버튼을 눌렀을 때 완료 메시지가 나타난다.
개인정보 동의 체크박스를 선택해야 제출할 수 있다.
이런 기능을 통틀어 상호작용이라고 부릅니다. 말 그대로 사용자와 웹사이트가 서로 반응하는 것입니다. 버튼을 누르면 화면이 바뀌고, 입력한 내용이 어딘가로 전달됩니다.
폼은 종이 신청서와 비슷합니다. 종이 신청서에 이름, 연락처, 요청사항을 적고 제출하듯이, 웹사이트의 폼도 방문자가 입력한 값을 모아 처리 담당자에게 전달합니다.
AI 생성 이미지
input, textarea, button의 역할
문의 폼을 만들 때 자주 쓰는 태그는 세 가지입니다.
input: 짧은 값을 입력받습니다. 이름, 이메일, 전화번호처럼 한 줄짜리 정보에 어울립니다.textarea: 긴 글을 입력받습니다. 문의 내용, 요청사항, 후기처럼 여러 줄이 필요한 정보에 어울립니다.button: 사용자가 행동을 실행할 때 누르는 버튼입니다. 예를 들어 "문의 보내기", "뉴스레터 신청하기"가 여기에 해당합니다.
여기서 중요한 것은 태그 이름을 외우는 것이 아닙니다. "이 칸은 어떤 정보를 받는가?"를 먼저 생각하는 것입니다.
문의 폼이라면 보통 다음 정보가 필요합니다.
| 항목 | 필요한 이유 |
|---|---|
| 이름 | 누가 문의했는지 확인하기 위해 필요합니다. |
| 이메일 | 답장을 보내기 위해 필요합니다. |
| 문의 내용 | 방문자가 원하는 것을 정확히 알기 위해 필요합니다. |
| 개인정보 동의 | 이름과 이메일 같은 개인정보를 받는다는 사실을 알리고 동의를 받기 위해 필요합니다. |
이메일 없이 문의 내용만 받으면 답장할 방법이 없습니다. 반대로 이메일만 받고 문의 내용을 받지 않으면 무엇을 원하는지 알 수 없습니다. 폼의 항목은 많을수록 좋은 것이 아니라, 처리에 필요한 만큼만 받는 것이 좋습니다.
예를 들어 단순한 문의 폼에 생년월일, 주소, 회사 규모, 예산, 유입 경로를 모두 묻는다면 방문자는 제출하기 전에 부담을 느낄 수 있습니다. 꼭 필요한 정보만 먼저 받고, 자세한 내용은 답장 과정에서 물어봐도 됩니다.
화면에 문의 폼 만들기
먼저 실제로 이메일을 보내지는 않고, 화면에서 입력하고 제출 완료 메시지만 보여 주는 폼을 만들어 보겠습니다. 이 단계는 "폼의 모양과 흐름을 이해하는 실습"입니다.
components/ContactForm.tsx 파일을 만들고 다음 코드를 넣습니다.
"use client";
import { useState, type FormEvent } from "react";
export default function ContactForm() {
const [sent, setSent] = useState(false);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSent(true);
}
return (
<form onSubmit={handleSubmit} className="space-y-5 rounded-2xl border p-6">
<div>
<label htmlFor="name" className="block font-medium">
이름
</label>
<input
id="name"
name="name"
type="text"
required
className="mt-2 w-full rounded-lg border px-4 py-3"
placeholder="홍길동"
/>
</div>
<div>
<label htmlFor="email" className="block font-medium">
이메일
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-2 w-full rounded-lg border px-4 py-3"
placeholder="[email protected]"
/>
</div>
<div>
<label htmlFor="message" className="block font-medium">
문의 내용
</label>
<textarea
id="message"
name="message"
required
rows={5}
className="mt-2 w-full rounded-lg border px-4 py-3"
placeholder="궁금한 내용을 적어 주세요."
/>
</div>
<label className="flex gap-2 text-sm">
<input type="checkbox" name="consent" required />
<span>문의 답변을 위해 이름과 이메일을 수집하는 데 동의합니다.</span>
</label>
<button
type="submit"
className="rounded-lg bg-black px-5 py-3 font-medium text-white"
>
문의 보내기
</button>
{sent && (
<p className="rounded-lg bg-green-50 p-4 text-green-700">
문의가 접수된 것처럼 표시했습니다. 실제 전송은 아직 연결하지 않았습니다.
</p>
)}
</form>
);
}그리고 app/contact/page.tsx에서 이 컴포넌트를 불러옵니다.
import ContactForm from "@/components/ContactForm";
export default function ContactPage() {
return (
<main className="mx-auto max-w-2xl px-6 py-16">
<h1 className="text-3xl font-bold">문의하기</h1>
<p className="mt-4 text-gray-600">
프로젝트 문의나 협업 제안이 있다면 아래 양식으로 남겨 주세요.
</p>
<div className="mt-8">
<ContactForm />
</div>
</main>
);
}브라우저에서 /contact로 이동해 보세요. 이름, 이메일, 문의 내용을 입력하고 체크박스를 선택한 뒤 버튼을 누르면 완료 메시지가 나타납니다.
여기서 꼭 기억할 점이 있습니다. 지금 만든 폼은 실제 이메일을 보내지 않습니다. 화면에서 "접수된 것처럼" 보여 줄 뿐입니다. 이것은 식당에서 주문서를 작성하는 연습만 한 상태와 같습니다. 실제로 주방에 주문이 전달되려면 별도의 연결이 필요합니다.
또 하나 알아둘 점이 있습니다. 위 예제는 폼의 흐름을 보여 주기 위한 가장 단순한 코드라서, 제출 후에도 입력한 내용이 화면에 그대로 남아 있습니다. 실제 사이트라면 제출이 끝난 뒤 입력칸을 비우거나, 제출 버튼을 잠시 비활성화하거나, 성공 화면으로 이동시키는 식으로 조금 더 다듬습니다.
왜 "use client"가 필요할까?
위 코드의 맨 위에는 "use client";가 있습니다.
Next.js의 app 폴더 안에 있는 컴포넌트는 기본적으로 서버 쪽에서 먼저 처리됩니다. 서버는 방문자의 클릭을 직접 듣고 있는 장소가 아닙니다. 버튼 클릭, 입력 중 화면 변경, 체크 여부에 따라 메시지 보여 주기처럼 브라우저 안에서 바로 반응해야 하는 기능은 클라이언트 컴포넌트에서 처리해야 합니다.
"use client"는 "이 파일은 브라우저에서 사용자의 행동에 반응해야 합니다"라고 알려 주는 표시입니다.
예를 들어 다음 기능에는 보통 "use client"가 필요합니다.
버튼을 눌렀을 때 메시지를 보여 준다.
입력값에 따라 글자 수를 세어 보여 준다.
메뉴 열기/닫기 상태를 기억한다.
체크박스 선택 여부에 따라 버튼을 활성화한다.
반대로 단순히 제목, 문단, 이미지, 카드 목록을 보여 주는 화면에는 꼭 필요하지 않습니다.
처음에는 이렇게 기억해도 좋습니다. 사용자의 행동을 기억하고 화면을 즉시 바꿔야 하면 "use client"가 필요할 가능성이 높습니다. 반대로 정해진 내용을 보여 주기만 한다면 굳이 붙이지 않아도 됩니다.
required는 첫 번째 안전장치다
코드에서 required라는 속성을 넣었습니다. 이것은 "이 칸은 비워 두면 제출할 수 없습니다"라는 뜻입니다.
<input name="email" type="email" required />type="email"은 이메일 형식인지 브라우저가 간단히 확인하게 해 줍니다. hello@example.com처럼 이메일 모양을 기대합니다.
하지만 required만 믿으면 안 됩니다. 이것은 가게 입구에 붙인 안내문과 비슷합니다. 대부분의 사람은 안내문을 따르지만, 마음먹고 우회하려는 사람까지 막지는 못합니다. 브라우저의 검사는 사용자를 도와주는 장치에 가깝고, 보안을 완전히 책임지는 장치는 아닙니다.
실제 운영에서는 두 번 확인하는 것이 좋습니다.
브라우저에서 먼저 확인합니다. 비어 있는 칸이 있으면 바로 알려 줍니다.
서버나 폼 서비스에서 다시 확인합니다. 잘못된 이메일, 너무 짧은 문의, 동의하지 않은 제출을 한 번 더 걸러냅니다.
Next.js의 서버 기능을 사용하면 폼이 제출된 뒤 서버에서 필수값을 검사하고, 성공 메시지나 오류 메시지를 다시 화면에 돌려줄 수 있습니다. App Router에서는 폼의 action에 서버에서 실행되는 함수를 연결하는 방식도 사용할 수 있고, 제출 중 버튼을 비활성화하거나 "전송 중…" 같은 문구를 보여 주는 패턴도 자주 사용됩니다.1
지금 단계에서는 서버 코드를 모두 이해하지 않아도 됩니다. 중요한 것은 "화면에서 한 번 확인하고, 실제 처리하는 곳에서 한 번 더 확인한다"는 원칙입니다.
입력값은 어디로 보내야 할까?
폼을 만들 때 가장 중요한 질문은 이것입니다.
방문자가 입력한 값은 최종적으로 어디로 가야 할까?
선택지는 크게 세 가지입니다.
1. 이메일 링크로 보내기
가장 쉬운 방법은 mailto: 링크입니다.
<a href="mailto:[email protected]">이메일로 문의하기</a>이 방식은 폼을 만들지 않아도 됩니다. 방문자의 이메일 앱이 열리고, 직접 메일을 작성하게 됩니다.
장점은 단순하다는 것입니다. 서버도 필요 없고, 보안 설정도 거의 없습니다. 단점은 방문자 환경에 따라 이메일 앱이 제대로 열리지 않을 수 있고, 문의 항목을 정해진 양식으로 받기 어렵다는 것입니다.
간단한 개인 포트폴리오나 초기 랜딩페이지라면 충분히 현실적인 선택입니다.
2. 폼 서비스를 사용하기
비개발자에게 가장 추천하기 쉬운 방법은 폼 서비스를 사용하는 것입니다. 예를 들어 Tally, Typeform, Google Forms, Formspree 같은 서비스가 있습니다.
이 방식은 접수 담당자를 외부에 맡기는 것과 비슷합니다. 우리는 사이트에 폼을 붙이거나 연결만 하고, 입력값 저장, 알림 이메일, 스팸 방지, CSV 다운로드 같은 기능은 서비스가 처리합니다.
장점은 빠르고 안전하다는 것입니다. 단점은 디자인이나 기능을 완전히 마음대로 바꾸기 어렵고, 무료 요금제에는 제한이 있을 수 있다는 것입니다.
문의 폼, 설문조사, 뉴스레터 사전 신청처럼 단순한 입력 기능이라면 먼저 폼 서비스를 검토하세요. 특히 "문의가 들어오면 이메일 알림만 받고 싶다" 정도라면 직접 서버를 만드는 것보다 폼 서비스를 쓰는 편이 훨씬 가볍습니다.
3. Next.js 서버 기능이나 API로 처리하기
더 자유롭게 만들고 싶다면 Next.js 안에서 서버 기능을 사용할 수 있습니다. 여기서 서버 기능이나 API는 보이지 않는 처리 담당자라고 이해하면 됩니다.
방문자는 화면에서 폼을 작성합니다. 버튼을 누르면 입력값이 보이지 않는 담당자에게 전달됩니다. 그 담당자는 다음 일을 할 수 있습니다.
입력값이 비어 있는지 확인한다.
이메일 형식이 맞는지 검사한다.
데이터베이스에 저장한다.
관리자에게 알림 메일을 보낸다.
뉴스레터 서비스에 이메일을 등록한다.
성공 또는 실패 결과를 화면에 알려 준다.
Next.js App Router에서는 클라이언트에서 fetch로 API를 호출하는 방식과, 폼 제출을 서버 쪽 함수에 연결하는 방식이 모두 사용됩니다. 서버 쪽에서 처리하면 비밀 키를 브라우저에 노출하지 않고, 제출 결과에 따라 오류 메시지나 완료 상태를 돌려줄 수 있습니다.2
다만 이 단계부터는 주의가 필요합니다. 이메일 발송 키, 데이터베이스, 개인정보 저장, 스팸 방지, 로그 관리가 얽히기 때문입니다. 작은 문의 폼이라도 실제 고객 개인정보를 받는다면 전문가의 검토를 받는 편이 안전합니다.
뉴스레터 신청은 더 단순하게 생각하기
뉴스레터 신청 폼은 문의 폼보다 보통 단순합니다.
필요한 정보는 대부분 이메일 하나입니다.
<form className="flex gap-2">
<input
name="email"
type="email"
required
placeholder="이메일 주소"
className="flex-1 rounded-lg border px-4 py-3"
/>
<button type="submit" className="rounded-lg bg-black px-5 py-3 text-white">
신청
</button>
</form>이 코드는 뉴스레터 신청 폼의 겉모양을 보여 주는 예시입니다. 실제로 구독 신청을 저장하려면 이 폼도 폼 서비스나 서버 기능에 연결해야 합니다. 아무 연결 없이 버튼만 누르면 브라우저가 페이지를 새로고침하거나, 아무 일도 일어나지 않는 것처럼 보일 수 있습니다.
실제 운영에서는 이 이메일을 어디에 등록할지 정해야 합니다. Mailchimp, ConvertKit, Stibee 같은 뉴스레터 서비스를 쓸 수도 있고, 자체 서버에 저장할 수도 있습니다.
비개발자라면 처음부터 자체 저장을 선택하지 않는 것이 좋습니다. 구독 취소, 중복 구독, 발송 실패, 스팸 신고, 수신 동의 기록 같은 문제가 따라오기 때문입니다. 뉴스레터는 이메일을 모으는 것보다 안전하게 관리하고 발송하는 것이 더 중요합니다.
실제 운영 전에 챙길 것들
폼은 화면에 입력칸만 있다고 끝나지 않습니다. 운영할 때는 다음 항목을 함께 확인해야 합니다.
제출 완료 메시지
사용자는 버튼을 누른 뒤 결과를 알고 싶어 합니다.
"문의가 접수되었습니다."
"입력한 이메일로 확인 메일을 보냈습니다."
"잠시 후 다시 시도해 주세요."
이런 메시지가 없으면 사용자는 제출이 된 것인지 몰라 같은 내용을 여러 번 보낼 수 있습니다.
제출 중 상태
전송에 1~2초가 걸릴 수 있습니다. 이때 버튼 문구를 "전송 중…"으로 바꾸거나 버튼을 잠시 비활성화하면 중복 제출을 줄일 수 있습니다.
예를 들어 사용자가 버튼을 한 번 눌렀는데 반응이 없어 보이면, 불안해서 같은 버튼을 여러 번 누를 수 있습니다. 그러면 같은 문의가 여러 번 저장되거나 같은 이메일이 여러 번 발송될 수 있습니다. 그래서 제출 중 상태는 작은 기능처럼 보여도 실제 운영에서는 꽤 중요합니다.
스팸 방지
폼을 공개하면 사람이 아닌 자동 프로그램도 접근할 수 있습니다. 이를 스팸 봇이라고 부릅니다. 스팸 봇은 광고 문구나 이상한 링크를 폼에 반복해서 보냅니다.
기본적인 방지 방법은 다음과 같습니다.
너무 짧은 문의 내용은 막기
같은 사람이 너무 자주 제출하지 못하게 하기
숨겨진 입력칸을 두고 봇을 걸러내기
Cloudflare Turnstile, reCAPTCHA 같은 인증 도구 사용하기
폼 서비스의 스팸 방지 기능 활용하기
처음에는 폼 서비스의 기본 스팸 방지 기능을 쓰는 편이 쉽습니다. 자체 구현은 생각보다 신경 쓸 것이 많습니다.
개인정보 안내
이름, 이메일, 전화번호는 개인정보입니다. 단순 문의라도 개인정보를 받는다면 "왜 받는지, 어디에 쓰는지, 얼마나 보관하는지"를 안내해야 합니다.
실제 사업용 사이트라면 개인정보처리방침 페이지를 만들고, 폼 근처에 링크를 두는 것이 좋습니다.
개인정보 안내 문구는 길고 어렵게 쓰기보다, 사용자가 제출 전에 핵심을 이해할 수 있게 적는 것이 좋습니다. 예를 들면 "문의 답변을 위해 이름과 이메일을 수집하며, 답변 완료 후 내부 기준에 따라 보관 또는 삭제합니다"처럼 목적을 분명히 알려 주는 방식입니다. 실제 문구와 보관 기간은 운영하는 서비스의 정책에 맞게 정해야 합니다.
이 부분은 직접 하고, 이 부분은 맡기기
비개발자가 직접 해도 좋은 일은 다음과 같습니다.
문의 폼에 어떤 항목이 필요한지 정하기
이름, 이메일, 문의 내용, 동의 문구 작성하기
입력칸과 버튼의 문구 바꾸기
완료 메시지와 오류 메시지 문장 다듬기
폼 서비스로 간단한 문의 폼 연결하기
전문가에게 맡기면 좋은 일은 다음과 같습니다.
Next.js 서버 기능으로 실제 이메일 발송 연결하기
데이터베이스에 문의 내역 저장하기
관리자 페이지 만들기
스팸 방지와 제출 제한 구현하기
개인정보 저장, 삭제, 보안 정책 검토하기
결제, 로그인, 회원 정보와 연결되는 폼 만들기
폼은 겉으로 보면 단순한 입력칸입니다. 하지만 실제 운영에서는 방문자의 정보를 다루는 입구입니다. 그래서 화면은 직접 만들 수 있어도, 중요한 데이터 처리는 신중하게 결정해야 합니다.
자주 만나는 오류와 확인 순서
폼을 만들 때 초보자가 자주 만나는 문제는 거의 비슷합니다.
버튼을 눌러도 완료 메시지가 안 보입니다."use client"가 파일 맨 위에 있는지 확인하세요. useState, onClick, onSubmit처럼 사용자 행동에 반응하는 코드는 클라이언트 컴포넌트에서 동작합니다.
Event handlers cannot be passed... 같은 오류가 나옵니다.
서버 컴포넌트에 onClick이나 onSubmit 같은 이벤트를 넣었을 가능성이 큽니다. 이벤트가 들어간 컴포넌트를 별도 파일로 분리하고 맨 위에 "use client"를 추가하세요.
ContactForm을 찾을 수 없다는 오류가 나옵니다.
파일 위치와 import 경로를 확인하세요. components/ContactForm.tsx로 만들었다면 import ContactForm from "@/components/ContactForm";처럼 불러올 수 있습니다. 프로젝트 설정에 따라 @/가 안 되면 상대 경로를 사용해야 할 수도 있습니다.
폼이 실제로 이메일을 보내지 않습니다.
정상입니다. 화면에 폼을 만든 것과 이메일 전송 기능을 연결한 것은 다른 단계입니다. 폼 서비스나 서버 기능을 연결해야 실제 전송이 됩니다.
입력했는데도 "이 입력란을 작성하세요" 같은 안내가 나옵니다.required가 붙은 칸 중 비어 있는 곳이 있는지 확인하세요. 체크박스에도 required가 있으면 체크해야 제출할 수 있습니다. 이메일 칸은 type="email" 때문에 이메일처럼 보이는 형식으로 입력해야 합니다.
이번 장의 핵심
폼은 방문자가 웹사이트에 말을 걸 수 있게 해 주는 장치입니다.
input은 짧은 입력, textarea는 긴 입력, button은 행동 실행에 사용합니다. 이름, 이메일, 문의 내용, 개인정보 동의는 실제 문의 처리에 필요한 최소 구성입니다.
다만 폼을 만든다고 해서 자동으로 이메일이 발송되지는 않습니다. 입력값을 어디로 보낼지 정해야 합니다. 간단한 사이트라면 이메일 링크나 폼 서비스가 현실적이고, 더 복잡한 처리가 필요하다면 Next.js의 서버 기능이나 API를 사용할 수 있습니다.
서버 기능은 보이지 않는 처리 담당자입니다. 방문자가 제출한 값을 확인하고, 저장하고, 알림을 보내고, 결과를 다시 화면에 알려 줍니다.
폼을 운영할 때는 필수 입력 확인, 제출 완료 메시지, 제출 중 상태, 스팸 방지, 개인정보 안내까지 함께 챙겨야 합니다. 그래야 정적인 소개 페이지를 넘어 방문자와 실제로 연결되는 웹사이트가 됩니다.
참고
1Next.js Forms with Server Actions [2026] - Robin Wieruch
2Next.js Form — How to Handle Forms in the App Router (2026)
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.