
빌드 결과 정적/동적 확인하기
Next.js 빌드 로그에 나오는 동그라미(○)와 람다(λ) 기호의 의미를 아시나요? 실수로 모든 페이지를 동적으로 만들어버리지 않는 방법을 확인하세요.

Next.js 빌드 로그에 나오는 동그라미(○)와 람다(λ) 기호의 의미를 아시나요? 실수로 모든 페이지를 동적으로 만들어버리지 않는 방법을 확인하세요.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

느리다고 느껴서 감으로 최적화했는데 오히려 더 느려졌다. 프로파일러로 병목을 정확히 찾는 법을 배운 이야기.

엄청난 데이터를 아주 적은 메모리로 검사하는 방법. 100% 정확도를 포기하고 99.9%의 효율을 얻는 확률적 자료구조의 세계. 비트코인 지갑과 스팸 필터는 왜 이것을 쓸까요?

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

제가 처음으로 Next.js로 만든 커머스 사이트를 런칭하던 날이었습니다. 모든 기능 개발은 완벽했습니다. 결제도 잘 되고, 장바구니도 잘 담기고, 디자인은 아름다웠습니다. 마지막으로 AWS EC2 t3.small 인스턴스에 배포를 하고, 떨리는 마음으로 도메인을 입력했습니다.
엔터.
... ... ... (3초 경과) ... (5초 경과)
화면이 떴습니다. 5초 만에요.
"아니, 로컬에서는 빨랐는데?" 서버 리소스를 확인해보니 CPU가 100%를 치고 있었습니다. 사용자는 저 한 명뿐인데 말이죠. 단순한 소개 페이지(Landing Page)조차 로딩하는 데 2초가 넘게 걸렸습니다. 이미지는 최적화했고, 폰트도 서브셋을 썼습니다. 도대체 뭐가 문제일까?
밤을 새우며 원인을 찾던 중, 무심코 지나쳤던 빌드 로그가 눈에 들어왔습니다.
Route (app) Size First Load JS
┌ λ / 5.4 kB 89 kB
├ λ /about 2.1 kB 84 kB
├ λ /blog/[slug] 3.5 kB 87 kB
└ λ /products 1.5 kB 83 kB
모든 경로 앞에 λ (람다) 기호가 붙어 있었습니다.
저는 이 기호가 그냥 "Next.js 로고" 같은 건 줄 알았습니다. 하지만 이건 "너의 사이트는 지금 최악의 성능으로 동작하고 있어"라는 경고였습니다.
저는 정적 사이트(Static Site)를 만들었다고 생각했지만, 실제로는 모든 페이지가 SSR(Server-Side Rendering)로 돌고 있었던 겁니다.
Next.js의 npm run build 결과 화면은 단순한 로그가 아닙니다. 여러분의 서버 비용과 사용자 이탈률을 결정하는 성적표입니다.
이 기호들의 의미를 정확히 아는 것만으로도, 추가 비용 없이 성능을 10배 높일 수 있습니다.
"Automatically rendered as static HTML (uses no initial props)"
index.html 파일을 Nginx(또는 CDN)가 던져줍니다."Automatically generated as static HTML + JSON (uses getStaticProps)"
getStaticProps나 generateStaticParams를 사용한 경우입니다. 결과물은 역시 정적 파일입니다."Server-side renders at runtime (uses getInitialProps or getServerSideProps)"
제 로그는 전부 λ였습니다. 정적인 회사 소개 페이지(/about)조차도요.
즉, 저는 Next.js를 쓴 게 아니라 비싼 PHP를 쓰고 있었던 셈입니다.
"잠깐, 저는 getServerSideProps를 안 썼어요! App Router 쓰면서 fetch만 썼는데요?"
억울했습니다. Next.js 13부터는 "Static by Default (기본적으로 정적)"라고 광고했잖아요? 하지만 Next.js에는 "Dynamic Functions(동적 함수)"라는 함정이 있습니다. 코드를 짜다가 무심코 이 함수들을 하나라도 쓰면, Next.js는 해당 페이지 전체를 Dynamic으로 강등시킵니다.
cookies()와 headers() (가장 흔한 실수)로그인 여부에 따라 상단 네비게이션(GNB)을 다르게 보여주려고 layout.tsx에 이런 코드를 짰습니다.
// app/layout.tsx
import { cookies } from 'next/headers';
export default function RootLayout({ children }) {
const cookieStore = cookies(); // <-- 여기가 범인!
const accessToken = cookieStore.get('accessToken');
return (
<html>
<body>
<Navbar isLoggedIn={!!accessToken} />
{children}
</body>
</html>
);
}
이 코드는 재앙입니다.
RootLayout은 모든 페이지의 부모입니다. 여기서 cookies()를 호출한다는 건, "이 사이트의 모든 페이지는 요청(쿠키)마다 달라진다"고 선언한 것과 같습니다.
결과적으로 /, /about, /blog 모든 페이지가 λ (Dynamic)으로 변합니다. 정적 최적화가 전멸했습니다.
searchParams Props블로그 목록 페이지에서 "현재 페이지 번호"를 알기 위해 searchParams를 썼습니다.
// app/blog/page.tsx
export default function Blog({ searchParams }) { // <-- 범인
const page = searchParams.page || '1';
// ...
}
URL의 쿼리 스트링(?page=2)은 요청할 때마다 달라질 수 있습니다. Next.js는 "어? 너 쿼리 스트링 쓰네? 그럼 미리 빌드 못 하지."라며 Dynamic으로 전환합니다.
이게 합리적인 판단일까요? 블로그 글 목록 1페이지는 항상 똑같잖아요? 굳이 Dynamic일 필요가 없습니다.
fetch의 no-storefetch('https://api.example.com/posts', { cache: 'no-store' });
"최신 글을 보여줘야지" 하고 무지성으로 캐시를 껐습니다. 데이터의 최신성을 얻은 대신, 페이지 로딩 속도를 잃었습니다. 이 코드 한 줄 때문에 페이지 전체가 SSR로 동작합니다.
문제를 알았으니 코드를 뜯어고쳤습니다. 목표는 "개인화된 페이지 빼고는 전부 정적으로 만든다"였습니다.
cookies()는 클라이언트 컴포넌트로 격리한다서버 컴포넌트(layout.tsx)에서 쿠키를 읽지 마세요.
로그인 상태에 따라 UI를 바꾸고 싶다면, Client Component 안에서 useEffect를 쓰거나 전용 라이브러리(Auth.js의 SessionProvider)를 써야 합니다.
// app/layout.tsx (Server Component)
// cookies() 삭제!
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider> {/* 클라이언트 컴포넌트 */}
<Navbar />
</AuthProvider>
{children}
</body>
</html>
);
}
이렇게 하면 RootLayout은 다시 정적이 될 수 있습니다. 로그인 상태 확인은 브라우저(클라이언트)에서만 일어나니까요.
generateStaticParams 적극 활용동적 라우트([slug])를 쓴다고 무조건 λ가 되는 건 아닙니다. "가능한 모든 경로"를 미리 알려주면 됩니다.
// app/blog/[slug]/page.tsx
// 이 함수가 핵심입니다!
export async function generateStaticParams() {
const posts = await getPosts(); // DB에서 모든 글 slug 가져옴
return posts.map((post) => ({ slug: post.slug }));
}
export default function Post({ params }) { ... }
이 함수가 있으면 Next.js는 빌드 타임에 getPosts()를 실행해서, blog/1, blog/2, blog/3... 모든 HTML을 미리 만들어둡니다(●).
force-static)가장 강력한 무기입니다.
"나는 headers()를 읽긴 하지만(예: IP 확인), 값이 없으면 그냥 기본값 보여줘도 되니까 제발 정적으로 만들어줘!"
이런 경우(하이브리드)가 필요할 때가 있습니다.
export const dynamic = 'force-static';
이 코드를 페이지 파일 상단에 추가하면, 동적 함수(cookies(), headers())가 있어도 빌드 타임에는 빈 값(undefined)을 반환하고 강제로 정적 페이지를 생성합니다.
며칠 간의 리팩토링 끝에 다시 빌드를 돌렸습니다.
Route (app) Size First Load JS
┌ ○ / 5.4 kB 89 kB
├ ○ /about 2.1 kB 84 kB
├ ● /blog/[slug] 3.5 kB 87 kB
├ ○ /login 1.5 kB 83 kB
└ λ /my-page 1.2 kB 80 kB
드디어!
메인, 소개, 블로그, 로그인 페이지까지 전부 ○ (Static) 또는 ● (SSG)로 돌아왔습니다. /my-page만 λ (Dynamic)으로 남았는데, 이건 사용자별로 정보가 다르니 당연한 겁니다.
배포 후 다시 성능 테스트를 했습니다.
Next.js를 쓴다고 저절로 빨라지는 게 아닙니다. 잘못 쓰면 오히려 PHP보다 느려질 수 있습니다.
npm run build 로그를 꼭, 제발 확인하세요.
cookies(), headers(), searchParams가 은밀한 범인일 확률이 90%입니다. 이 녀석들을 Suspense 경계 안으로 가두거나, 클라이언트로 보내버리세요.○와 ●를 사랑하세요. 그리고 λ는 정말 필요할 때만 허락하세요. 그것이 여러분의 사용자와 지갑을 모두 지키는 길입니다.