
서버 컴포넌트에서의 데이터 로딩
먼저 클라이언트 컴포넌트에서의 데이터를 불러오는 방식은 다음과 같습니다.
컴포넌트가 마운트되면, useEffect로 사이드이펙트를 트리거하여 fetch API를 사용해 state를 변경해주었습니다.

이 방식은 브라우저가 Javascript를 다 다운로드하고 실행한 뒤에야 데이터 요청을 시작하므로
필연적으로 네트워크 워터풀 현상이 발생하고, 초기 로딩 속도(FCP)가 느려질 수 밖에 없었습니다.
하지만 서버 컴포넌트를 통한 방식은 다릅니다. 브라우저가 아닌 서버에서 먼저 실행됩니다.
따라서 복잡한 React Hooks를 전부 사용하지 않고, 컴포넌트 자체에 async 기호 하나만 붙이면 됩니다.
그러면 컴포넌트 내부에서 await 키워드를 사용해 동기식 흐름으로 데이터를 가져옵니다.
순서대로 작성하자면, 먼저 가져올 데이터의 타입 정의를 합니다. (모든 데이터 정의가 아니라 필요한 데이터만 정의)
interface Post {
id: number;
title: string;
body: string;
}그리고, 컴포넌트 함수 앞에 async 키워드를 붙이고, await fetch()를 통해 데이터를 가져옵니다.
여기서 명심해야 할 점은 이 코드가 브라우저가 아닌 서버 간의 통신이라는 점입니다.
따라서, 브라우저의 네트워크 디버깅으로도 보이지 않고, 로직도 노출되지 않아 보안성에서 안전합니다.
그리고, 자바스크립트에도 영향을 받지 않아 사용되는 번들 사이즈는 0입니다.
export default async function ProductPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await res.json()
}하지만 이 방식의 단점도 있는데 더보기나 정렬 등의 상호작용을 위해서는 client 방식을 사용해야 합니다.
혹은 Next.js의 고급 기술인 Server Actions를 사용할 수도 있습니다.
따라서 초기 데이터를 빠르게 보여주는 것은 서버 컴포넌트가, 이후 작용은 클라이언트 컴포넌트가 담당할 것입니다.
캐싱 전략(Caching Strategy)
코드를 실행해보면 데이터를 새로고침할 때 마다 API 호출 횟수가 계속 무한대로 증가하는데
만약 이 데이터가 고정된 데이터라면, 서버는 똑같은 데이터를 보여주기 위해 여러 번 API를 호출하고 계산해야 합니다.
이는 엄청난 서버 자원 낭비이자, 속도 저하의 원인이 됩니다.
그 캐싱 전략 중 하나로, 성능 최적화의 핵심인 불필요한 동적 랜더링에 대해 이해하고 이걸 정적 랜더링으로 바꾸어 봅니다.
그 예시로 랜더링 방식을 눈으로 확인하기 위해 매초 변하는 시간 데이터를 사용할 것입니다.
다음과 같이 page.tsx 코드를 작성해봅시다.
export default async function ProductsPage() {
const res = await fetch('https://timeapi.io/api/Time/current/zone?timeZone=Asia/Seoul')
const timeData = await res.json()
return (
<div>Current Time : {timeData.dateTime}</div>
)
}먼저 API를 호출하는 부분에서 fetch 함수의 2번째 인자로 옵션이 비어있는 점에 주목하세요.
Next.js 15+ 이후부터는 옵션이 없을 경우 기본적으로 데이터를 캐싱 처리하지 않고 매번 새로 가져옵니다.
여기서 브라우저를 새로 고침할 때마다 시간이 변경되는데 이것이 바로 동적 랜더링입니다.
즉, 사용자가 요청을 보낼 때마다 서버에서 HTML을 새로 생성하여 전달하는 것입니다.
이것은 실시간성이 생명인 데이터 등에 적합할 것입니다.
하지만, 변하지 않는 고정된 데이터까지 매번 새로 가져오는 것은 비효율적인 행위입니다.
이 때 사용하는 것이 정적 랜더링입니다.
적용 방법은 fetch 함수의 2번째 인자로 { cache: 'force-cache' } 를 붙여주면 됩니다.
ISR & Revalidation
위에서 정적 랜더링과 동적 랜더링을 배웠지만, 2개 중 하나를 선택할 경우 딜레마에 빠질 상황들이 많습니다.
정적 랜더링을 쓰자니 갱신이 안되는 문제가 생기고, 동적 랜더링을 쓰자나 서버 과부하가 걱정됩니다.
그래서 평소에는 캐시된 빠른 페이지를 보여주다가, 일정 시간마다 한번씩 최신 데이터로 업데이트되길 원합니다.
그래서 나온 기술이 ISR과 Revalidation입니다.
ISR은 Incremental Static Regeneration의 약자로, 전체 사이트를 다시 빌드하지 않는다는 의미입니다.
필요한 페이지만 조금씩 다시 생성하는 점진적 정적 재생성 기술입니다.
과거에는 글자 하나만 고쳐도, 사이트 전체를 새로 빌드해야 했지만, 이제는 수정된 페이지만 갈아끼울 수 있게 됩니다.
Revalidation은 유효기간이 지난 낡은 데이터를 싱싱한 데이터로 재검증하는 과정을 의미합니다.
이 두 개념을 실습으로 알아보겠습니다.
3초 동안은 캐시된 데이터를 보여주고, 3초가 지나면 새로운 시간으로 갱신하는 예시입니다.
export default async function ProductsPage() {
const res = await fetch('https://timeapi.io/api/Time/current/zone?timeZone=Asia/Seoul', {
next: { revalidate: 3 }
})
const timeData = await res.json()
return (
...
)
}두번째 인자로, 객체 형태의 옵션을 전달할 것입니다. 객체로 { next: { revalidate: 3 } } 를 전달합니다.
이제 이 방식의 작동 원리를 살펴볼 것입니다. 이를 **SWR(Stale While Revalidate)**라고 부릅니다.
오래된 데이터를 먼저 보여주는 동안 새로운 데이터를 재검증할 준비를 하는 것이라는 의미입니다.
구체적인 흐름 시점을 살펴봅니다.
먼저 최초 접근 시점에는 서버에서 API를 호출해 데이터를 가져오느라, 잠깐의 시간이 필요합니다.
그러나 그 이후 방문해서는 API 호출을 하지 않고, 캐시된 데이터를 가져와 즉시 응답합니다.

그러다 유효 기간인 3초가 지난 직후에 누군가 접속하면 Next.js는 사용자를 기다리게 하지 않기 위해
일단 저장되어 있는 캐시된 데이터를 먼저 보여주고, background process에서는 몰래 revalidate 중입니다.

그리고 그 이후에 접속한 사람은 다시 새로운 캐시 데이터를 응답받게 됩니다.

그러나 이러한 ISR은 개발 모드인 dev 모드에선 확인할 수 없으므로, 실제 배포 환경과 유사하게 테스트 해야 합니다.
먼저 npm run build로 프로덕션 빌드를 한 후, npm start로 서버를 실행하여 확인할 수 있습니다.
그렇다면, fetch 함수가 아니라 axios나 prisma를 사용하여 통신한다면 어떠할까요?
이런 경우, Route Segment Config를 이용합니다.

Page 컴포넌트 최상단의 import 구문 바로 아래에 export const revalidate = 60을 적어주면 됩니다.
그러면 이 페이지에서의 API는 모두 60초 주기로 재검증(revalidate) 옵션이 적용됩니다.
지금까지 상황에 따른 캐싱 전략 비법을 살펴보면
동적 랜더링은 검색이나 장바구니 그리고 나의 데이터처럼 항상 최신 데이터가 필요할 경우 사용합니다.
정적 랜더링은 회사 소개나 블로그 글처럼 한번 올리면 거의 변하지 않는 것에 사용합니다.
ISR은 쇼핑몰의 상품 목록이나 뉴스 메인처럼 데이터가 바뀌긴 하나 성능과 최신성 사이의 균형이 필요한 경우 사용합니다.
generateStaticParams
사용자가 상품을 클릭했을 때 이동하는 상세 페이지에서 성능상의 딜레마가 발생합니다.
동적 라우팅으로 만든 페이지는 사용자가 접속하는 그 순간에 id를 찾고 그에 따라 데이터를 가져와야 하므로
Next.js는 이 컴포넌트를 동적 랜더링으로 처리합니다.
이를 비유하면, 매번 신선한 데이터를 응답받는 것으로 주문을 받고 조리를 시작하는 비효율적인 방식으로 비유됩니다.

그렇다면, 우리 쇼핑몰에 등록된 상품이 100개라고 가정할 때, 이 100개의 상세 페이지를 미리 만들수는 없을까요?
Next.js는 강력한 정적 빌드 함수인 generateStaticParams를 제공합니다.
이 함수의 의미는 Next.js 빌드 시스템에 정적인 페이지를 만들기 위한 파라미터를 미리 만드는 행위를 의미합니다.
그러면, Next.js는 우리가 명령한 대로 빌드 타임에 100개의 HTML 파일을 뚝딱 만들어냅니다.
실습으로, 리스트 페이지에 해당하는 page.tsx에서 generateStaticParams 라는 이름의 함수를 하나 더 export 합니다.
예시로, 1번부터 10번까지 상세 페이지를 미리 만들어보겠습니다.
export async function generateStaticParams() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts')
const res = await posts.json()
return res.slice(0, 10).map((post: { id: string }) => ({
id: String(post.id)
})
}이 함수는 우리가 사이트를 배포하기 위해 빌드하는 그 순간에 가장 먼저 실행됩니다.
함수 로직을 보면 전체 게시글 목록을 가져온 뒤, map 함수를 사용하여 id를 속성으로 가지는 객체의 배열을 반환합니다.
여기서 유의할 점은 반환하는 배열 안의 객체의 키 이름이 동적 라우팅 폴더 이름과 일치해야 하고
값은 반드시 문자열이어야 합니다.
이렇게 배열을 반환하면, 내가 만들어야 할 페이지 개수를 id로 인식하고, 미리 그 개수만큼 HTML을 생성해둡니다.
이것의 적용 확인은 프로덕션 빌드 시에 나타나므로, npm run build로 빌드를 한후 서버를 켜야 합니다.
이렇게 정적으로 미리 생성해둔 기술을 **SSG(Static Site Generation)**이라 명명합니다.
추가로 생기는 의문점은 이렇게 정적으로 접속하는 id 이외의 id로 접근한다면 어떠할까요?

결론적으로는 설정에 따라 다른데, 기본적으로 generateStaticParams에 포함되지 않은 id로 접속하면
Next.js는 그제야 서버에서 실시간으로 동적 랜더링하여, 페이지를 생성하여 보여줍니다.
자주 찾는 인기 상품은 정적으로 빠르게 보여주고, 가끔 찾는 비인기 상품은 동적으로 보여주는 유연한 전략입니다.
만약 미리 만들지 않은 페이지는 접속을 막고 싶다면 다음과 같이 설정하세요.
export const dynamicParams = falseSEO(Search Engine Optimization)
SNS를 통해 사이트를 공유할 때 썸네일이나 타이틀에 아무것도 뜨지 않는다면 어떠할까요?
사용자는 그 링크를 클릭하고 싶지 않을 것이고, 구글 엔진을 통한 검색 노출도 없을 것입니다.
웹 사이트의 명함이자 얼굴이라고 할 수 있는 metadata를 상품 마다 다르게 설정하는 방법에 대해 알아봅니다.
이런 metadata를 검색 엔진이 읽기 좋게 다듬는 것이 SEO의 과정입니다.
지금까지 우리는 layout.tsx나 page.tsx에서 고정된 metadata를 다음과 같이 생성해 왔습니다.
export const metadata = {
title: 'My Site',
description: 'Fixed Description'
}하지만 상품 상세 페이지처럼 ID에 따라 제목이 변경되는 경우에는 어떠할까요?
이 때 사용하는 것이 generateMetadata 함수입니다. 이 함수는 metadata를 생성하는 역할을 합니다.
앞서 살펴봤듯이 Next.js 컴포넌트에서 params는 비동기로 호출되기 때문에, 이 함수 역시 async-await 패턴입니다.
다음과 같이 generateMetadata 함수를 추가합니다.
import { Metadata } from 'next'
export async function generateMetadata({
params
} : {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await param
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
const product = await res.json()
return {
title: `${product.title} | My Amazing Shop`,
description: product.body.slice(0, 100),
openGraph: {
title: product.title,
description: '지금 바로 우리 쇼핑몰에서 확인하세요!',
images: [`/api/og?title=${product.title}`]
}
}
}여기서 Metadata 값을 조작하기 위해 API 호출을 하고 있는데, 이거 때문에 page.tsx에서의 호출과
중복되지 않느냐는 의문이 생길 수 있습니다.
그러나 Next.js에서는 성능 저하없이 똑같은 API가 여러번 호출되면 자동으로 요청 중복제거 매커니즘에 따라
자동으로 하나로 합쳐서 한번만 호출합니다.