페이지 라우터는 Pages 폴더의 구조를 기반으로 페이지 라우팅을 제공한다.
일단 Next.js 프로젝트부터 생성해보자
npx create-next-app14 section02
생성 후 파일 구조를 살펴보면,
App Component는 모든 컴포넌트의 부모 컴포넌트. -> 헤더처럼 모든 페이지에 필요한 경우 앱 컴포넌트에 넣어주면 된다.
strict mode는 false로 설정해주자. (엄격한 체킹)
페이지 라우터는 Pages 폴더의 구조를 기반으로 자동으로 라우팅을 생성함.
파일 기반 라우팅 -> 파일명이 곧 라우트 경로가 됨.
일단, 우리는 앱라우터가 아닌 페이지라우터를 사용하기때문에 next/router에서 import되는 useRouter를 사용하자.
import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
console.log(router);
const { q } = router.query;
return <h1>Search {q}</h1>;
}
이렇게 작성하면 url에서 ?q=홍길동 이렇게 받아올 수 있다.
만약 계속 변하는 url 파라미터를 받아서 동적 라우팅으로 페이지를 생성해야 한다면
다음과 같이 진행할 수 있다.

import { useRouter } from "next/router";
export default function Page() {
const router = useRouter();
console.log(router);
const { id } = router.query;
return <h1>Book {id}</h1>;
}
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import Link from "next/link";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<header>
<Link href={"/"}>index</Link>
<Link href={"/search"}>search</Link>
<Link href={"/book/1"}>book1</Link>
</header>
<Component {...pageProps} />;
</>
);
}
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import Link from "next/link";
import { useRouter } from "next/router";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push("/test");
};
return (
<>
<header>
<Link href={"/"}>index</Link>
<Link href={"/search"}>search</Link>
<Link href={"/book/1"}>book1</Link>
<div>
<button onClick={onClickButton}>/test 페이지로 이동</button>
</div>
</header>
<Component {...pageProps} />;
</>
);
}


앞서 보았던 예시에서
Link를 이용한 방식은 프리패칭이 진행되는데, Router로 이동한건 그렇지 않다.
하지만, Router로 구현한것도 프리패칭으로 하고싶으면
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push("/test");
};
useEffect(() => {
router.prefetch("/test");
});
return (
<>
<header>
<Link href={"/"} prefetch={false}>
index
</Link>
<Link href={"/search"}>search</Link>
<Link href={"/book/1"}>book1</Link>
<div>
<button onClick={onClickButton}>/test 페이지로 이동</button>
</div>
</header>
<Component {...pageProps} />;
</>
);
}
이렇게 작성할 수 있다.
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(
req: NextApiRequest,
res: NextApiResponse) {
const date = new Date();
res.json({ time: date.toLocaleString() });
}
import style from ""
<h1 className={style.h1}>인덱스</h1>
이렇게 하여 클래스 이름이 겹치는 문제를 차단한다.
Index.tsx
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from "react";
export default function Home() {
return <h1>인덱스</h1>;
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
이렇게 getLayout을 이용하여 레이아웃을 지정할 수 있다.
App.tsx
import GlobalLayout from "@/components/global-layout";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
const getLayout = Component.getLayout;
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}
아래는 SearchableLayout
SerachablaeLayout.tsx
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
export default function SearchableLayout({
children,
}: {
children: ReactNode;
}) {
const [search, setSearch] = useState("");
const router = useRouter();
const q = router.query.q as string;
useEffect(() => {
setSearch(q || "");
}, [q]);
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const onSubmit = () => {
if (!search || q === search) return;
router.push(`/search?q=${search}`);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSubmit();
}
};
return (
<div>
<div>
<input
value={search}
onChange={onChangeSearch}
onKeyDown={onKeyDown}
placeholder="검색어를 입력하세요 ..."
/>
<button onClick={onSubmit}>검색</button>
</div>
{children}
</div>
);
}
Index.module.css
.searchbar_container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.searchbar_container > input {
flex: 1;
padding: 15px;
border-radius: 5px;
border: 1px solid rgb(220, 220, 220);
}
.searchbar_container > button {
width: 80px;
border-radius: 5px;
border: none;
background-color: rgb(37, 147, 255);
color: white;
cursor: pointer;
}
사전 렌더링(Pre-rendering):
- 목적: 초기 페이지 로드 성능 개선
- 동작: 서버에서 HTML을 미리 생성
- 시점: 빌드 타임(SSG) 또는 요청 시점(SSR)
- 대상: 페이지의 초기 HTML 컨텐츠
- 결과물: 완전한 HTML 문서
// 사전 렌더링: 서버에서 HTML 생성 export async function getStaticProps() { const data = await fetchData(); return { props: { data } }; } - 사전 렌더링은 "첫 방문"을 빠르게 하는 것이 목적
프리패칭(Prefetching):
- 목적: 페이지 전환 성능 개선
- 동작: 다음 페이지의 JavaScript 코드를 미리 다운로드
- 시점: 현재 페이지 로드 후 백그라운드에서
- 대상: 링크된 페이지의 JavaScript 번들
- 결과물: 캐시된 JavaScript 코드
// 프리패칭: 클라이언트에서 다음 페이지 준비 <Link href="/about" prefetch> About </Link> - 프리패칭은 "페이지 이동"을 빠르게 하는 것이 목적
React의 일반적인 렌더링: 클라이언트 사이드에서 모든 렌더링 수행
React
1) 불러온 데이터들을 보관할 State 생성
2) 데이터 페칭 함수 생성
3) 컴포넌트 마운트 시점에 fetchData 호출
4) 데이터 로딩중일 때의 예외처리
문제점: 초기 로딩이 느리고, 데이터 페칭 후 추가 대기 시간 발생
Next.js 해결책: 사전 렌더링을 통해 초기 HTML을 미리 생성
하지만 사전 렌더링이 오래 걸릴 것 같은 경우가 있다. 이런 경우에는 빌드타임에 미리 사전 렌더링할 수 있도록 여러가지 기능들이 있음.
getServerSideProps 함수를 사용하여 구현export const getServerSideProps = async () => {
const data = await fetchData();
return {
props: { data }
};
};
이때, return은 객체여야 한다 ! (그냥 받아들이기)
이때, 이 getServerSideProps() 함수는 딱 한번만 실행됨 (오직 서버 측에서만 실행이 됨)
그렇기에, 로그를 저 함수내에서 찍으면 웹에는 로그가 안뜨고 그냥 터미널에서만 뜰거임.

Home 컴포넌트는 서버에서 한 번(사전렌더링), 브라우저(Hydration)에서 한 번 실행이 되어 총 두 번 실행이 됨!
server에서 이용하려하면 server는 undefined이기때문에 만약 브라우저에서만 실행되는 코드를 쓰고 싶으면? -> useEffect()를 사용하면 컴포넌트가 마운트된 이후에 실행이 된다.


export const getServerSideProps = async () => {
// const allBooks = await fetchBooks();
// const recoBooks = await fetchRandomBooks();
const [allBooks, recoBooks] = await Promise.all([fetchBooks,fetchRandomBooks])
// Promise.all을 통해 배열 안에 있는 함수들을 순차적이 아닌 병렬적으로 한번에 실행. -> 더 빠름
return {
props: {
allBooks,
recoBooks,
},
};
};
import { BookData } from "@/types";
export default async function fetchBooks(): Promise<BookData[]> {
const url = `http://localhost:12345/book`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.error(err);
return [];
}
}
getStaticProps 함수를 사용getStaticPaths 필요장점 : 빠르게 응답 가능 / 단점 : 빌드 타임 이후에서 페이지를 새로 생성하지 않아 최신 데이터를 반영하기 어렵다. -> 데이터가 자주 업데이트 되지 않는 정적인 페이지에 적합 (매번 똑같은 페이지만 응답)
import BookItem from "@/components/book-item";
import SearchableLayout from "@/components/searchable-layout";
import fetchBooks from "@/lib/fetch-books";
import fetchRandomBooks from "@/lib/fetch-random-books";
import { InferGetStaticPropsType } from "next";
import { ReactNode } from "react";
import style from "./index.module.css";
export const getStaticProps = async () => {
// const allBooks = await fetchBooks();
// const recoBooks = await fetchRandomBooks();
console.log("인덱스 페이지");
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks(),
]);
// Promise.all을 통해 배열 안에 있는 함수들을 순차적이 아닌 병렬적으로 한번에 실행. -> 더 빠름
return {
props: {
allBooks,
recoBooks,
},
};
};
export default function Home({
allBooks,
recoBooks,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
{recoBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
</div>
);
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
import BookItem from "@/components/book-item";
import SearchableLayout from "@/components/searchable-layout";
import fetchBooks from "@/lib/fetch-books";
import { BookData } from "@/types";
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
// export const getStaticProps = async (context: GetStaticPropsContext) => {
// //context에는 현재 브라우저에서 받은 모든 요청에 대한 정보가 다 있음.
// const q = context.query.q;
// const books = await fetchBooks(q as string);
// return {
// props: { books },
// };
// };
export default function Page() {
const [books, setBooks] = useState<BookData[]>([]);
const router = useRouter();
const q = router.query.q;
const fetchSearchResult = async () => {
const data = await fetchBooks(q as string);
setBooks(data);
};
useEffect(() => {
if (q) {
fetchSearchResult();
}
});
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
dynamic한 경로를 갖는 페이지는 이렇게 getStaticPath라는 함수가 필요함!!
이때, fallback에 대해 알아보자.
import fetchOneBook from "@/lib/fetch-one-book";
import { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import style from "./[id].module.css";
export const getStaticProps = async (context: GetStaticPropsContext) => {
const id = context.params!.id;
const book = await fetchOneBook(Number(id));
return {
props: { book },
};
};
export const getStaticPaths = () => {
return {
paths: [
{ params: { id: "1" } },
{ params: { id: "2" } },
{ params: { id: "3" } },
],
fallback:
//예외상황에 대한 대비
false, //not found
};
};
export default function Page({
book,
}: InferGetStaticPropsType<typeof getStaticProps>) {
if (!book) return "문제 발생";
const {
// id,
title,
subTitle,
description,
author,
publisher,
coverImgUrl,
} = book;
return (
<div className={style.container}>
<div
className={style.cover_img_container}
style={{ backgroundImage: `url('${coverImgUrl}')` }}
>
<img src={coverImgUrl} />
</div>
<div className={style.title}>{title}</div>
<div className={style.subTitle}>{subTitle}</div>
<div className={style.author}>
{author} | {publisher}
</div>
<div className={style.description}>{description}</div>
</div>
);
}
revalidate 옵션으로 시간 설정return {
props: {
allBoocks,
recoBooks,
},
revalidate:3,
};
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
await res.revalidate("/");
return res.json({ revalidate: true });
} catch (err) {
console.log(err);
res.status(500).send("Revalidation Failed");
}
}
http://localhost:3000/api/revalidate
이렇게 API routes를 통해서 가능.
next/head 컴포넌트 사용<Head>
<title>페이지 제목</title>
<meta property="og:title" content="제목" />
<meta property="og:description" content="설명" />
</Head>
<Head>
<title>한입북스</title>
<meta property="og:image" content="/thumbnail.png" />
<meta property="og:title" content="한입북스" />
<meta
property="og:description"
content="한입북스에 등록된 도서들을 만나보세요"
/>
</Head>
sudo npm install -g vercel
vercel login
vercel
vercel --prod
장점:
단점: