이정환님의 '한 입 크기로 잘라먹는 Next.js(v15)' 강의를 듣고 정리한 내용입니다.
한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런
이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지💡 Page Router란?Next.js
www.inflearn.com
1. Next.js가 무엇일까
Next.js = React의 활용판
Next.js가 인기가 많은 이유: Framework이므로
주도권이 개발자에게 있다: Library
React.js (Library)에서 페이지 라우팅 기능을 구현한다면
- React Router말고 Tanstack Router같은 다른거 써도 됨
주도권이 개발자에게 없다: Framework
프레임워크가 제공하는 기능을 이용하거나 허용하는 범위 내에서만 추가 도구 사용 가능
- Next.js에서 페이지 라우팅 기능을 구현해야한다면 Next.js에서 제공하는 App Router나 Page Router를 써야함
자유도가 낮다 → 거의 모든 기능을 제공
Next.js 사전 렌더링
사전 렌더링: 브라우저의 요청에 사전에 렌더링이 완료된 HTML을 응답하는 렌더링 방식
→ Client Side Rendering의 단점을 효율적으로 해결하는 기술
Client Side Rendering(CSR)이란?
- 클라이언트(브라우저)에서 직접 화면을 렌더링 하는 방식
장점: 초기 접속 이후의 페이지 이동이 빠름
단점: FCP(초기 접속 속도)가 느림
- 유저가 서버에 접속 요청
- 서버가 브라우저에 빈껍데기인 index.html을 줌
- 브라우저는 유저에게 빈 화면을 렌더링함
- 서버는 리액트 앱을 번들링해서 브라우저에 보내준다 (서비스에서 접근 가능한 모든 컴포넌트 코드가 존재 → 나중에 페이지를 이동해도 서버에게 새로운 페이지를 요청할 필요가 없음 → 초기 접속 이후의 페이지 이동이 빠른 이유)
요청 시작 후 화면이 렌더링되기까지 꽤 많은 시간이 소요
→ FCP(First Contentful Paint, 요청 시작 시점으로부터 컨텐츠가 화면에 처음 나타나는데 걸리는 시간)이 커짐: 이탈률 증가
사전 렌더링
- 서버가 서버 측에서 직접 리액트 앱을 실행을 시켜서 HTML로 변환
- 서버에서 렌더링이 완료된 HTML 파일을 그대로 브라우저에게 보내줌
- 브라우저는 HTML 파일을 그대로 화면에 렌더링
렌더링 단어 뜻 차이
서버에서 실행되는 렌더링: 자바스크립트 코드(React 컴포넌트)를 HTML로 변환하는 과정
화면에 렌더링: HTML 코드를 브라우저가 화면에 그려내는 작업
하지만 화면에 HTML 파일이 렌더링을 하였다고 해서 클릭, 페이지 이동 등 상호작용은 동작하지 않음
→ 상호작용은 HTML이 아니라 자바스크립트를 활용해야되기 때문
->브라우저는 서버로부터 받은 자바스크립트 코드를 실행해서 HTML 요소들과 상호작용을 연결 → 수화(hydration)
TTI(Time To Interactive): 요청 시작 후 상호작용까지 걸리는 시간
앞으로의 페이지 이동 등의 과정은 CSR처럼 작동함
Next.js에서 사전 렌더링을 도입함으로써
- React App의 단점 해소: 빠른 FCP 달성
- React App의 장점 승계: 빠른 페이지 이동
2. Page Router에 관하여
Page Router: pages 폴더 아래에 들어있는 폴더/파일들의 이름을 기준으로 페이지 라우팅을 제공
pages/index.js → ~/
pages/about.js → ~/about
pages/about/index.js → ~/about
동적 경로(Dynamic Routes): 가변적인 값을 포함하고 있는 경로
- pages/item/[id].js → ~/item/1, ~/item/2, …
npx: node package excutor
→ npmjs.com에 등록되어 있는 최신 버전의 노드 패키지를 다운로드 없이 바로 실행시키는 명령어
_app.tsx
- 리액트에서의 앱 컴포넌트와 같이 루트 컴포넌트의 역할
- 모든 페이지 역할을 하는 컴포넌트들의 부모 컴포넌트
- Component: 현재 페이지 역할을 할 컴포넌트를 받음
- pageProps: Component에 전달될 페이지의 props들을 모두 객체로 보관한 것
- 만약 모든 페이지에 공통적으로 적용해야할 내용이 있으면 _app.tsx에 넣으면 적용이 된다
_document.tsx
- 모든 페이지에 공통적으로 적용이 되어야하는 Next.js 앱의 html 코드를 설정하는 컴포넌트: index.html과 비슷한 역할
- meta tag, 폰트, 캐릭터 셋, 서드 파티 스크립트 설정 등 페이지 전체에 다 적용되는 HTML 태그를 관리하깅 ㅟ해 사용
nextconfig.mjs
- next app의 설정을 관리하는 파일
- reactStrictMode on/off 가능
page router에서 쿼리스트링을 활용할거면 useRouter를 사용하자 (next/router에서 import해야 함)
const { q } = router.query로 쿼리스트링의 값을 가져올 수 있다
url parameter를 활용할거면 파일명에 대괄호([])를 활용하자 (예시: book/[id].tsx)
book/[…id].tsx로 파일명을 바꾸면 book 경로 뒤에 여러 개의 id가 연달아 들어올 수 있다. (/book/123/413/45/123434…) : Catch All segment라고 부름
[[…id]].tsx로 파일명을 바꾸면 book으로 경로가 끝나도 오류가 발생하지 않는다: Optional Catch All Segment라고 부름
Page/404.tsx를 만들면 존재하지 않는 경로로 접속했을 때 404.tsx에 있는 내용이 뜨게 된다.
Page Router에서 Navigating
a 태그는 서버에게 새로운 페이지를 매번 다시 요청하므로 쓰지 않는게 좋다.
next app에서는 Link를 사용하자
<Link href={”/”}>index</Link>
Programmatic Navigation: 특정 버튼이 클릭이 되었거나, 특정 조건이 만족했을 경우에 어떠한 함수 내부에서 페이지를 이동시키는 방법
import { useRouter } from "next/router";
(...)
const router = useRouter();
const onClickButton = () => {
router.push("/test");
};
return (
<>
<div>
<button onClick={onClickButton}>/test 페이지로 이동></button>
</>
replace: 뒤로가기를 방지하며 페이지 이동
프리페칭(Prefetching)
현재 페이지에서 링크들을 통해서 이동할 수 있는 페이지들을 사전에 미리 다 불러와놓는 기능
→ 빠른 페이지 이동을 위해 제공
→ 프리페칭은 개발 모드에서 실행이 안되기 때문에 빌드하고 프로덕션 모드에서 확인하자
사전 렌더링 기능에서 HTML 페이지 응답 이후 후속으로 모든 자바스크립트 코드를 번들 파일 형태로 전달
→ 페이지 이동 요청이 있어도 서버에게 추가적인 리소스를 요청할 필요가 없다고 알고 있는데 프리페칭이 필요한 이유가 무엇일까?
->Next.js에서는 모든 리액트 컴포넌트들을 페이지별로 분리해서 저장을 해두기 때문
즉, 사전 렌더링의 과정에서 번들 파일을 전달할 때 모든 페이지에 필요한 자바스크립트 코드가 다 전달되는게 아니라, 현재 페이지에 필요한 번들 파일만 전달된다.
만약 모든 페이지의 번들파일을 전달하면 용량이 커져서 hydration이 늦어지기 때문
이 때 프리페칭이 작동하는데, 현재 페이지의 연결된 모든 페이지의 JS 번들을 불러오게 된다.
Link component로 명시된 경로가 아니면 프리페칭이 작동되지 않는다.
→ 만약 programmatic하게 경로를 이동한다면, 프리페칭을 할 수 있게 코드를 넣어주면 된다.
const router = useRouter();
const onClickButton = () => {
router.push("/test");
};
useEffect(() => {
router.prefetch("/test");
}, []);
만약에 특정 페이지를 프리페칭을 시키고 싶지 않다면 해당 링크 컴포넌트에 prefetch={false}를 넣어주면 된다.
API Routes
Next.js에서 API를 구축할 수 있게 해주는 기능
/pages/api 안의 파일이 API 응답을 정의하는 파일
import type { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const date = new Date();
res.json({ time: date.toLocaleString() });
}
type은 Next.js에 탑재된 NextApiRequest, NextApiResponse를 활용하자
위처럼 생성 후 localhost:3000/api/time로 이동 하면 코드의 작성된 것처럼 time: date.toLocalString을 확인할 수 있다.
스타일링
Next.js에서는 App component가 아닌 파일에서 import문을 통해 css 파일을 그대로 불러오는 것을 제한한다(페이지별로 css의 className이 겹치는 문제를 방지하기 위해)
→ Next.js에서는 CSS Module을 사용하자 (*.module.css)
import style from "./index.module.css";
(...)
<h1 className={style.h1}>Hello</h1>
페이지별 레이아웃 설정하기
특정 페이지에만 레이아웃을 설정하고 싶다면 페이지 파일에서 getLayout 메서드를 생성하고, App component에서 받아오면 된다
SearchableLayout을 적용한다고 했을 때
페이지 컴포넌트
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from "react";
export default function Home() {
return (
<>
<h1>인덱스</h1>
<h2>H2</h2>
</>
);
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
_app.tsx
import GlobalLayout from "@/components/global-layout";
import "@/styles/globals.css";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import { ReactNode } from "react";
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactNode) => ReactNode;
};
export default function App({
Component,
pageProps,
}: AppProps & {
Component: NextPageWithLayout;
}) {
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}
데이터 페칭
리액트에서는 데이터 페칭을 어떻게 했었지?
- 불러온 데이터를 보관할 State 생성
- 데이터 페칭 함수 생성
- 컴포넌트 마운트 시점에 fetchData 호출
- 데이터 로딩중일 때의 예외처리
단점: 초기 접속 요청부터 데이터 로딩까지 오랜 시간이 걸림
→ 이유: 컴포넌트 마운트가 된 이후에야 fetchData를 호출하기 때문
Next.js에서는 백엔드 서버에서 미리 렌더링을 할 때 (사전 렌더링 중) 필요한 데이터를 미리 불러오도록 설정할 수 있다.
→ React보다 훨씬 더 빠른 타이밍에 백엔드 서버로부터 데이터를 요청할 수 있다.
그런데 이 때 서버로부터 데이터를 받아오는 시간이 오래 걸려버릴 수도 있다.
→ 이를 위해 Next.js는 다양한 사전 렌더링 방식을 제공한다.
SSR(Server Side Rendering)
가장 기본적인 사전 렌더링 방식
요청이 들어올때마다 사전렌더링을 진행함
Next.js에서 페이지 파일 안에 getServerSideProps같은 메서드를 만들어서 내보내면 해당 페이지는 SSR로 동작한다.
getServerSideProps: 컴포넌트보다 먼저 실행되어서, 컴포넌트에 필요한 데이터를 불러오는 함수
- 객체를 반환해야하는데 그 객체 안에는 props 프로퍼티가 있어야한다.
- 서버에서 실행되기 때문에 브라우저에서 실행되는 함수를 실행하면 안된다(ex. window.location;)
- 사실, 페이지에서 실행되는 컴포넌트도 서버에서 한 번 실행되기 때문에 window.location;을 실행하면 에러가 발생한다.
- 만약 위 코드를 실행하고 싶으면 useEffect를 활용하면 된다.
- props의 타입은 InferGetServerSidePropsType<typeof getServerSideProps>를 활용하면 된다.
- 쿼리스트링, url 파라미터를 가져오려면 파라미터에 context: GetServerSidePropsContext를 넣어주자 (자동 추론)
- 컴포넌트에 전달할 때에도 타입은 InferGetServerSidePropsType<typeof getServerSideProps>로 자동추론 해주자
템플릿
export const getServerSideProps = async (
context: GetServerSidePropsContext
) => {
// const id = context.params!.id;
const q = context.query.q;
const books = await fetchBooks(q as string);
return {
props: {
books,
},
};
};
export default function Page({
books,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
api (/src/lib/*.ts)
반환이 서버에서 비동기적으로 진행되므로 Promise 타입을 넣어주자
export default async function fetchBooks(q?: string): Promise<BookData[]> {
let url = `http://localhost:(숫자)/book`;
if (q) {
url += `/search?q=${q}`;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.error(err);
return [];
}
}
SSG(정적 사이트 생성)
SSR에서 페이지를 접속할 때, 페이지가 백엔드 서버로부터 특정 데이터를 필요로 한다면 브라우저에 접속 요청이 있을 때마다 사전 렌더링 과정에서 새로운 데이터를 요청한다.
→ 페이지 내부의 데이터를 항상 최신 버전으로 유지 가능
→ 데이터의 응답 속도가 느려진다면 브라우저 로딩이 길어져서 사용자 경험이 안 좋아지는 단점
정적 사이트 생성(Static Site Generation)
- SSR의 단점을 해결하는 사전 렌더링 방식
- 빌드 타임에 페이지를 미리 사전렌더링
→ 백엔드 서버와의 소통이 서버가 가동되기 이전인 빌드타임에만 발생
→ 사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답 가능
→ 매번 똑같은 페이지만 응답하므로, 최신 데이터 반영은 어렵다
SSG를 사용하려면 getStatisProps를 활용하면 된다.
타입을 적용할 때 InferGetStaticPropsType, 제네릭으로 getStaticProps를 전달하면 된다.
주의: SSG가 잘 동작하는 지 확인하려면 빌드해서 프로덕션 모드에서 확인해야한다.
터미널에 나타나는 페이지별 빌드 결과에서 흰색 동그라미 기호가 있는 페이지가 SSG 방식으로 동작하는 페이지
(f는 주문형, SSR로 작동한다는 뜻. 빈 동그라미는 getStaticProps가 설정되지 않은 정적 페이지(기본값, 404나 테스트 페이지) )
getStaticProps에는 쿼리스트링이 없다.
→ 빌드 타임에 페이지를 미리 사전렌더링하므로 쿼리스트링에 어떤 값이 들어올지 알 수 없으므로
→ 클라이언트에서 진행해줘야한다(리액트 앱에서 했던 방식으로 데이터 페칭으로 진행해야)
SSG 동적 경로에도 적용하기
동적 경로를 가진 페이지는 빌드타임 때 이 페이지에 어떤 경로들이 존재할 수 있는지(어떤 URL 파라미터들이 존재할 수 있는지) 설정해야한다.
→ 미리 각 페이지들을 사전렌더링 해야하므로
→ getStaticPaths를 활용해야
URL 파라미터 값들은 무조건 문자열로 설정해야
Fallback 또한 설정해야한다.
fallback: false → 404 Not Found 반환
fallback: ‘blocking’ → 실시간으로 요청받은 페이지를 사전 렌더링해서 브라우저에게 반환 (like SSR)
한 번 만들어지면 next 서버에 저장된 것을 확인할 수 있다.
사전 렌더링을 하는동안 로딩 발생
fallback: true → Props 없는 페이지(getStaticProps로 부터 받은 데이터가 없는 페이지) 반환
나중에 Props를 계산해서 Props만 따로 반환
fallback 상태: 페이지 컴포넌트가 아직 서버로부터 데이터를 전달받지 못한 상태
fallback 상태일 때와 에러 상태일 때를 구분하고 싶을 때는 router.isFallback을 활용하자
const router = useRouter();
if (router.isFallback) return "로딩중입니다";
if (!book) return "문제가 발생했습니다 다시 시도하세요";
not-found를 출력하고 싶을 때에는 getStaticProps에서 notFound:true 문구를 주자.
export const getStaticProps = async(
context: GetStaticPropsContext
) => {
const book = await fetchOneBook(Number(id));
if (!book) {
return {
NotFound: true,
};
}
ISR(Incremental Static Regeneration, 증분 정적 재생성)
단순히 그냥 SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 기술
만약 유통기한이 60초라면, 60초 이후 요청이 들어오면 초기 버전의 페이지를 반환하고, 그 때 새로 페이지를 생성한다. 그리고 그 다음부터 새로 생성한 페이지를 반환
매우 빠른 속도로 응답 가능 (SSG 장점) + 최신 데이터 반영 가능 (SSR 장점)
적용방법
getStaticProps의 return문 안의 props 바깥에 revalidate라는 프로퍼티를 추가하고, 유통기한을 초 단위로 주면 된다.
export const getStaticProps = async () => {
(...)
return {
props: {
(...)
},
revalidate: 3, // 유통기한 3초
}
}
ISR의 주문형 재검증(On-Demand-ISR)
(시간이 아닌) 요청을 받을 때마다 페이지를 다시 생성하는 ISR
/api/routes에 revalidate.ts 생성
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// "/" 경로(홈페이지)를 on-demand로 revalidate
await res.revalidate("/");
// revalidate가 성공하면 JSON 응답을 전송
return res.json({ revalidate: true });
} catch (err) {
res.status(500).send("Revalidation Failed");
}
}
대부분의 케이스를 커버할 수 있는 강력한 사전 렌더링 방식
→ 오늘날 거의 대부분의 Next.js 구축된 웹서비스에서 사용됨
SEO 설정
브라우저 상단 탭 favicon 설정 → public/favicon.ico 변경
next/head의 Head 사용하기
return 문 안에 Head 태그에서 메타 태그 작성
return (
<>
<Head>
<title>페이지 제목</title>
// 썸네일 (/로 시작은 /public을 의미)
<meta property="og:image" content="/thumbnail.png />
// open graph title
<meta property="og:title" content="페이지 제목" />
// open graph description
<meta property="og:description" content="페이지에 대한 설명" />
</Head>
getStaticPaths의 static:true를 활용하면 SEO 설정이 안되어버리는 문제 발생
-> router.isFallback일 때에도 기본적인 메타태그들을 설정
if (router.isFallback) {
return (
<>
<Head>
(...)
</Head>
</>
)
}
배포하기
1. (sudo) npm install -g vercel로 vercel 패키지 설치
2. ‘vercel login’ 명령어를 입력하여 로그인 진행
3. ‘vercel’ 명령어를 입력하여 배포 진행(백엔드 서버도 배포 진행해야함)
4. vercel 홈페이지 dashboard에서 백엔드 프로젝트로 이동해서 도메인 주소 복사
5. src/lib/*.ts의 url들을 아까 복사한 도메인 주소로 변경
즉시 배포 명령어: ‘vercel —prod’
메타 태그(오픈그래프 태그)가 잘 적용되었는지 확인하려면 주소를 복사해서 카카오톡에 붙여넣기를 해보자
결론
Page Router의 장점
- 파일 시스템 기반의 간편한 페이지 라우팅 제공
- 다양한 방식의 사전 렌더링 제공
Page Router의 단점
- 페이지별 레이아웃 설정이 번거롭다
- 데이터 페칭이 페이지 컴포넌트에 집중된다
- 불필요한 컴포넌트들도 JS Bundle에 포함된다(상호작용이 필요없는 컴포넌트들도 hydration)
다음 챕터부터 Page Router의 단점들을 보완한 App Router에 대해서 배우게 된다. 꾸준히 학습해보자!
'Frontend > Next.js' 카테고리의 다른 글
[Next.js 강의 정리] 서버 액션, Parallel Route, 최적화에 관하여 (4) | 2025.06.08 |
---|---|
[Next.js 강의 정리] App Router에 관하여 (0) | 2025.06.06 |
[Next.js] Next.js(SSR 환경)에서 css 애니메이션이 작동 안하는 이유 (1) | 2024.07.12 |