인프런 "한 입 크기로 잘라먹는 Next.js" 수강
기존 Page Router에서는 서버에서 데이터를 불러오기 위해 getServerSideProps, getStaticProps, getStaticPaths 와 같은 전용 함수를 사용했다.

이 함수들이 데이터를 불러오고, 그 결과를 props 형태로 컴포넌트에 전달했다.
즉, 모든 데이터가 "페이지 단위"로만 전달될 수 있었음.

props 전달 혹은 Context API를 사용해야 했음
서버 컴포넌트의 등장으로 문제 해결!
App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 동작한다. 즉, 브라우저에서 실행되지 않기 때문에 async와 같은 키워드를 붙여도 문제없다.
“데이터는 필요한 곳에서 직접 불러와라” - Next.js 공식문서 (Fetching data where it’s needed)
덕분에 각 컴포넌트가 스스로 필요한 데이터를 fetch해서 렌더링할 수 있게 되었다.

import BookItem from '@/components/book-item';
import books from '@/mock/books.json';
import style from './page.module.css';
export default function Home() {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
</div>
);
}
데이터 페칭을 할 수 있도록 async 키워드로 비동기 함수를 만든 후 아래와 같이 작성했다.
import BookItem from '@/components/book-item';
import { BookData } from '@/types';
import style from './page.module.css';
async function AllBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/books`);
if (!response.ok) {
return <div>오류가 발생했습니다...</div>;
}
const allBooks: BookData[] = await response.json();
return (
<div>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
async function RecoBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/books/random`);
if (!response.ok) {
return <div>오류가 발생했습니다...</div>;
}
const randomBooks: BookData[] = await response.json();
return (
<div>
{randomBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
export default function Home() {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
<RecoBooks />
</section>
<section>
<h3>등록된 모든 도서</h3>
<AllBooks />
</section>
</div>
);
}
Next.js는 fetch() 결과를 서버 측에서 자동으로 캐싱할 수 있도록 만들었다.
이 덕분에 동일한 요청이 반복될 때, 데이터를 어떤 방식으로 캐싱할지 지정할 수 있다.
등의 기능을 수행하며, 사용 방법은 아래와 같다.
const response = await fetch(`/api`, { cache: "force-cache" });
| 옵션 | 설명 |
|---|---|
cache: "no-store" | 캐싱하지 않음 (기본값). 항상 새로운 요청을 보냄 |
cache: "force-cache" | 요청 결과를 무조건 캐싱. 이후 동일 요청은 캐시에서 가져옴 |
next: { revalidate: 3 } | 3초마다 캐시를 갱신 (ISR 방식과 유사) |
next: { tags: ['a'] } | On-Demand Revalidation — 특정 태그를 지정해 수동으로 최신화 |
{ cache: "no-store" }
새로고침 후 콘솔에서 확인해보면
cache skip으로 출력됨을 확인할 수 있다.
{ cache: "force-cache" }
새로고침 후. 콘솔로 확인해보면
cache hit즉, 캐싱된 데이터를 찾는다는 것을 알 수 있다.
{ next: { revalidate: 3 } }실행 후, 지정한 시간을 주기로 새로고침 되는 것을 확인할 수 있다.
{ next: { tags: ['a'] } }추가로 데이터 페칭이 발생할 때마다 로그를 출력하는 설정은 아래와 같다.
import type { NextConfig } from 'next';
>
const nextConfig: NextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};
>
export default nextConfig;
“요청을 기억한다.” — 동일한 요청을 자동으로 한 번만 수행하도록 최적화하는 기능이다.
서버 컴포넌트 구조상, 여러 컴포넌트가 같은 데이터를 요청할 수 있다. (ex. 각 서버 컴포넌트들이 자신들이 필요한 데이터를 알아서 불러오기 때문!)
이때 Next.js는 동일한 요청을 하나로 묶어 처리하고, 결과를 공유한다.

| 구분 | 데이터 캐시 (Data Cache) | 요청 메모이제이션 (Request Memoization) |
|---|---|---|
| 목적 | 데이터를 오래 보관 | 동일한 요청을 중복 방지 |
| 지속 기간 | 서버 인스턴스가 유지되는 동안 | 렌더링 1회 주기 동안만 |
| 활용 예 | API 결과 영구 저장 | 한 페이지 내 중복 요청 제거 |