프로젝트 : API 라우트

·2024년 4월 4일
0

NextJS

목록 보기
13/26
post-thumbnail

🔗 레파지토리에서 보기

📌 스스로 해보기

  1. 뉴스레터 구독 기능 추가 : 이메일 주소를 일단 콘솔에 기록
  2. 댓글 제출 기능

📖 뉴스레터 구독 기능 추가

💎 components/input/newsletter-registration.js

import { useRef } from "react";
import classes from "./newsletter-registration.module.css";

// api/newsletter로 fetch 전송
function NewsletterRegistration() {
  const emailInputRef = useRef();

  function registrationHandler(event) {
    event.preventDefault();
    const userEmail = emailInputRef.current.value;

    if (!userEmail || userEmail.trim() === "" || !userEmail.includes("@")) {
      console.log("이메일을 입력해주세요.");
    }

    const registrationEmail = { email: userEmail };

    fetch("api/newsletter", {
      method: "POST",
      body: JSON.stringify(registrationEmail),
      headers: {
        "Content-Type": "application/json",
      },
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        emailInputRef.current.value = null;
      });
    // fetch user input (state or refs)
    // optional: validate input
    // send valid data to API
  }

  return (
    <section className={classes.newsletter}>
      <h2>Sign up to stay updated!</h2>
      <form onSubmit={registrationHandler}>
        <div className={classes.control}>
          <input
            type="email"
            id="email"
            placeholder="Your email"
            aria-label="Your email"
            ref={emailInputRef}
          />
          <button>Register</button>
        </div>
      </form>
    </section>
  );
}

export default NewsletterRegistration;

💎 pages/api/newsletter.js

export default function handler(req, res) {
  if (req.method === "POST") {
    const registrationEmail = req.body.email;

    res.status(200).json({
      message: "뉴스레터 구독에 성공하였습니다.",
      email: registrationEmail,
    });
  }
}

📖 댓글 기능

💎 components/input/comments.js

import { useState } from "react";

import CommentList from "./comment-list";
import NewComment from "./new-comment";
import classes from "./comments.module.css";

function Comments(props) {
  const { eventId } = props;

  const [showComments, setShowComments] = useState(false);

  function toggleCommentsHandler() {
    setShowComments((prevStatus) => !prevStatus);
  }

  function addCommentHandler(commentData) {
    // send data to API
    const newComment = {
      id: eventId,
      ...commentData,
    };
    console.log(newComment);
    fetch("/api/comment", {
      method: "POST",
      body: JSON.stringify(newComment),
      headers: { "Content-Type": "application/json" },
    })
      .then((response) => response.json())
      .then((data) => console.log(data));
  }

  return (
    <section className={classes.comments}>
      <button onClick={toggleCommentsHandler}>
        {showComments ? "Hide" : "Show"} Comments
      </button>
      {showComments && <NewComment onAddComment={addCommentHandler} />}
      {showComments && <CommentList />}
    </section>
  );
}

export default Comments;

💎 pages/api/comment.js

export default function handler(req, res) {
  if (req.method === "POST") {
    const id = req.body.id;
    const commentId = new Date().toISOString();
    const email = req.body.email;
    const name = req.body.name;
    const text = req.body.text;

    const newComment = {
      id,
      commentId,
      email,
      name,
      text,
    };

    res.status(200).json({
      message: "새로운 댓글 생성에 성공했습니다.",
      comment: newComment,
    });
  }
}

📌 프로젝트

📖 뉴스레터 경로 추가하기

💎 pages/api/newsletter.js

export default function handler(req, res) {
  if (req.method === "POST") {
    const registrationEmail = req.body.email;

    if (!registrationEmail || !registrationEmail.includes("@")) {
      res.status(422).json({
        message: "유효하지 않은 이메일입니다. 이메일을 다시 입력해주세요.",
      }); // 422 : 사용자 입력값이 유효하지 않다는 코드.
      return;
    }

    res.status(201).json({
      message: "뉴스레터 구독에 성공하였습니다.",
      email: registrationEmail,
    });
  }
}

💎 components/input/newsletter-registration.js

import { useRef } from "react";
import classes from "./newsletter-registration.module.css";

function NewsletterRegistration() {
  const emailInputRef = useRef();

  function registrationHandler(event) {
    event.preventDefault();

    const enteredEmail = emailInputRef.current.value;

    fetch("api/newsletter", {
      method: "POST",
      body: JSON.stringify({ email: enteredEmail }),
      headers: {
        "Content-Type": "application/json",
      },
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        emailInputRef.current.value = null;
      });
    // fetch user input (state or refs)
    // optional: validate input
    // send valid data to API
  }

  return (
    <section className={classes.newsletter}>
      <h2>Sign up to stay updated!</h2>
      <form onSubmit={registrationHandler}>
        <div className={classes.control}>
          <input
            type="email"
            id="email"
            placeholder="Your email"
            aria-label="Your email"
            ref={emailInputRef}
          />
          <button>Register</button>
        </div>
      </form>
    </section>
  );
}

export default NewsletterRegistration;

📖 댓글 API 라우트 추가하기

💎 pages/api/comments/[eventId].js

  • 댓글은 각 이벤트의 id에 따라 달라지므로 동적으로 구성해야한다.
export default function handler(req, res) {
  const eventId = req.query.eventId;

  if (req.method === "POST") {
    const { email, name, text } = req.body;

    if (
      !email ||
      !email.inclues("@") ||
      !name ||
      name.trim() === "" ||
      !text ||
      text.trim() === ""
    ) {
      res
        .status(422)
        .json({ message: "유효하지 않은 값입니다. 다시 입력해주세요." });
      return;
    }

    const newComment = {
      id: new Date().toISOString(),
      email,
      name,
      text,
    };

    res.status(201).json({
      message: "새로운 댓글 생성에 성공했습니다.",
      comment: newComment,
    });
  }

  if (req.method === "GET") {
    const dummyList = [
      { id: "c1", name: "Zoe", email: "test@test.com", text: "1등" },
      { id: "c2", name: "Taemin", email: "test@test.com", text: "2등" },
    ];

    res.status(200).json({ comments: dummyList });
  }
}

📖 프론트엔드를 Comment API 라우트로 연결하기

💎 components/input/comments.js

import { useEffect, useState } from "react";

import CommentList from "./comment-list";
import NewComment from "./new-comment";
import classes from "./comments.module.css";

function Comments(props) {
  const { eventId } = props;
  const [comments, setComments] = useState([]);
  const [showComments, setShowComments] = useState(false);

  useEffect(() => {
    if (showComments) {
      fetch(`/api/comments/${eventId}`)
        .then((response) => response.json())
        .then((data) => {
          setComments(data.comments);
        });
    }
  }, [showComments]);

  console.log(comments);

  function toggleCommentsHandler() {
    setShowComments((prevStatus) => !prevStatus);
  }

  function addCommentHandler(commentData) {
    // send data to API
    fetch(`/api/comments/${eventId}`, {
      method: "POST",
      body: JSON.stringify(commentData),
      headers: { "Content-Type": "application/json" },
    })
      .then((response) => response.json())
      .then((data) => console.log(data));
  }

  return (
    <section className={classes.comments}>
      <button onClick={toggleCommentsHandler}>
        {showComments ? "Hide" : "Show"} Comments
      </button>
      {showComments && <NewComment onAddComment={addCommentHandler} />}
      {showComments && <CommentList items={comments} />}
    </section>
  );
}

export default Comments;

💎 components/input/comment-list.js

import classes from "./comment-list.module.css";

function CommentList({ items }) {
  return (
    <ul className={classes.comments}>
      {/* Render list of comments - fetched from API */}
      {items.map((item) => (
        <li key={item.id}>
          <p>{item.text}</p>
          <div>
            By <address>{item.name}</address>
          </div>
        </li>
      ))}
    </ul>
  );
}

export default CommentList;


📌 MongoDB 사용하기

📖 MongoDB 데이터베이스 설정하기

  • MongoDB 페이지에서 회원가입 → 무료 클러스터 설정
  • 설치 : npm install mongodb → mongodb 패키지가 MongoDB를 위한 공식 드라이버
    • 보안 상의 문제로 서버 사이드(즉 API 라우트)에만 데이터베이스 코드를 실행하도록 해야한다.
  • MongoDB의 클러스터에서 connect 클릭

📖 API 라우트의 내부로부터 MongoDB 쿼리 실행하기

🔗 npm mongodb

💎 pages/api/newsletter.js

  • mongodb의 url에 들어갈 password는 MongoDB 페이지의 Database Access 에서 설정 가능하다.
import { MongoClient } from "mongodb";

export default async function handler(req, res) {
  if (req.method === "POST") {
    const registrationEmail = req.body.email;

    if (!registrationEmail || !registrationEmail.includes("@")) {
      res.status(422).json({
        message: "유효하지 않은 이메일입니다. 이메일을 다시 입력해주세요.",
      }); // 사용자 입력값이 유효하지 않다는 코드.
      return;
    }

    const client = await MongoClient.connect(
      "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/?retryWrites=true&w=majority&appName=<name>"
    );
    const db = client.db("newsletter");
    await db.collection("emails").insertOne({ email: registrationEmail });

    client.close();

    res.status(201).json({
      message: "뉴스레터 구독에 성공하였습니다.",
    });
  }
}


📖 데이터베이스로 댓글 삽입하기

💎 pages/api/comments/[eventId].js

import { MongoClient } from "mongodb";

export default async function handler(req, res) {
  const eventId = req.query.eventId;

  const client = await MongoClient.connect(
    "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/events?retryWrites=true&w=majority&appName=<name>"
  );

  if (req.method === "POST") {
    const { email, name, text } = req.body;

    if (
      !email ||
      !email.includes("@") ||
      !name ||
      name.trim() === "" ||
      !text ||
      text.trim() === ""
    ) {
      res
        .status(422)
        .json({ message: "유효하지 않은 값입니다. 다시 입력해주세요." });
      return;
    }

    const newComment = {
      email,
      name,
      text,
      eventId,
    };

    const db = client.db();
    const result = await db.collection("comments").insertOne({ newComment });

    console.log(result);

    res.status(201).json({
      message: "새로운 댓글 생성에 성공했습니다.",
    });
  }

  if (req.method === "GET") {
    const dummyList = [
      { id: "c1", name: "Zoe", email: "test@test.com", text: "1등" },
      { id: "c2", name: "Taemin", email: "test@test.com", text: "2등" },
    ];

    res.status(200).json({ comments: dummyList });
  }

  client.close();
}

// {
//   acknowledged: true,
//   insertedId: new ObjectId('660e4be8af131abdddfc9dc7')
// }
  • 아예 events라는 이름의 테이블 생성 후, 그 안에 comments와 newsletter가 들어가도록 했다.


📖 데이터베이스로부터 데이터 가져오기

💎 pages/api/comments/[eventId].js

import { MongoClient } from "mongodb";

export default async function handler(req, res) {
  const eventId = req.query.eventId;

  const client = await MongoClient.connect(
    "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/events?retryWrites=true&w=majority&appName=<name>"
  );

  if (req.method === "POST") {
    const { email, name, text } = req.body;

    if (
      !email ||
      !email.includes("@") ||
      !name ||
      name.trim() === "" ||
      !text ||
      text.trim() === ""
    ) {
      res
        .status(422)
        .json({ message: "유효하지 않은 값입니다. 다시 입력해주세요." });
      return;
    }

    const newComment = {
      email,
      name,
      text,
      eventId,
    };

    const db = client.db();
    const result = await db.collection("comments").insertOne({ newComment });

    console.log(result);

    res.status(201).json({
      message: "새로운 댓글 생성에 성공했습니다.",
    });
  }

  if (req.method === "GET") {
    const db = client.db();
    const documents = await db
      .collection("comments")
      .find()
      .sort({ _id: -1 })
      .toArray(); //

    res.status(200).json({ comments: documents });
  }

  client.close();
}
  • documents를 받아올 때, toArray()를 실행한 이유 : 기본값은 배열이 아니라 커서가 나타난다. 단순히 comments 컬렉션의 모든 엔트리를 배열로 제공.
  • sort({ _id: -1 }) : _id를 기준으로 내림차순으로 정렬

📖 에러 핸들링 추가하기

  • 데이터베이스 연결이 실패하거나 데이터 삽입에 실패할 수도 있기 때문에 데이터베이스에 대한 에러 핸들링이 필요하다.

💎 pages/api/newsletter.js

import { MongoClient } from "mongodb";

async function connectDB() {
  const client = await MongoClient.connect(
    "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/events?retryWrites=true&w=majority&appName=<name>"
  );

  return client;
}

async function insertDocument(client, document) {
  const db = client.db();
  await db.collection("newsletter").insertOne({ email: document });
}

export default async function handler(req, res) {
  if (req.method === "POST") {
    const registrationEmail = req.body.email;

    if (!registrationEmail || !registrationEmail.includes("@")) {
      res.status(422).json({
        message: "유효하지 않은 이메일입니다. 이메일을 다시 입력해주세요.",
      }); // 사용자 입력값이 유효하지 않다는 코드.
      return;
    }

    let client;

    // DB 연결 에러 핸들링
    try {
      client = await connectDB();
    } catch (error) {
      res.status(500).json({ message: "데이터베이스 연결 실패" });
      return;
    }

    // 데이터 저장 에러 핸들링
    try {
      await insertDocument(client, { email: registrationEmail });
      client.close();
    } catch (error) {
      res.status(500).json({ message: "데이터 삽입 실패" });
    }

    res.status(201).json({
      message: "뉴스레터 구독에 성공하였습니다.",
    });
  }
}


📖 에러 핸들링 더 자세히 살펴보기

💎 helper/db-util.js

import { MongoClient } from "mongodb";
export async function connectDB() {
  const client = await MongoClient.connect(
    "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/events?retryWrites=true&w=majority&appName=<name>"
  );

  return client;
}

export async function insertDocument(client, collection, document) {
  const db = client.db();
  const result = await db.collection(collection).insertOne({ email: document });
  return result;
}

export async function getAllDocument(client, collection, sort) {
  const db = client.db();
  const documents = await db.collection(collection).find().sort(sort).toArray(); //기본값은 배열이 아니라 커서가 나타난다. 단순히 comments 컬렉션의 모든 엔트리를 배열로 제공.(_id를 기준으로 내림차순)
  return documents;
}

💎 pages/api/newsletter.js

import { connectDB, insertDocument } from "../../helper/db-util";
export default async function handler(req, res) {
  if (req.method === "POST") {
    const registrationEmail = req.body.email;

    if (!registrationEmail || !registrationEmail.includes("@")) {
      res.status(422).json({
        message: "유효하지 않은 이메일입니다. 이메일을 다시 입력해주세요.",
      }); // 사용자 입력값이 유효하지 않다는 코드.
      return;
    }

    let client;

    try {
      client = await connectDB();
    } catch (error) {
      res.status(500).json({ message: "데이터베이스 연결 실패" });
      return;
    }

    try {
      await insertDocument(client, "newsletter", { email: registrationEmail });
      client.close();
    } catch (error) {
      res.status(500).json({ message: "데이터 삽입 실패" });
      return;
    }

    res.status(201).json({
      message: "뉴스레터 구독에 성공하였습니다.",
    });
  }
}

💎 pages/api/comments/[eventId].js

import {
  connectDB,
  insertDocument,
  getAllDocument,
} from "../../../helper/db-util";
export default async function handler(req, res) {
  const eventId = req.query.eventId;

  let client;
  try {
    client = await connectDB();
  } catch (error) {
    res.status(500).json({ message: "DB 연결 실패" });
    return;
  }

  if (req.method === "POST") {
    const { email, name, text } = req.body;

    if (
      !email ||
      !email.includes("@") ||
      !name ||
      name.trim() === "" ||
      !text ||
      text.trim() === ""
    ) {
      res
        .status(422)
        .json({ message: "유효하지 않은 값입니다. 다시 입력해주세요." });
      client.close();
      return;
    }

    const newComment = {
      email,
      name,
      text,
      eventId,
    };

    let result;
    try {
      result = await insertDocument(client, "comments", { newComment });
      newComment._id = result.insertedId;
      res.status(201).json({ message: "새로운 댓글 생성에 성공했습니다." });
    } catch (error) {
      res.status(500).json({ message: "댓글 삽입 실패" }); // client.close() 작업을 위해 return 하지 않음
    }
  }

  if (req.method === "GET") {
    try {
      const documents = await getAllDocument(client, "comments", { _id: -1 });
      res.status(200).json({ comments: documents });
    } catch (error) {
      res.status(500).json({ message: "댓글 데이터 가져오기 실패" });
    }
  }

  client.close();
}
  • 현재 client.close()를 이용해 MongoDB의 연결을 항상 닫는다. 하지만 MongoDB 관련 코드가 자주 실행되는 어플리케이션을 구축하는 경우, MongoDB의 Connection Pool을 활용하는 것이 좋다.

🔗 Connection Pool 블로그 참고


📖 (+) 특정 이벤트에 대한 댓글 가져오기

💎 helper/db-util.js

import { MongoClient } from "mongodb";
export async function connectDB() {
  const client = await MongoClient.connect(
    "mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/events?retryWrites=true&w=majority&appName=<name>"
  );

  return client;
}

export async function insertDocument(client, collection, document) {
  const db = client.db();
  const result = await db.collection(collection).insertOne({ email: document });
  return result;
}

// filter = {} 추가
export async function getAllDocument(client, collection, sort, filter = {}) {
  const db = client.db();
  const documents = await db
    .collection(collection)
    .find(filter) // 추가
    .sort(sort)
    .toArray();
  return documents;
}

💎 pages/api/comments/[eventId].js

import {
  connectDB,
  insertDocument,
  getAllDocument,
} from "../../../helper/db-util";
export default async function handler(req, res) {
  const eventId = req.query.eventId;

  let client;
  try {
    client = await connectDB();
  } catch (error) {
    res.status(500).json({ message: "DB 연결 실패" });
    return;
  }

  if (req.method === "POST") {
    const { email, name, text } = req.body;

    if (
      !email ||
      !email.includes("@") ||
      !name ||
      name.trim() === "" ||
      !text ||
      text.trim() === ""
    ) {
      res
        .status(422)
        .json({ message: "유효하지 않은 값입니다. 다시 입력해주세요." });
      client.close();
      return;
    }

    const newComment = {
      email,
      name,
      text,
      eventId,
    };

    let result;
    try {
      result = await insertDocument(client, "comments", { newComment });
      newComment._id = result.insertedId;
      res.status(201).json({ message: "새로운 댓글 생성에 성공했습니다." });
    } catch (error) {
      res.status(500).json({ message: "댓글 삽입 실패" }); // client.close() 작업을 위해 return 하지 않음
    }
  }

  if (req.method === "GET") {
    try {
      const documents = await getAllDocument(
        client,
        "comments",
        { _id: -1 },
        { eventId: eventId } // 추가
      );
      res.status(200).json({ comments: documents });
    } catch (error) {
      res.status(500).json({ message: "댓글 데이터 가져오기 실패" });
    }
  }

  client.close();
}
  • GET 메서드일 때, getAllDocument(client,"comments",{_id:-1},{eventId:eventId})로 설정하여 특정 이벤트에 속한 댓글만을 가져오게 한다.

0개의 댓글

관련 채용 정보