Next.js를 공부하기에 앞서 Next.js가 무엇이고 사람들은 왜 Next.js를 사용하며, 어떠한 이점이 있는지 알아보자.
What is Next.js?
Next.js is a framework for building web applications.
With Next.js, you can build user interfaces using React components. Then, Next.js provides additional structure, features, and optimizations for your application.
Under the hood, Next.js also abstracts and automatically configures tooling for you, like bundling, compiling, and more. This allows you to focus on building your application instead of spending time setting up tooling.
Whether you're an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast web applications. - Next.js Docs
Next.js는 웹 애플리케이션을 구축하기 위한 프레임워크이다.
Next.js는 React 컴포넌트를 이용하여 UI를 구축할 수 있으며, 내부적으로 번들링, 컴파일 등과 같이 툴링을 추상화하고, 자동으로 구성하기에 이에 대한 시간을 절약하고 애플리케이션 구축에 집중할 수 있다. 개인 개발자이든 대규모 팀 소속이든 관계없이 interactive하고 dynamic한 빠른 웹 애플리케이션을 구축하는데에 도움이 될 수 있다.
What are the benefits of using Next.js?
1. Rendering
아마 알고 있겠지만 React는 Client에서 UI를 렌더링한다. 그러나 Next.js는 필요에 따라 클라이언트 측이나 서버 측에서 UI를 렌더링하도록 선택하는 유연한 렌더링 옵션을 제공한다.
What is Client-side rendering, Server-side rendering?
Client-side rendering(CSR)-유저가 서버에 HTML 문서와 JavaScript를 요청하면 브라우저가 컴포넌트의 렌더링으로 이어지는 JavaScript 코드를 다운하고 실행하여 웹 사이트를 표시한다.
Server-side rendering(SSR) - 클라이언트의 장치로 전송하기 전에 서버에서 웹 페이지를 렌더링하는 작업이 포함된다. 유저가 페이지를 요청하면 서버는 요청을 처리하고 서버 측에서 컴포넌트를 렌더링한 다음 완전히 렌더링된 HTML을 클라이언트의 브라우저로 다시 전송하여 즉시 표시할 수 있다.
(SEO 우선순위를 매긴 검색 결과 순위가 높아짐에 따라 유기적인 트래픽 증가로 사용자 경험 신뢰도 및 신뢰도가 향상되고 경쟁 우위 확보 가능)
추가적으로 알고 싶다면 잘 정리된 링크를 보는 것을 추천한다.
2. Routing
React의 경우 주로 React Router 패키지를 이용하여 라우팅을 구현한다. Next.js는 파일 기반 라우팅 시스템을 제공한다. 이것은 외부 패키지나 복잡한 라우팅 구성이 필요 없음을 의미한다.
3. Fullstack
Next.js는 API routes라는 기능을 제공하는데 이것은 API 요청을 처리하기 위한 서버리스 함수를 생성할 수 있다. 즉, Next.js에서 API를 구축할 수 있는 솔루션을 제공한다. Next.js의 serverless API는 기존 서버 없이 API endpont를 생성하는 방법이다. 이를 통해 서버 인프라를 관리하거나 트래픽이 증가할 때 서버의 확장성에 대해 걱정할 필요 없이 API를 구축하고 배포할 수 있다.
app 디렉토리 내의 특정 폴더에 route.js라는 파일을 만들기만 하면 API endpont를 만들 수 있다.
4. Automatic Code Splitting
Code Splitting은 큰 자바스크립트 번들을 필요에 따라 로드할 수 있는 작고 관리하기 쉬운 청크로 분해하는 기술이다. 이것은 웹사이트의 초기 로딩 시간을 줄이고 브라우징하는 동안 사용자의 경험을 최적화한다. React의 경우 필요할 때에 동적으로 컴포넌트를 가져오기 위해 lazy 함수를 사용할 수 있고 컴포넌트가 로딩 중일 때 fallback UI를 보여주기 위해 suspense 컴포넌트를 사용할 수 있다. 하지만 이 과정을 Next.js는 자동적으로 제공해준다는 것이다.
5. TypeScript 지원
정적 타입 검사, 향상된 도구 지원 및 코드 유지 관리성을 제공하는 TypeScript를 완벽하게 지원한다. Next.js는 TypeScript를 프로젝트에 쉽게 통합할 수 있도록 해주어 오류를 초기에 잡아내고 개발 프로세스를 향상시킬 수 있다.
마지막으로 It's still just React
설치
npx create-next-app@latest
디렉토리 구조 살펴보기
Next.js는 이제 기본적으로 TypeScript, ESLint 및 Tailwind CSS 구성과 함께 제공된다. src 폴더를 사용하도록 선택할 수도 있다. app 라우터가 안정화 되었기 때문에 더 이상 page 폴더를 제공하지 않는 것을 볼 수 있다.
app 디렉토리
1. layout
layout은 경로 간에 공유되는 UI다. root layout은 app 디렉토리의 최상위 레이아웃으로 <html>과 <body> 태그 및 전역적으로 공유되는 UI를 정의한다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
2. page.js
page는 경로에 고유한 UI다. params과 searchParams를 선택적 props로 받을 수 있다.
Next.js 12의 메인페이지의 경우 index.tsx 파일이고, 앱 라우터는 page.tsx다.
위 예시 구조가 두 버전 간의 큰 차별점이다.
위 라우팅 구조를 보면 알 수 있는 점은 각 페이지가 개인화된 라우팅 구조를 가질 수 있게 되었다.
app 폴더가 더러워지는 것을 방지하기 위해 괄호()로 감싸서 폴더를 만들면 실 URL 경로는 존재하지 않고 깔끔한 폴더구조를 만들어줄 수도 있다.
Server Component & Client Component
클라이언트 컴포넌트와 서버 컴포넌트를 함께 사용함으로써 서버 측 렌더링의 이점을 활용하면서도 React의 기능을 활용하여 동적이고 대화형 사용자 인터페이스를 구축할 수 있다.
const page = () => {
console.log("hello");
return <div>login</div>;
};
export default page;
page.tsx 파일에 console을 찍어보았다. 개발자 도구를 열면 로그가 찍혀야 하는데 로그가 찍히지 않았다. 컴포넌트가 정상적으로 렌더링 되었음에도 console.log('hello')는 어디로 갔을까? Next.js에서는 기본적으로 모두 Server Component이기에 CSR이 발생하지 않는다. 즉, 서버에서 실행된 후 클라이언트로 다시 전송되어 실제로 렌더링된다.
🟢 Server Component
Server Component를 사용하면 서버 측 렌더링을 활용하여 클라이언트 측 JavaScript 번들의 크기를 줄일 수 있다. 이로 인해 초기 페이지 로드 속도가 향상되며, 이는 SEO(검색 엔진 최적화) 및 사용자 경험을 개선하는 데 도움이 된다.
Next.js로 라우트가 로드될 때, 해당 페이지의 초기 HTML은 서버에서 생성되어 클라이언트에 전달된다. 이 초기 HTML은 사용자의 브라우저에서 점진적으로 향상되고, 이후 클라이언트는 Next.js와 React 클라이언트 측 런타임을 비동기적으로 로드하여 애플리케이션을 제어하고 상호 작용성을 추가한다. 이는 초기 페이지 로딩 속도를 향상시키고, 사용자가 빠르게 상호 작용할 수 있는 장점을 제공한다.
Server Component는 이벤트 리스너와 같은 클라이언트 측 api들을 사용할 수 없다.
Server Component를 사용할 때
- 데이터 패칭
- 백엔드 리소스 직접 접근: 클라이언트 측 JavaScript에서 백엔드 API에 직접 요청을 보내거나 데이터를 가져오는 대신 서버 측에서 백엔드 리소스에 접근하여 필요한 데이터를 가져온다.이를 통해 클라이언트와 서버 사이에 API 요청을 왕복하는 시간을 줄일 수 있으며, 초기 렌더링 시점에서 클라이언트 측에서 데이터를 로드하는 대신 서버에서 미리 데이터를 가져와 페이지를 완전히 준비할 수 있다.
- 서버에 민감한 정보를 서버에 보관(액세스 토큰, API 키 등)
- 큰 의존성(dependencies)을 서버에 유지하고 클라이언트 측 JavaScript의 크기를 줄임: 외부 패키지나 라이브러리를 서버 측에서 관리하고, 클라이언트측 JS 번들의 크기를 최소화하는 용도로 사용한다.
🟢 Client Component
Client Component를 사용하면 응용 프로그램에 클라이언트 측 상호 작용을 추가할 수 있다. 이는 서버에서 pre-rendering되고 클라이언트에서 hydration된다. 'use client' 키워드로 Client Component를 명시적으로 설정해서 사용할 수 있다.
Client Component를 사용할 때
- 상호작용 및 이벤트 리스너
- 상태나 생명 주기 효과(Effects) 사용
- 브라우저 API 사용: DOM 조작, 웹 애니메이션, 웹 저장소 (로컬 스토리지, 세션 스토리지 등), 웹소켓 통신, 브라우저 이벤트 등
- 상태(State), 효과(Effect) 혹은 브라우저 API에 의존하는 커스텀 훅 사용
- 리액트 클래스 컴포넌트 사용
Dynamic Route
React에서는 /post/:postId 와 같은 형태의 동적 세그먼트를 이용하여 Dynamic route를 구현했다.
(동적 세그먼트 - :postId처럼 경로에서 사용되는 변수)
Next.js에서 [folderName]처럼 폴더명에 대괄호([])를 묶음으로써 처리할 수 있다.
동적 세그먼트는 layout, page, route와 generateMetadata 함수 param이라는 prop으로 전달되는 동적 세그먼트 값을 활용할 수 있다.
예시를 보자. [slug]는 blog 게시글들을 위한 동적 세그먼트이다.
// app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}
Route | Example URL | params |
app/blog/[slug]/page.js | /blog/a | { slug: 'a' } |
app/blog/[slug]/page.js | /blog/b | { slug: 'b' } |
Catch-all Segments
Dynamic Segment는 [...fileName] 형식의 Catch-all Segment로 확장될 수 있다.
Route | Example URL | params |
app/blog/[...slug]/page.js | /blog/a | { slug: [ 'a' ] } |
app/blog/[...slug]/page.js | /blog/a/b | { slug: [ 'a', 'b' ] } |
app/blog/[...slug]/page.js | /blog/a/b/c | { slug: [ 'a', 'b', 'c' ] } |
Loading, Error Handling
서버 컴포넌트에서 로딩, 에러 처리하는 방법을 알아보자.
로딩은 간단하다. loading 특수 파일을 생성해서 의미있는 로딩 UI를 만들 수 있다. 콘텐츠가 로드되는 동안 로딩 상태를 표시할 수 있으며 렌더링이 완료되면 새 콘텐츠로 교체된다.
error 특수파일은 런타임 에러를 정상적으로 처리한다.
error 파일은 ErrorBoundary 컴포넌트를 자동으로 생성한다. 바운더리로 감싸진 경계 내에서 오류가 발생하면 Error 컴포넌트가 렌더된다.
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) ...
Error 컴포넌트는 error와 reset을 받을 수 있다. reset을 통해 재시도를 유도할 수 있다.
중첩된 컴포넌트 계층은 중첩된 경로에서 error.js 파일의 동작에 영향을 미친다.
- Errors는 가장 가까운 부모 에러 바운더리까지 버블링 된다.
export class AuthRequiredError extends Error {
constructor(message = "Auth is required to access this page.") {
super(message);
this.name = "AuthRequiredError";
}
}
// 사용하는 쪽 (page)
if (!session) throw new AuthRequiredError();
Data fetching
Next.js는 Data fetching을 어떻게 할지에 대해 3가지 선택지를 제공한다.
🟢 1. Server Side Rendering (SSR)
앞에서 봤듯이 SSR은 이미 알고 있어야하는 내용이다. 이는 "동적으로 서버에서 렌더링되는 데이터"를 의미하며, SSR을 사용하면 각 요청마다 새로운 렌더링 주기와 data fetch가 트리거 되어 컨텐트가 항상 최신 상태로 유지된다. 예시를 보자.
async function Page({ params }) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`,
{ cache: "no-store" }
);
const data = await res.json();
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
export default Page;
jsonplaceholder API에서 데이터를 가져오려는 비동기 함수 페이지이다. 위 페이지는 동적 페이지다. 왜냐하면 params를 통해 Id를 받아오기 때문이다. cache: "no-store"는 간단하게 캐시를 저장하지마! 라는 의미다. SSR은 동적인 컨텐츠를 표시하기 위해 매번 데이터를 다시 가져와서 렌더링한다.
🟢 2. Static Site Generation (SSG)
async function Page({ params }) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`
);
const data = await res.json();
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
export default Page;
SSG는 cache: "no-store"를 제거한다. 이렇게 하면 Next.js는 기본적으로 정적 사이트 생성을 사용한다. 처음에는 데이터를 fetch하고 다음부터는 이미 캐시된 데이터로 표시한다. 이 방법은 블로그 글, 문서, 마케팅 페이지와 같이 자주 변경되지 않는 컨텐츠에 적합하다.
🟢 3. Incremental Static Generation (ISR)
async function Page({ params }) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`,
{ next: { revalidate: 10 } }
);
const data = await res.json();
SSR과 SSG의 이점을 결합한 하이브리드 솔루션이다. 이를 사용하면 정적으로 미리 가져와야 하는 데이터를 지정할 수 있다. 이 데이터는 빌드 시간에 가져오며, 나머지 부분은 여전히 동적으로 처리된다. 이렇게 하면 전체 페이지를 다시 빌드하지 않고도 일부 페이지의 데이터를 업데이트할 수 있다.
위 코드처럼 재유효화(revalidation) 시간 간격을 정의하여, 간격 동안에 데이터 변경이 없는 경우 이전에 빌드된 데이터를 계속 사용하고, 변경이 발생하면 해당 페이지의 데이터가 업데이트 된다.
Next.js API Endpoints
앞에서 Next.js는 Fullstack을 지원하는 것은 프론트와 백 모두에서 앱을 실행한다는 의미다. 외부 서버 없이 HTTP 요청을 처리하고 백엔드 기능을 개발 할 수 있다는 것이다. 우선 express.js 서버를 사용하여 간단한 요청 경로를 얻으려면 무엇을 해야 하는지 예시를 보자.
const express = require("express");
const app = express();
app.get("/api/users", (req, res) => {
const users = [
{ id: 1, name: "John" },
{ id: 1, name: "Min" },
{ id: 1, name: "Ana" },
];
res.json(users);
});
app.listen(3000, () => {
console.log("Server is listening on port 3000");
});
위의 과정을 Next.js로 작성해보자. 각 폴더에 route.js를 만들어서 사용하는 것보다는 App 디렉토리에 api 폴더를 만들어서 백엔드 관련 로직과 API endpoint를 관리함으로써 프론트와 백을 분리하고 깔끔한 폴더 구조를 가져갈 수 있다.
export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}
Next.js가 지원하는 HTTP method이다.
export async function GET(request) {
const users = [
{ id: 1, name: "John" },
{ id: 1, name: "Min" },
{ id: 1, name: "Ana" },
];
return new Response(Json.stringify(users));
}
추가 Express 구성을 설정할 필요없이 편리한 endpoint를 만들 수 있다. 위 요청을 사용하려면 폴더 구조만 확인하면 된다. http://localhost:3000/api/users가 되겠다. 엄청 간단명료하다.
SEO & Metadata
Next.js는 최근에 새로운 metadata API를 소개했다. static과 dynamic 이 두가지 방법으로 metadata를 관리할 수 있다.
1. Static Metadata
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home',
}
// OUTPUT:
// <head>
// <title>Home</title>
// </head>
export default function Page() {}
static metadata를 정의하려면 layout 혹은 state page파일에서 metadata 객체를 내보내면 된다.
2. Dynamic Metadata
export async function generateMetadata({ params, searchParams}) {
const product = await getProduct(params.id);
return { title: product.title}
}
// Output:
// <head>
// <title>Unique Product</title>
// </head>
export default function Page() {}
이렇게 메타데이터를 관리하여 SEO를 엄청나게 높일 수 있다.