NextJS Part2

남재상·2025년 4월 6일
post-thumbnail

NextJS to CodingApple

코딩애플 강의를 통해 배운 NextJS를 정리한 글입니다.

📅 작성일

2025년 4월 05일


📌 목차

  1. 소개
  2. part2-1 : 새로운 프로젝트 생성 / MongoDB 셋팅
  3. part2-2 : Next.js에서 MongoDB 사용하기
  4. part2-3 : 글목록 조회기능 만들기 (DB 데이터 출력)
  5. part2-4 : 상세페이지 만들기 1 (Dynamic route)
  6. part2-5 : 상세페이지 만들기 2 (useRouter)
  7. part2-6 : 글 작성기능 만들기 1 (서버기능 개발은)
  8. part2-7 : 글 작성기능 만들기 2
  9. part2-8 : 수정기능 만들기 1
  10. part2-9 : 수정기능 만들기 2
  11. part2-10 : 삭제기능 만들기 1 (Ajax)
  12. part2-11 : part2-11 삭제기능 만들기 2 (Ajax 추가내용과 에러처리)
  13. part2-12 : 삭제기능 만들기 3 (query string / URL parameter)
  14. part2-13 : static rendering, dynamic rendering, cache
  15. part2-14 : JWT, session, OAuth 설명시간
  16. part2-15 : 회원기능 만들기 : Auth.js 사용한 소셜로그인
  17. part2-16 : 회원기능 만들기 : OAuth + session방식 사용하기
  18. part2-17 : 회원기능 만들기 : 아이디/비번 + JWT 사용하기
  19. part2-18 : 회원기능 만들기 : JWT 사용시 refresh token 사용하려면
  20. part2-19 : 댓글기능 만들기 1 (input 데이터 다루기)
  21. part2-20 : 댓글기능 만들기 2 (useEffect)
  22. part2-21 : 댓글기능 만들기 3
  23. part2-22 : loading.js, error.js, not-found.js
  24. part2-23 : AWS Elastic Beanstalk에 Next.js서버 배포
  25. part2-24 : 이미지 업로드 기능 1 (AWS S3 셋팅)
  26. part2-25 : 이미지 업로드 기능 2 (Presigned URL)
  27. part2-26 : Dark mode 기능 1 (cookies / localStorage)
  28. part2-27 : Dark mode 기능 2
  29. part2-28 : 서버기능 중간에 간섭하려면 Middleware
  30. part2-29 : Next.js의 Server actions 기능
  31. 참고 자료

📝 소개

Part 2 : 게시판 프로젝트


part2-1

🔹 MongoDB

  • 비관계형
  • SNS 서비스처럼 많은 데이터 입출력이 필요할 때 사용
  • 몽고디비 Database Access / Network Access 두개 적용하기

part2-2

🔹 MongoDB설치

  • npm install mongodb
// database.js
import { MongoClient } from "mongodb";

const url =
  "mongodb+srv://admin:fkaus123@cluster0.zrch4.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0";
const options = { useNewUrlParser: true };
let connectDB;

if (process.env.NODE_ENV === "development") {
  if (!global._mongo) {
    global._mongo = new MongoClient(url, options).connect();
  }
  connectDB = global._mongo;
} else {
  connectDB = new MongoClient(url, options).connect();
}
export { connectDB };

// page.js
import { connectDB } from "@/util/database";

export default async function Home() {
  const client = await connectDB;
  const db = client.db("forum");

  let result = await db.collection("post").find().toArray();
  console.log(result);
  return (
    <div>
      <div>a</div>
    </div>
  );
}

part2-3

🔹 DB의 데이터 적용


part2-4

🔹 상세폴더

  • [작명]을 통해 url뒤에오는 값을 정할 수 있다
// Next.js 15버전
export default async function Detail({ params }) {
  const { id } = await params;
  return()
}


// 14이하
export default async function Page({ params }) {
  const id = params.id;
  return <p>{id}</p>;
}

part2-5

🔹 client router

  • router.prefetch : 미리 로드
  • usePathname : url
  • useSearchParams : query
"use client";

import { useRouter } from "next/navigation";

export default function DetailLink() {
  let router = useRouter();
  return (
    <button
      onClick={() => {
        router.push("/");
      }}
    ></button>
  );
}

part2-6

🔹 서버개발발

  • pages/api/test.js 구조로 개발
// test.js
export default function handler(요청, 응답) {
  if (요청.method == "POST") {
    return 응답.status(200).json("처리완료");
  }
}

part2-7

🔹 insert

import { connectDB } from "@/util/database";

export default async function handler(요청, 응답) {
  if (요청.method == "POST") {
    const db = (await connectDB).db("forum");
    let result = await db.collection("post").insertOne(요청.body);
    return 응답.status(200).redirect("/list");
  }
}

part2-8

🔹수정기능 세팅


part2-9

🔹업데이트

await db
  .collection("post")
  .updateOne({ _id: new ObjectId(요청.body.id) }, { $set: 바꿀꺼 });

🔹redirect

  • return 없이 응답.status(302).redirect("/list");

part2-10

🔹삭제

  • 삭제할 떄 fetch를 사용
  • SSR페이지에서 props로 데이터를 넘겨준걸 받아서 사용

part2-11

🔹배운거

// list.js
onClick={() => {
  fetch("/api/post/delete", {
    method: "DELETE",
    body: item._id,
  })
    .then((r) => {
      if (r.status == 200) {
        return r.json();
      } else {
        //서버가 에러코드전송시 실행할코드
      }
    })
    .then((result) => {
      //성공시 실행할코드
    })
    .catch((error) => {
      //인터넷문제 등으로 실패시 실행할코드
      console.log(error);
    });
}}


// delete.js
import { connectDB } from "@/util/database";
import { ObjectId } from "mongodb";

export default async function handler(요청, 응답) {
  if (요청.method == "DELETE") {
    const db = (await connectDB).db("forum");
    let result = await db
      .collection("post")
      .deleteOne({ _id: new ObjectId(요청.body) });
    return 응답.status(200).json("삭제됨");
  }
}

part2-12

query string

  • 데이터많으면 더러움
  • URL에 데이터 노출됨
fetch("/api/test?name=kim&age=20");

parameter

  • 데이터많으면 더러움
  • URL에 데이터 노출됨
fetch("/api/test/ㅇㅁㄴㅇㅁ");

// api test
export default function handler(요청, 응답) {
  console.log(요청.qurery); // ㅇㅁㄴㅇㅁ
}

//directory
pages / api / test / [작명.js];

part2-13

배포

  • npm run build 후 npm run start시 배포환경보여줌

static rendering

  • Next.js에서 페이지를 하나 만들면 기본적으로 static rendering 식으로 페이지를 보여줌
  • 유저가 접속할 때 마다 npm run build시 생성한 html을 그대로 보내주는 것을 말함
    (페이지 안에 별 기능이 없어서 매번 html 페이지를 새로 만들 필요가 없으니까 그냥 그대로 보내는 것임)
  • ㅇ로 표시되면 static rendering

dynamic rendering

  • 데이터 가져오는 문법 등 유저가 페이지 접속시 html에 변동사항이 들어가야할 때 적용용
  • λ로 표시되면 dynamic rendering

렌더링 바꾸는법

  • 서버가 부담될 수 있음
export const dynamic = "force-dynamic";

export const dynamic = "force-static";

cache

  • 재사용
fetch("/URL", { cache: "force-cache" });
fetch("/URL", { cache: "no-store" });

// 그 외
fetch("/URL", { next: { revalidate: 60 } });

export const revalidate = 60;

part2-14

JWT, session, OAuth 설명시간


part2-15

깃허브 OAuth 세팅

// 설치
npm install next-auth


// 디렉토리
pages/api/auth/[...nextauth].js


// nextauth.js
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: "Ov23limmnbY0Aqd6DvNU",
      clientSecret: "8c39afc500e87b2191bf5197c3b28b1ba895a7ec",
    }),
  ],
  secret: "0630",
};
export default NextAuth(authOptions);


// 로그인
"use client";

import { signIn } from "next-auth/react";

export default function LoginBtn() {
  return (
    <button
      onClick={() => {
        signIn();
      }}
    >
      로그인
    </button>
  );
}

// 데이터
import { authOptions } from "@/pages/api/auth/[...nextauth]";

let session = await getServerSession(authOptions);
console.log(session);

part2-16

DB adapter

  • npm install @next-auth/mongodb-adapter
// nextauth.js
import { connectDB } from "@/util/database";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: "Ov23limmnbY0Aqd6DvNU",
      clientSecret: "8c39afc500e87b2191bf5197c3b28b1ba895a7ec",
    }),
  ],
  secret: "0630",
  adapter: MongoDBAdapter(connectDB),
};
export default NextAuth(authOptions);


// api.js
import { connectDB } from "@/util/database";
import { getServerSession } from "next-auth";
import { authOptions } from "@/pages/api/auth/[...nextauth]";

export default async function handler(요청, 응답) {
  let session = await getServerSession(요청, 응답, authOptions);

  if (session) {
    요청.body.author = session.user.email;
  }

  if (요청.method == "POST") {
    const db = (await connectDB).db("forum");
    let result = await db.collection("post").insertOne(요청.body);
    return 응답.status(200).redirect("/list");
  }
}

정보

  • account, session, users등이 있음
  • users는 가입된 유저정보
  • account는 유저들의 계정을 보관

part2-17

암호화

  • npm install bcrypt
import { connectDB } from "@/util/database";
import bcrypt from "bcrypt";

export default async function handler(요청, 응답) {
  if (요청.method == "POST") {
    let hash = await bcrypt.hash(요청.body.password, 10);
    요청.body.password = hash;
    let db = (await connectDB).db("forum");
    await db.collection("user_cred").insertOne(요청.body);
    응답.status(200).json("성공");
  }
}

JWT적용

import { connectDB } from "@/util/database";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: "Ov23limmnbY0Aqd6DvNU",
      clientSecret: "8c39afc500e87b2191bf5197c3b28b1ba895a7ec",
    }),

    CredentialsProvider({
      //1. 로그인페이지 폼 자동생성해주는 코드
      name: "credentials",
      credentials: {
        email: { label: "email", type: "text" },
        password: { label: "password", type: "password" },
      },

      //2. 로그인요청시 실행되는코드
      //직접 DB에서 아이디,비번 비교하고
      //아이디,비번 맞으면 return 결과, 틀리면 return null 해야함
      async authorize(credentials) {
        let db = (await connectDB).db("forum");
        let user = await db
          .collection("user_cred")
          .findOne({ email: credentials.email });
        if (!user) {
          console.log("해당 이메일은 없음");
          return null;
        }
        const pwcheck = await bcrypt.compare(
          credentials.password,
          user.password
        );
        if (!pwcheck) {
          console.log("비번틀림");
          return null;
        }
        return user;
      },
    }),
  ],
  //3. jwt 써놔야 잘됩니다 + jwt 만료일설정
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, //30일
  },

  callbacks: {
    //4. jwt 만들 때 실행되는 코드
    //user변수는 DB의 유저정보담겨있고 token.user에 뭐 저장하면 jwt에 들어갑니다.
    jwt: async ({ token, user }) => {
      if (user) {
        token.user = {};
        token.user.name = user.name;
        token.user.email = user.email;
      }
      return token;
    },
    //5. 유저 세션이 조회될 때 마다 실행되는 코드
    session: async ({ session, token }) => {
      session.user = token.user;
      return session;
    },
  },
  secret: "0630",
  adapter: MongoDBAdapter(connectDB),
};
export default NextAuth(authOptions);

part2-18

고장남


part2-19

몰랐던것

  • form형태로 통신하는건 화면 새로고침이 된다

part2-20

몽고디비

  • 그냥 여기서 대충 닉네임 지어서 insert하면 테이블이 만들어짐;

part2-21

DB에 이름

  • 관계형 DB에는 이름 넣는거 안좋지만 비관계형은 좋은 관습

part2-22

loading.js

  • 파일 자체에 loading.js파일을 만들어 두면 라우팅 될 때 알아서 이 파일을 보여준다
  • Suspense랑 같음
  • app 상위폴더에 둬도 됨

error.js

  • 파일 자체에 error.js파일을 만들어 두면 라우팅 될 때 알아서 이 파일을 보여준다
  • use client임
  • props로 error와 reset의 값을 전달해준다
  • app 상위폴더에 둬도 됨

not-found.js

  • 파일 자체에 not-found.js파일을 만들어 두면 라우팅 될 때 알아서 이 파일을 보여준다
  • import { notFound } from "next/navigation"; notFound();
  • app 상위폴더에 둬도 됨

part2-23

AWS적용법


part2-24

AWS S3 셋팅


part2-25

Presigned URL


part2-26

localStorage

  • 로컬저장
  • 클라에서만 실행가능
localStorage.setItem("자료이름", "값");
localStorage.getItem("자료이름");
localStorage.removeItem("자료이름");
  • cookie컬저장
document.cookie = "쿠키이름=값";
document.cookie = "쿠키이름=값; max-age=3600";

import { cookies } from "next/headers";

export default function 서버컴포넌트() {
  let result = cookies().get("쿠키이름");
  console.log(result);
}

part2-27

Dark mode

<body
  className={`${geistSans.variable} ${geistMono.variable} ${
    res !== undefined && res?.value === "dark" ? "dark-mode" : ""
  }`}
/>;

("use client");

import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Darkmode() {
  let router = useRouter();
  useEffect(() => {
    let 쿠키값 = ("; " + document.cookie).split(`; mode=`).pop().split(";")[0];
    if (쿠키값 == "") {
      document.cookie = "mode=light; max-age=" + 3600 * 24 * 400;
    }
  });
  return (
    <span
      onClick={() => {
        let 쿠키값 = ("; " + document.cookie)
          .split(`; mode=`)
          .pop()
          .split(";")[0];
        if (쿠키값 == "light") {
          document.cookie = "mode=dark; max-age=" + 3600 * 24 * 400;
          router.refresh();
        } else {
          document.cookie = "mode=light; max-age=" + 3600 * 24 * 400;
          router.refresh();
        }
      }}
    >
      {" "}
      🌙{" "}
    </span>
  );
}

part2-28

Middleware

  • 요청과 응답사이
  • middleware.js 파일 만들기
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";

export async function middleware(request) {
  const session = await getToken({ req: request });

  if (request.nextUrl.pathname.startsWith("/write")) {
    const session = await getToken({ req: request });
    console.log("세션", session);
    if (session == null) {
      return NextResponse.redirect(new URL("/api/auth/signin", request.url));
    }
  }

  if (request.nextUrl.pathname === "/list") {
    console.log(request.headers.get());
    return NextResponse.next();
  }

  request.cookies.get("쿠키이름"); //출력
  request.cookies.has("쿠키이름"); //존재확인
  request.cookies.delete("쿠키이름"); //삭제

  const response = NextResponse.next();
  response.cookies.set({
    name: "mode",
    value: "dark",
    maxAge: 3600,
    httpOnly: true,
  });
  return response;
}

part2-29

Server actions

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
};

export default nextConfig;



import { connectDB } from "@/util/database";
import { revalidatePath } from "next/cache"; //페이지 상단에 추가

export default async function Write2() {
  //DB에서 데이터 뽑아서 보여주기
  const db = (await connectDB).db("forum");
  let result = await db.collection("post_test").find().toArray();

  async function handleSubmit(formData) {
    "use server";
    const db = (await connectDB).db("forum");
    await db
      .collection("post_test")
      .insertOne({ title: formData.get("post1") });
    revalidatePath("/write2");
  }

  return (
    <form action={handleSubmit}>
      <input type="text" name="post1" />
      <button type="submit">Submit</button>
      {result ? result.map((a) => <p>글제목 : {a.title}</p>) : null}
    </form>
  );
}

📚 참고 자료

profile
작은 코드 하나에도 책임을 담는 개발자입니다!

0개의 댓글