문의하기
문의하기 폼입니다. 보내기 전에 입력을 검증하고, "보내는 중"과 "보내기 완료"를 화면에 보여 줍니다.
문의하기
핵심 포인트
- 1
보내는 중에는 버튼을 다시 누를 수 없게 하기
보내는 동안 버튼을 누를 수 없게 하고 "보내는 중…"을 표시합니다. 버튼을 누를 수 있으면 같은 문의가 두 번 도착할 수 있습니다.
- 2
보내기가 끝나면 화면 전체를 바꾸기
보내기가 끝나면 폼을 숨기고 "보냈습니다" 화면을 보여 줍니다. 언제쯤 답장할지도 적어 두면 상대방이 안심합니다.
- 3
얼마나 쓸 수 있는지 미리 알려 주기
본문 아래에 "현재 글자 수 / 최대"를 표시하고, 최대를 넘으면 빨간색으로 바꿉니다. 보낸 뒤에 "너무 깁니다"라고 듣는 것보다 친절합니다.
사용한 도구
누르면 제2부의 비교 페이지로
코드 고치기
코드를 고치면 오른쪽 미리보기가 바로 바뀝니다.
import { useState } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { CircleCheckIcon } from "lucide-react" import { Button } from "./components/ui/button" import { Input } from "./components/ui/input" import { Label } from "./components/ui/label" // 문의 유형 const kinds = ["사용 방법", "오류 신고", "기타"] // 입력 검사 규칙(Zod) const schema = z.object({ name: z.string().min(1, "이름을 입력해 주세요"), email: z.string().email("올바른 이메일 주소를 입력해 주세요"), kind: z.enum(kinds, { error: "유형을 선택해 주세요" }), message: z.string().min(10, "10자 이상 입력해 주세요").max(500, "500자 이내로 입력해 주세요"), }) // 보내는… 척을 하는 함수(1.5초 걸린다) function send(values) { return new Promise((resolve) => setTimeout(resolve, 1500)) } // 라벨, 입력란, 오류 메시지를 하나로 묶는다 function Field({ id, label, error, children }) { return ( <div className="flex flex-col gap-2"> <Label htmlFor={id}>{label}</Label> {children} {error && <p className="text-sm text-destructive">{error}</p>} </div> ) } export default function App() { // ① 보내기가 끝났는지(처음에는 false). state에 넣는다 const [sent, setSent] = useState(false) const { register, handleSubmit, watch, reset, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", email: "", kind: undefined, message: "" }, }) // 본문의 현재 글자 수를 세기 위해 본문을 지켜본다 const message = watch("message") // '보내기'를 누르고 입력 검사를 통과하면 실행된다 async function onSubmit(values) { await send(values) // 보내기가 끝날 때까지 기다린다(1.5초) // ② 보내기가 끝났으니 sent를 true로 한다 setSent(true) } // ③ 보내기가 끝나면 완료 화면을 먼저 반환한다 // 조건: 보내기가 끝났는가 if (sent) { return ( <div className="mx-auto my-6 w-full max-w-lg rounded-xl border bg-card p-10 text-center text-card-foreground"> <CircleCheckIcon className="mx-auto size-12 text-emerald-500" /> <p className="mt-4 text-lg font-semibold">보냈습니다</p> <p className="mt-2 text-sm text-muted-foreground"> 문의해 주셔서 감사합니다. 영업일 기준 2일 이내에 이메일로 답변드리겠습니다. </p> {/* ④ 누르면 입력을 비우고(reset) 폼 화면으로 돌아간다(sent를 false로) */} <Button variant="outline" className="mt-6" onClick={() => { reset() setSent(false) }}> 다시 보내기(데모용) </Button> </div > ) } return ( <form onSubmit={handleSubmit(onSubmit)} className="mx-auto my-6 flex w-full max-w-lg flex-col gap-5 rounded-xl border bg-card p-6 text-card-foreground" > <div> <h1 className="text-lg font-semibold">문의하기</h1> <p className="mt-1 text-sm text-muted-foreground">질문이나 의견을 편하게 보내 주세요.</p> </div> <Field id="name" label="이름" error={errors.name?.message}> <Input id="name" placeholder="박지훈" {...register("name")} /> </Field> <Field id="email" label="이메일" error={errors.email?.message}> <Input id="email" type="email" placeholder="jihoon@example.com" {...register("email")} /> </Field> <div className="flex flex-col gap-2"> <Label>유형</Label> <div className="flex flex-wrap gap-2"> {kinds.map((kind) => ( <label key={kind} className="cursor-pointer rounded-full border px-3 py-1.5 text-sm has-checked:border-primary has-checked:bg-primary has-checked:text-primary-foreground" > <input type="radio" value={kind} className="sr-only" {...register("kind")} /> {kind} </label> ))} </div> {errors.kind && <p className="text-sm text-destructive">{errors.kind.message}</p>} </div> <Field id="message" label="문의 내용" error={errors.message?.message}> <textarea id="message" rows={5} placeholder="10자 이상 입력해 주세요" className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" {...register("message")} /> {/* ⑦ 500자를 넘으면 빨간색(text-destructive), 그 밖에는 회색(text-muted-foreground) */} <p className={message.length > 500 ? "text-right text-xs text-destructive" : "text-right text-xs text-muted-foreground"}> {/* ⑥ 본문의 현재 글자 수 */} {message.length} / 500 </p> </Field> {/* ⑤ 보내는 중(isSubmitting이 true)에는 누를 수 없다. 글자는 '보내는 중…', 그 밖에는 '보내기' */} <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "보내는 중…" : "보내기"} </Button> </form> ) }