본문 바로가기

Frontend/Next.js

Next.js 사용하기

React는 라이브러리이며, 서버로부터 데이터를 받아와 브라우저에서 실시간으로 HTML을 만드는 Client Side Rendering 방식이다. 이러한 방식은 구글 검색 노출에 오랜 시간이 소요되고, 첫 페이지 로딩 속도가 느리다는 단점이 있다.

반면에, Next.js는 풀스택 프레임워크이며, 프론트엔드(React 문법)랑 백엔드를 모두 개발할 수 있다. 또한, Server Side Rendering 방식으로 모든 정보가 담긴 HTML을 서버가 만들어서 전송해 준다.

 

💬 React는 라이브러리로 우리가 모든 것을 직접 생성하고 설정해주어야 했지만, Next에서는 이미 만들어져 있어 Next 규칙에 따라 코드만 작성하면 된다.

💬 Next.js는 React 문법을 사용해서 웹 페이지를 제작한다.

 

장점

◼ 모든 정보가 담긴 HTML을 응답해 주기 때문에, 봇이 데이터 수집이 가능해져 구글 검색 노출이 좋아진다.

◼ 첫 페이지 로딩 속도가 빨라진다.

◼ 원하는 곳에서는 Client Side Rendering을 사용할 수 있기 때문에 요즘 많이 사용하는 추세이다.

단점

◼ 서버에서 전체 앱을 미리 렌더링 하기 때문에, 로딩이 오래 걸리면 유저는 빈 화면을 봐야 한다.

◼ view 변경 시, 매번 서버에 요청하기 때문에, 서버 부하가 크다.

◼ 페이지를 요청할 때마다 새로고침된다.


Next.js 세팅

◼ node를 설치한다.

◼ yarn create next-app --typescript 명령어를 입력한다.

💬 yarn create next-app 명령어를 입력하면, 아래 과정에서 typescript을 사용할 건지 물어본다.

◼ 위 과정을 거치면 프로젝트가 생성된다.

 

💬 yarn run dev 실행 시 다음과 같은 오류가 발생하면, ..\..\..\ 폴더로 이동해서 yarn init으로 package.json을 만들면 해당 오류가 사라진다.

 

💬 Intellij에서 Prettier 사용하기

 

◼ setting -> plugins에서 Prettier 플러그인을 설치한다.

yarn add --dev --exact prettier  명령어를 사용해서 node_modules에 Prettier를 설치한다.

◼ setting -> Prettier에서  Prettier package를 방금 설치한 node_modules의 Prettier를 선택하고, On save 옵션을 활성화 및 All actions on save 버튼을 클릭해서 Run Prettier만 활성화한다.

◼ 이렇게 설정하면, 저장 시 자동으로 Prettier가 작동된다.


라우팅

export default function List() {
  return <h4>새로운 페이지 입니다.</h4>;
}

◼ Nex.js는 파일, 폴더를 만들면 자동으로 라우팅 한다. (자동으로 Route가 생성된다.)

◼ list 폴더를 만들고 page.tsx 파일을 만들어서 위 코드와 같이 넣어주면, localhost:3000/list로 접속하면 된다.

page.tsx 파일이 해당 uri로 접속할 때, 보여주는 파일이다.

 

◼ 세부 uri는 원하는 uri 경로 아래 새로운 폴더를 만들어 주면 된다.

◼ 위 파일 구조에서는 localhost:3000/list 접속 시, index.tsx 파일이, localhost:3000/list/somthing 접속 시, something.tsx 파일이 보인다.


Link

  <Nav>
    <Link href="/">홈</Link>
    <Link href="/list">List</Link>
  </Nav>

◼ 보통 링크를 만들고 싶으면, a 태그를 사용하지만 Next.js는 Link 컴포넌트를 사용하는 것이 좋다.

◼ 조금 더 부드럽게 페이지 전환이 가능하다.


Image 최적화 사용

import Image from "next/image";
import carImage from "/public/car.png";

<Image src={carImage} className="item-img" alt="car" />

◼ next에서 제공하는 Image 컴포넌트를 사용한다.

◼ lazy loading, 사이즈 최적화, layout shift(이미지 로딩이 늦개 되어, 레이아웃이 밀려나는 현상)를 방지해준다.

◼ 이미지를 import해서 넣어야 한다.

◼ 외부 이미지를 사용하려면 src="링크" width={} height={} 과 같이 width랑 height를 반드시 넣어줘야 한다. 혹은 fill="true" 옵션을 넣고, 부모 <div> 태그에서 width랑 heigh를 조절해도 된다.

 

const nextConfig = {
  experimental: {
    appDir: true,
  },
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "s3.amazonaws.com",
        port: "",
        pathname: "/my-bucket/...",
      },
    ],
  },
};

◼ 외부 이미지를 사용할 때, next.config.js에서 도메인 등록 및 이미지 경로를 등록해야 한다.


layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="navbar">
          <Link href="/">홈</Link>
          <Link href="/list">List</Link>
        </div>
        {children}
      </body>
    </html>
  );
}

◼ 화면에 출력하는 html 태그가 들어 있다.

◼ NavBar와 같이 모든 화면에 출력해야 할 component는 해당 파일에서 작성하고, {children} 위에 넣어주면 된다.

💬 {children}이 app 디렉터리 아래 모든 page.tsx 파일의 component가 들어갈 자리이다. 즉, 상위 폴더에 layout.js가 있으면 해당 파일 안의 {children}으로 page.tsx 파일의 component가 들어간다. 상위의 여러 개의 layout.tsx 파일이 존재하면 하위부터 계속해서 감싸진다.

 

export default function Layout({ children }) {
  return (
    <div>
      <div>공통 태그</div>
      {children}
    </div>
  )
}

◼ list 폴더에 layout.tsx 파일을 만들고 위와 같은 코드를 작성한다.

◼ list 폴더 아래 item 폴더를 만들고 page.tsx 파일에 component를 작성하고 localhost:3000/list/item에 접속하면, 공통 태그가 보인다.

◼ list 폴더 상위에 layout.tsx 파일이 존재하면 <div>공통 태그</div>와 {children}이 상위 layout.tsx 파일의 {chilren}에 들어간다. 


유틸성 Page

✔  loading.tsx

◼ 원하는 라우팅 폴더에 loading 파일을 둘 수 있다. (스트리밍 기능)

◼ 해당 uri를 처리하기 위해 콘텐츠가 로딩되는 동안 loading.tsx 컴포넌트를 출력해준다.

◼ loading.tsx 파일을 만들면, Next.js 내부적으로 Suspense를 사용하는 것과 같이 동작하게 해준다.

 

✔  error.tsx

◼ 원하는 라우팅 폴더에 error.tsx 파일을 두고, 에러가 발생했을 때 보여줄 component를 작성할 수 있다.

◼ 항상 client component로 만들어서 사용해야 한다.

◼ props로 error라 reset이 들어온다. error는 에러와 관련된 데이터, reset은 페이지 로드 관련 데이터

◼ page.tsx의 component 자리만 error.tsx component로 대체된다.

◼ 폴더 내에 error.tsx 파일이 없으면 상위 폴더의 error.tsx 파일을 찾는다.

💥 현재 폴더에도 error.tsx 파일이 있고, 상위 폴더에도 error.tsx 파일이 존재하면 두 개의 component를 합쳐서 보여준다.

💥 layout.tsx 파일에서 에러가 발생하면, 상위 폴더의 error.tsx 파일을 찾는다. 따라서, app 폴더 바로 아래의 layout.tsx 파일에서 에러가 발생할 때는, global-error.tsx 파일을 만들어서 처리해야 한다.

 

✔  not-found.tsx

◼ not-found.tsx 파일을 만들고, 원하는 부분에서 return notFound(); 를 사용하면 not-found의 component가 화면에 출력된다.


Next.js 컴포넌트

◼ Next.js에서는 server component, client component 총 2가지 컴포넌트가 있다.

◼ 아무 곳에서 만든 component는 server component이다.

◼ 파일 맨 위에 'use client'를 작성하고 그 파일에 만드는 component들은 client component이다.

 

💬 Server Component

Server Component는 서버에서 동작하기 때문에 브라우저 API를 사용할 수 없다. 따라서, 브라우저와 관련된 기능을 사용할 수 없다.

onClick, useState, useEffect, localStorage, sessionStorage 등의 기능도 사용할 수 없다. 단, cookie는 server component에서도 사용할 수 있다.

◼ 대신, 페이지 로드에 자바스크립트가 필요 없기 때문에, 로딩 속도가 빠르다. 

◼ 검색 엔진에 노출되기 쉽다.

 

💬 Client Component

html을 동적으로 바꿀 수 있는 기능을 넣을 수 있다.

◼ 페이지 로드에 자바스크립트 파일이 많이 필요하기 때문에, 로딩 속도가 느리다.

hydration 과정을 거쳐야 한다. (html을 유저에게 보낸 뒤, 자바스크립트로 html을 다시 읽고 분석하는 일)

 

👍 최대한 component를 분리해서, JavaScript 기능을 사용해야 하는 client component 범위를 줄이는 것이 좋다.

👍 client component로 사용하는 부분에서 DB 조회는 삼가하는 것이 좋다.

💥 "use client"를 사용한다고 모든 것을 다 브라우저에서 실행하지 않고, 서버 측에서 데이터를 받아와 props로 넘기는 등, 서버 내에서 처리가 가능한 부분은 처리한 HTML 파일을 보내준다.

💬  "use client" 속성

useEffect(() => {
  if (typeof window != "undefined") {
    localStorage.setItem("자료 이름", "값");
  }
}, [])

"use client"를 사용해서 client component로 만들어도 최대한 서버에서 가능한 작업은 서버에서 하려고 하기 때문에, localstorage와 같이 서버에서는 접근할 수 없는 코드는 useEffect와 window 타입을 통해 client에서만 사용할 수 있도록 코드를 작성해야 한다.

💬  deduplication

◼ DB에 같은 데이터를 요청하는 fecth() 함수가 여러 개 있을 경우, 1개로 압축해서 요청한다.


Dynamic Route

◼ /posts/detail/1...100 인 상황에서 폴더를 detail 아래 1부터 100까지 만들 수 없기 때문에 dynamic route를 사용한다.

◼ /1...100 인 부분의 폴더를 대괄호를 사용하고 원하는 키 값을 넣어준다. ([키값])

 

export default async function PostDetail(props) {
  console.log(props);
  return <div />;
}
{ params: { id: '645670488124b6192ee844dd' }, searchParams: {} }

◼ 위와 같이 props를 찍어보면, props.params에 키 값으로 설정한 변수명으로 파라미터가 넘어온다.

 

✔  useRouter

"use client";

import { useRouter } from "next/navigation";

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

◼ client component 안에서만 사용할 수 있다. page.js에 useRouter 내용이 적혀 함께 서버 응답으로 내려와서 client 측에서 렌더링 작업을 진행한다.

◼ next/navigation에서 import 할 수 있다.

◼ router.push("{이동할 경로}")로 페이지 이동이 가능하다.

◼ 단순 페이지 이동 말고, 다양한 기능을 제공하기 때문에 Link component 대신 사용한다.

   💬 router.back()을 통한 뒤로 가기, router.forward()를 통한 앞으로 가기, router.refresh()를 통한 soft 새로고침 기능(변한 부분만 새로고침 된다), router.prefetch('{uri 경로}')를 통한 페이지 미리 로드 등을 지원한다.

   💥 Link component에도 prefetch 기능은 되어 있다. 화면을 내리다 Link component를 만나는 순간 prefetch가 진행된다. prefetch={false}로 미리 로드 기능을 끌 수 있다.

 

"use client";

import { usePathname } from "next/navigation";

export default function DetailLink() {
  const pathname = usePathname();
  console.log(pathname); // uri 경로 출력
  return (<div />);
}

◼ usePathname()을 통해 현재 경로를 가져올 수 있다. ("/posts")

 

"use client";

import { usePathname } from "next/navigation";

export default function DetailLink() {
  const params = useParams();
  console.log(params); // queryString 출력 가능
  return (<div />);
}

◼ useParams()를 통해 queryString을 출력할 수 있다.

 


fetch

fetch(`api/post/delete?id=${post._id}`, {
  method: "DELETE",
  body: post._id.toString(),
})
  .then((res) => {
    if (res.status === 200) {
      return res.json();
    } else {
      console.log("실패!!");
    }
  })

◼ fetch()를 통해, ajax 요청을 할 수 있다.

◼ 아무 설정을 하지 않으면, 기본 GET 요청이다.

◼ 옵션으로 Method 및 Header, Body 값 등을 설정할 수 있다.

 


NextAuth.js

◼ Next.js에서 Authentication 기능을 제공한다.

◼ 소셜 로그인, JWT, Session 방법 등을 선택해서 사용할 수 있다.

◼ 단, 아아디/비번 로그인 시 JWT를 강제로 사용해야 한다. (개발자가 직접 아이디와 비밀번호를 취급하면 보안 이슈가 발생할 수 있기 때문)

◼ DB Adapter로 Session 방식을 사용할 수 있다.


Next.js에서 MongoDB 사용하기

✔ MongoDB 세팅

import { MongoClient } from "mongodb";

const databaseUri: string = "{DB URL}";

let connectDB;
if (process.env.NODE_ENV === "development") {
  if (!global._mongo) {
    global._mongo = new MongoClient(databaseUri).connect();
  }
  connectDB = global._mongo;
} else {
  connectDB = new MongoClient(databaseUri).connect();
}

export { connectDB };

◼ 터미널에 yarn add mongodb를 입력한다.

◼ src 폴더 아래 util 폴더를 만들고 database.ts 파일을 생성 후, 위 코드를 입력한다.

◼ 개발 중일 때, 파일을 저장하면 자동으로 새로고침이 되기 때문에 무의미한 connection이 쌓일 수 있다.

◼ 따라서, process.env.NODE_ENV가 "development"일 때는 global에 _mongo 변수를 만들고 해당 값이 비어있을 때만 커넥션을 넣어준다.

 

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

export default async function Post() {
  const client = await connectDB;
  const db = client.db("next-test");
  const data = await db.collection("post").find().toArray();
  console.log(data);

  return (
    <div>
    </div>
  );
}

◼ 사용하고자 하는 곳에서 connectDB를 가져와서 사용한다.

◼ db를 설정하고, 해당 DB의 Collection에 대해 find() 명령어를 통해 모든 데이터를 가져온다.

◼ DB 입출력 코드는 sever component 안에서만 작성하는 것이 좋다.

◼ client component에서 작성하면, client component는 유저 브라우저에서 실행되므로 민감한 코드는 작성하는 것은 좋지 않다.

await이 붙은 것을 export 해서 쓰면 이상해질 수 있어, 이를 방지하는 top-level await을 next.config.js 파일에 넣어줄 수 있지만, 아직 실환경에서 적용될만한 것은 아니라 await은 사용하는 곳에서 붙이는 것이 좋다.


Next.js에서 Server 기능 만들기

◼ pages 폴더 아래 api 폴더를 만들고 작성하는 방법과 app 폴더 아래 api 폴더를 만들고 작성하는 방법 2가지가 존재한다.

◼ 현재 시점에서는 pages 폴더 아래 api 폴더를 만들고 작성하는 방법이 권장된다.

 

const PostWriteForm = () => {
  return (
    <div className="post-write">
      <h4>글 작성</h4>
      <form action="/api/posts" method="POST">
        <input name="title" type="text" placeholder="글제목" />
        <input name="content" type="textarea" placeholder="글내용" />
        <button type="submit">등록</button>
      </form>
    </div>
  );
};

export default PostWriteForm;

◼ 위와 같이 코드를 작성한다.

◼ input 태그의 name 속성이 key 값으로 데이터가 요청된다.

◼ 전송 버튼을 클릭하면, /api/posts 경로로 POST 요청을 날린다.

 

import { CreatePostType } from "@/constants/types/post-types";
import { connectDB } from "@/util/database";

const { NEXT_PUBLIC_DATABASE_NAME: MONGO_DB_NAME } = process.env;

export default async function handler(req, res) {
  if (req.method == "POST") {
    const data: CreatePostType = req.body;
    if (data.title === "" || data.content === "") {
      return res.status(400).json("Invalid");
    }
    
    data.createdDate = new Date().toISOString().split("T")[0];
    const db = (await connectDB).db(MONGO_DB_NAME);
    await db.collection("post").insertOne(data);
    // return res.status(200).json(data);
    return res.status(200).redirect("/posts");
  }
}

◼ req.body를 통해, 요청을 보낸 Http Body 메시지를 볼 수 있다.

◼ 위 코드와 같이 사용자가 보낸 게시글을 MongoDB에 저장할 수 있다.


프로젝트 빌드

◼ yarn run build (npm run build) 명령어를 입력해서 코드를 html, js ,css 파일로 빌드한다.

◼ build가 완료되면 최상위에 .next 폴더가 생긴다.

 

◼ build 메시지를 보면 위와 같이 만들어진 페이지를 확인할 수 있다.

◼ 왼쪽에 기호로 표시 해주는데, 동그라미는 static rendering을 의미하고, 람다는 dynamic rendering을 의미한다.

◼ 가끔 DB에서 데이터를 가져와서 화면에 출력하는데 static rendering으로 표시되는 경우가 있다. 위의 /posts 역시 게시글을 DB에서 가져와서 화면에 보여지만 static rendering으로 되어 있다.

   ❗ yarn run start 명령어를 입력하여 실제 서버 환경에서 테스트를 진행해본다.

   ❗ 글을 작성 후, /posts 로 이동해도 새로운 글이 적용이 안된다. -> static rendering으로 동작해서, yarn run build 시 제작된 html만 계속 보내주기 때문이다.

  

💥 Dynamic Rendering 적용시키기

export const dynamic = "force-dynamic"; // dynamic rendering (1번)
export const dynamic = "force-static"; // static rendering (2번)

◼ dynamic 이라는 변수를 사용한다.

◼ posts 폴더의 page.tsx에서 1번을 작성하면 dynamic rendering으로 동작한다.

◼ 다시 yarn run build 명령어를 입력하면 /posts 페이지가 dynamic Rendering으로 동작하는 것을 확인할 수 있다.

 


Next.js Rendering 설명

◼ Next.js 에서 페이지를 만들면 default로 static rendering (yarn run build 과정에서 만들어진 html을 그대로 유저에게 전송한다.)을 한다. 미리 페이지 완성본을 만들어놨기 때문에 전송이 빠르다.

◼ dynamic rendering은 유저가 요청할 때마다 build 시 생성된 html 대신 서버에서 rendering 후 전송한다.

   ❗ featch('{경로}', {cache: "no-store"}), useSearchParams(), cookies(), headers(), [dynamic route] 등을 페이지에서 사용하면, 자동으로 dynamic rendering으로 동작한다.

   ❗ 캐싱 기능을 사용하면 서버와 DB 부담을 줄일 수 있다.

 

💥 캐싱 기능 사용하기

const result = await fetch("/api/post", {
    cache: "force-cache",
  });

◼ cache: "force-cache" 옵션을 주면, GET 요청 결과를 캐싱할 수 있다. 캐싱된 데이터는 하드 공간에 저장된다.

◼ 옵션에 넣지 않아도 default로 캐싱 기능을 제공한다. 따라서, cache를 사용하고 싶지 않을 때만 cache: "no-store" 옵션을 주면 된다.

◼ 따라서, 실시간 데이터가 중요하면 캐싱 기능을 끄는 것이 좋다.

 

export const revalidate = 60;

◼ revalidate 예약 변수를 사용해서 페이지 단위 캐싱이 가능하다.

◼ 해당 코드가 작성된 page 요청 시, 60초 단위로 캐싱 기능을 제공한다.

◼ 60초 마다 dynamic rendering이 이루어지고, 그 전에는 이미 저장된 html이 전송된다.


프로젝트 빌드

◼ Next.js는 Vercel을 이용해서 쉽게 배포가 가능하다. Next.js를 Vercel이 만들고 관리하기 때문이다.

◼ Github repo에 코드를 올릴 때마다, 그걸 자동으로 배포해주는 식으로 동작한다.

◼ 월 100GB 트래픽까지 무료 계정으로 해결할 수 있어, 작은 사이트를 운영할 때 편리하다.

◼ 하드 디스크를 사용할 수는 없다.


정리

Next에서 페이지의 Rendering 방식을 고민할 때 참고

 

💬 Client-Side Rendering

◼ 사용자와 상호 작용이 이루어져야 하는 부분은 "use client"를 사용해 client component로 동작하게 한다. 최대한 Sever component랑 분리해서, 해당 부분의 component만 client로 사용하는 것이 좋다.

◼ 실시간으로 html을 업데이트 해야 하는 경우

 

💬 Server-Side Rendering

◼ 만들어진 페이지(정적 페이지)가 아닌 서버에서 데이터를 가져와서 데이터가 업데이트가 되어야 할 때 사용한다.

 

💬 Static-Site Rendering

◼ 업데이트가 필요 없이 빌드 시 만들어진 HTML을 응답으로 내려줘도 될 때 사용한다.