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을 응답으로 내려줘도 될 때 사용한다.