🔗 레파지토리에서 보기
📖 문의 양식 준비하기
import classes from "./contact-form.module.css";
export default function ContactForm() {
return (
<section className={classes.contact}>
<h1>How can I help you?</h1>
<form className={classes.form}>
<div className={classes.controls}>
<div className={classes.control}>
<label htmlFor="email">Your Email</label>
<input type="email" id="email" required />
</div>
<div className={classes.control}>
<label htmlFor="name">Your Name</label>
<input type="text" id="name" required />
</div>
</div>
<div className={classes.control}>
<label htmlFor="message">Your Message</label>
<textarea id="message" rows="5" />
</div>
<div className={classes.actions}>
<button>Send Message</button>
</div>
</form>
</section>
);
}
💎 pages/contact.js
import ContactForm from "@/components/contact/contact-form";
export default function ContactPage() {
return <ContactForm />;
}

📖 문의 API 라우트 추가하기
import { useState } from "react";
import classes from "./contact-form.module.css";
export default function ContactForm() {
const [enteredEmail, setEnteredEmail] = useState("");
const [enteredName, setEnteredName] = useState("");
const [enteredMessage, setEnteredMessage] = useState("");
function sendMessageHandler(event) {
event.preventDefault();
fetch("/api/contact", {
method: "POST",
body: JSON.stringify({
email: enteredEmail,
name: enteredName,
message: enteredMessage,
}),
headers: {
"Content-Type": "application/json",
},
});
}
return (
<section className={classes.contact}>
<h1>How can I help you?</h1>
<form className={classes.form} onSubmit={sendMessageHandler}>
<div className={classes.controls}>
<div className={classes.control}>
<label htmlFor="email">Your Email</label>
<input
type="email"
id="email"
required
value={enteredEmail}
onChange={(event) => setEnteredEmail(event.target.value)}
/>
</div>
<div className={classes.control}>
<label htmlFor="name">Your Name</label>
<input
type="text"
id="name"
required
value={enteredName}
onChange={(event) => setEnteredName(event.target.value)}
/>
</div>
</div>
<div className={classes.control}>
<label htmlFor="message">Your Message</label>
<textarea
id="message"
rows="5"
required
value={enteredMessage}
onChange={(event) => setEnteredMessage(event.target.value)}
/>
</div>
<div className={classes.actions}>
<button>Send Message</button>
</div>
</form>
</section>
);
}
- 제출 시, 작동시킬 함수
sendMessageHandler
작성.
💎 pages/api/contact.js
import { MongoClient } from "mongodb";
async function handler(req, res) {
if (req.method === "POST") {
const { email, name, message } = req.body;
if (
!email ||
!email.includes("@") ||
!name ||
name.trim() === "" ||
!message ||
message.trim() === ""
) {
res.status(422).json({ message: "Invalid input." });
return;
}
const newMessage = {
email,
name,
message,
};
let client;
const connectionString =
"mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/<db name>?retryWrites=true&w=majority&appName=<app name>";
try {
client = await MongoClient.connect(connectionString);
} catch (error) {
res.status(500).json({ message: "Could not connect to database." });
return;
}
const db = client.db();
try {
const result = await db.collection("messages").insertOne(newMessage);
newMessage.id = result.insertedId;
} catch (error) {
client.close();
res.status(500).json({ message: "Storing message failed!" });
return;
}
client.close();
res
.status(201)
.json({ message: "Successfully stored message!", message: newMessage });
}
}
export default handler;
- contact-form.js에서 작성한 submit handler의
fetch
는 api/contact로 생성한 메시지 객체를 전송..
- mongodb와 연결하여, 클러스터 내의 db → collection으로 데이터가 저장


📖 알림을 통해 UI 피드백 추가하기
- components/ui 추가 → Notification을 위한 컴포넌트와 스타일시트
import { useEffect, useState } from "react";
import classes from "./contact-form.module.css";
import Notification from "../ui/notification";
async function sendContactData(contactDetails) {
const response = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(contactDetails),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Something went wrong.");
}
}
export default function ContactForm() {
const [enteredEmail, setEnteredEmail] = useState("");
const [enteredName, setEnteredName] = useState("");
const [enteredMessage, setEnteredMessage] = useState("");
const [requestStatus, setRequestStatus] = useState();
const [requestError, setRequestError] = useState();
useEffect(() => {
if (requestStatus === "success" || requestStatus === "error") {
const timer = setTimeout(() => {
setRequestStatus(null);
setRequestError(null);
}, 3000);
return () => clearTimeout(timer);
}
}, [requestStatus]);
async function sendMessageHandler(event) {
event.preventDefault();
setRequestStatus("pending");
try {
await sendContactData({
email: enteredEmail,
name: enteredName,
message: enteredMessage,
});
setRequestStatus("success");
setEnteredEmail("");
setEnteredName("");
setEnteredMessage("");
} catch (error) {
setRequestError(error.message);
setRequestStatus("error");
}
}
let notificationData;
if (requestStatus == "pending") {
notificationData = {
status: "pending",
title: "Sending Message...",
message: "메시지를 전송하는 중입니다.",
};
}
if (requestStatus == "success") {
notificationData = {
status: "success",
title: "Success!",
message: "메시지를 전송하는데 성공했습니다.",
};
}
if (requestStatus == "error") {
notificationData = {
status: "error",
title: "Error!",
message: requestError,
};
}
return (
<section className={classes.contact}>
<h1>How can I help you?</h1>
<form className={classes.form} onSubmit={sendMessageHandler}>
<div className={classes.controls}>
<div className={classes.control}>
<label htmlFor="email">Your Email</label>
<input
type="email"
id="email"
required
value={enteredEmail}
onChange={(event) => setEnteredEmail(event.target.value)}
/>
</div>
<div className={classes.control}>
<label htmlFor="name">Your Name</label>
<input
type="text"
id="name"
required
value={enteredName}
onChange={(event) => setEnteredName(event.target.value)}
/>
</div>
</div>
<div className={classes.control}>
<label htmlFor="message">Your Message</label>
<textarea
id="message"
rows="5"
required
value={enteredMessage}
onChange={(event) => setEnteredMessage(event.target.value)}
/>
</div>
<div className={classes.actions}>
<button>Send Message</button>
</div>
</form>
{notificationData && (
<Notification
status={notificationData.status}
title={notificationData.title}
message={notificationData.message}
/>
)}
</section>
);
}
requestStatus
상태를 추가하여 데이터를 보내는 현 상태가 pending / success / error 인지 파악 → Notification 컴포넌트에 전달
requestError
상태를 추가하여 만약 에러가 발생했다면 에러 메시지를 저장하고 이를 Notification 컴포넌트에 메시지로서 전달할 것.
🔗 레파지토리에서 커밋 히스토리 별로 보기
📌 기타
📖 메타데이터 - head 데이터 추가하기
💎 pages/_app.js
import Head from "next/head";
import "../styles/globals.css";
import Layout from "@/components/layout/layout";
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
💎 pages/index.js
import Head from "next/head";
import Hero from "@/components/home-page/hero";
import FeaturedPosts from "@/components/home-page/featured-posts";
import { getFeaturedPosts } from "@/lib/posts-util";
export default function HomePage({ posts }) {
return (
<>
<Head>
<title>쑤's Blog</title>
<meta
name="description"
content="Next.js를 이용하여 블로그 앱 구축하기"
/>
</Head>
<Hero />
<FeaturedPosts posts={posts} />
</>
);
}
export function getStaticProps() {
const featuredPosts = getFeaturedPosts();
return {
props: {
posts: featuredPosts,
},
};
}
💎 pages/contact.js
import Head from "next/head";
import ContactForm from "@/components/contact/contact-form";
export default function ContactPage() {
return (
<>
<Head>
<title>Contact Me</title>
<meta name="description" content="send me your message" />
</Head>
<ContactForm />
</>
);
}
💎 pages/posts/index.js
import Head from "next/head";
import AllPosts from "@/components/posts/all-posts";
import { getAllPosts } from "@/lib/posts-util";
export default function AllPostsPage({ posts }) {
return (
<>
<Head>
<title>All My Posts</title>
<meta
name="description"
content="A list of all programming-related tutorials and posts!"
/>
</Head>
<AllPosts posts={posts} />
</>
);
}
export function getStaticProps() {
const allPosts = getAllPosts();
return {
props: {
posts: allPosts,
},
};
}
💎 pages/posts/[slug].js
import Head from "next/head";
import PostContent from "@/components/posts/post-detail/post-content";
import { getPostData, getPostsFiles } from "@/lib/posts-util";
export default function PostDetailPage({ post }) {
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
</Head>
<PostContent post={post} />
</>
);
}
export function getStaticProps(context) {
const { params } = context;
const { slug } = params;
const postData = getPostData(slug);
return {
props: {
post: postData,
},
revalidate: 600,
};
}
export function getStaticPaths() {
const postFileNames = getPostsFiles();
const slugs = postFileNames.map((fileName) => fileName.replace(/.md$/, ""));
return {
paths: slugs.map((slug) => ({ params: { slug: slug } })),
fallback: false,
};
}
📖 _document.js 파일 추가하기
💎 pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head></Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
- _document.js 작성 후, 개발 서버 종료 → 재시작
📖 React Portal 사용하기
- _document.js 파일을 활용하여 portal을 통해 notification을 렌더링 하고자 한다.
💎 _document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head></Head>
<body>
<Main />
<NextScript />
<div id="notifications"></div>
</body>
</Html>
);
}
}
export default MyDocument;
- id가 notifications인 div 생성
💎 components/ui/notification.js
import ReactDOM from "react-dom";
import classes from "./notification.module.css";
function Notification(props) {
const { title, message, status } = props;
let statusClasses = "";
if (status === "success") {
statusClasses = classes.success;
}
if (status === "error") {
statusClasses = classes.error;
}
const cssClasses = `${classes.notification} ${statusClasses}`;
return ReactDOM.createPortal(
<div className={cssClasses}>
<h2>{title}</h2>
<p>{message}</p>
</div>,
document.getElementById("notifications")
);
}
export default Notification;
ReactDOM.createPortal
을 이용해서 id가 notifications인 element에 해당 부분을 삽입(?)
