진행 배경
- 초기 렌더링 개선과 SEO를 위해 RSC(React Server Component)를 사용하기 위해 Next14 App Router적용,
렌더링이 서버에서 처리되면서 서버비용이 많이 들어감 - 트래픽이 많은경우 CPU 부하가 심해짐
- 상세페이지 레이아웃, 상품정보는 자주 변경되는 정보가 아님
웹서버의 성능이 중요한 이유
얼마나 빠르고, 효율적으로 사용자들에게 콘텐츠를 제공하느냐를 의미
- 빠른로딩으로 사용자 경험 향상
- 빠른사이트는 SEO유리
- 네트워크 사용량 감소, 서버 요청 최소화로 인프라 비용 절감
개선할점
상품스펙에 관련 된 상품정보는 자주 변경되는 정보가 아니기 때문에 상품페이지는 Full Route Cache를 적용해서 캐시된 HTML을 적용한다.
Full Route Cache란?
Next.js는 4가지 캐시 매커니즘이 있다.
Request Memoiztion - 동일한 요청(fetch 등)이 같은 렌더링 트리 안에서 여러 번 호출될 때, 메모리에서 즉시 재사용
Data Cache - 서버에서 실행된 fetch 결과를 디스크/인프라 레벨에 캐싱
Full Route Cache - 정적 생성(SSG) 결과를 전역 CDN에 저장, 전 세계적으로 캐싱
Router Cache - 특정 라우트의 전체 렌더링 결과(HTML + 데이터)를 캐싱
이번에 사용하는 캐시는 Full Route Cache
서버 렌더링 과정에서 웹 서버의 리소스를 대부분 사용하게 되는데,
Full Route Cache는 서버 렌더링 결과를 재사용함으로서 서버비용을 줄일 수 있다.


변경해야하는 부분
1. 해당 라우트는 정적 캐시 대상으로 설정해야함
export const dynamicParams = true;
export const revalidate = 86000;
export const dynamic = 'force-static';
dynamicParams
dynamicParams를 true로 설정하면 generateStaticParams로 미리 생성되지 않은 동적 세그먼트도 런타임에 접근이 가능해집니다.
거기에 revalidate 속성도 있다면 ISR로 작동해서 그 시간동안 캐시를 유지하고, 요청 시 페이지를 새로 생성하고 캐시합니다.
revalidate
레이아웃이나 페이지의 기본 재검증 시간을 설정
false : 리소스 무제한 캐시
0 : 항상 동적으로 렌더링
number: (초) 레이아웃이나 페이지의 기본 재검증 빈도를 n초로 설정
dynamic
레이아웃이나 페이지의 동적 동작을 완전히 정적이거나, 동적으로 변경
// 'auto' | 'force-dynamic' | 'error' | 'force-static'
force-static : 정적 렌더링을 강제하고, 레이아웃이나 페이지의 데이터를 캐시
2. 서버 컴포넌트 내 동적 데이터 제거
Cookies(), headers() 등과 같은 Dynamic Funciton은 request 기반이라 SSR처리가 되서 관련한 코드는 클라이언트로 변경해줘야한다
- middleware에서 진행했던 특정 패스 다른 패스로 redirect시키는 로직 클라이언트 컴포넌트로 수정
// as is
if ((pathname.includes('/today') && searchParams.get('tab')) || (pathname.includes('/release') && searchParams.get('tab'))) {
return NextResponse.redirect(`${origin}${pathname}`);
}
// to be
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
const RedirectHandler = () => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
const origin = window.location.origin;
const tab = searchParams.get('tab');
if ((pathname.includes('/today') && tab) || (pathname.includes('/release') && tab)) {
router.replace(`${origin}${pathname}`);
return;
}
}, [pathname, searchParams, router]);
return null;
};
export default RedirectHandler;
- Next.js middleware에서 처리했던 로그인 검증 로직 authProvider를 만들어서 처리
로그인이 필요한 패스에 authProvider를 만들어서 HOC 디자인 패턴으로 작동할 수 있게 해줍니다.
// as is
const token = request.cookies.get(LOGIN_UUID);
const currentPath = pathname;
if (!token && PROTECTED_ROUTES.includes(currentPath)) {
const url = request.nextUrl.clone();
url.searchParams.set('nextUrl', currentPath);
url.pathname = encodeURI(`/login`);
return NextResponse.redirect(url);
}
if (token && PUBLIC_ROUTES.includes(currentPath)) {
const url = request.nextUrl.clone();
url.pathname = searchParams.get('nextUrl') || '/';
url.searchParams.delete('nextUrl');
return NextResponse.redirect(url);
}
// to be
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const pathname = usePathname();
useEffect(() => {
const token = getCookie(LOGIN_UUID);
setIsAuthenticated(!!token);
setIsLoading(false);
}, []);
const requestLogin = () => {
const url = new URL(window.location.href);
url.searchParams.set('nextUrl', pathname);
url.pathname = encodeURI(`/login`);
redirect(url.toString());
};
if (isLoading) return <></>;
if (!isAuthenticated) {
requestLogin();
return <></>;
}
if (isAuthenticated) {
return <>{children}</>;
}
return <></>;
};
- 앱전용 레이아웃(앱에서는 헤더, 푸터가 없음 등)들도 클라이언트에서 처리해주도록 수정해준다.
(단점은 마운트 될때 깜빡임이 있을수 있음)
3. 상세페이지 같은 동적 라우트를 처리하기위해 generateStaticParams()를 사용해 static한 파일로 만들어줍니다.
상세페이지가 너무 광범위하기때문에 dynamicParams, revalidate를 사용해서 동적으로 생성, ISR로 작동하게하여 그 시간동안 캐시를 유지하고, 요청 시 페이지를 새로 생성하고 캐시합니다.
4. Full Route Cache를 적용하는 페이지 내 서버fetch는 next.revalidate 값을 설정해줍니다.
html에 적용한 revalidate시간보다 크커나 같아야한다.
캐시 적용 확인
캐시가 HIT 된 것을 확인하기 위해 간단한 서버 컴포넌트를 추가해줍니다.
function DebugCache({path}: {path: string}) {
return (
<div>
{format(new Date(), 'yyyy-MM-dd-HH:mm:ss')}
<RevalidateButton path={path} />
</div>
);
}
'use client'
function RevalidateButton({ path }: {path: string}) {
return <button onClick={() => revalidateFullRouteCache(path)}>revalidate</button>
}
'use server'
import { revalidatePath } from 'next/cache';
export async function revalidateFullRouteCache(path) {
if (path) {
revalidatePath(path, 'layout');
}
}
한번 받아온 html과 RSC는 cache HIT 처리가 되는것을 확인할수있다.

응답시간과 TPS
응답시간: 클라이언트가 서버에 요청을 하고 그 요청에 대한 응답을 받을 때까지 걸린 시간을 의미
TPS: 서버가 초당 처리할 수 있는 요청의 개수, TPS가 높을수록 초당 처리할 수 있는 요청의 수가 많다.
결과
테스트 Apache JMeter를 사용하여 부하테스트를 진행하였고
테스트 시나리오: 1초에 50번의 부하를 10번 반복하도록 하였습니다.
TPS(서버)가 캐시전보다 50%정도 개선된걸 확인할 수 있었습니다.
[적용 전]

Label - Sampler 이름
Samples - requset 개수
Average - 응답 평균
Min - 응답 최소
Max - 응답 최대
Std. Dev. - 응답 표준편차
Error % - 에러율
Throughput - 시간당 처리량
Received KB/sec - 시간당(sec) 받은 데이터(KB)
Sent KB/sec - 시간당(sec) 보낸 데이터(KB)
Avg. Bytes - 평균 바이트

[적용 후]


참고
https://fe-developers.kakaoent.com/2024/240418-optimizing-nextjs-cache/
https://leeggmin.tistory.com/10 - jmeter 사용방법
'Next.js' 카테고리의 다른 글
| Next.js App Router의 실제 사용 경험과 한계점 (0) | 2025.12.30 |
|---|---|
| Server Component와 Client Component 차이 (3) | 2025.07.25 |
| Next 스트리밍 with Suspense (5) | 2025.07.24 |
| persistQueryClient를 활용해 초기렌더링 개선하기 (3) | 2025.07.08 |
| Next.js with CSS-in-JS (0) | 2024.12.26 |
