Next.js 16 소개
React만으로 생기는 라우팅·데이터 페칭·SEO·배포의 빈틈을 살펴보고 Next.js 16의 도입 기준을 세웁니다.
React로 서비스를 만들어 보았다면 한 번쯤 이런 순간을 겪습니다. 첫 화면은 느리고, 검색 노출(SEO)은 기대만큼 나오지 않으며, 라우팅과 데이터를 가져오는 데이터 페칭(data fetching), 배포 설정이 프로젝트마다 조금씩 달라 유지보수가 불편해지는 순간입니다. 작게 시작한 코드가 커질수록 기능을 구현하는 시간보다 구조를 정리하고 성능을 손보는 시간이 더 늘어나기도 합니다.
Next.js는 바로 이 지점을 해결하려고 React 위에 필요한 실행 규칙과 최적화 도구를 얹은 프레임워크입니다. 이 글에서는 Next.js 16을 기준으로, Next.js가 왜 편의 도구에 그치지 않고 실전 개발의 생산성과 안정성을 함께 높이는 선택이 되는지부터 정리합니다. 이 배경을 이해하면 뒤의 장에서 배우는 App Router와 캐싱, 서버 액션이 자연스럽게 이어집니다. 이 교재는 React 교재에서 다룬 컴포넌트와 props, state, 훅을 알고 있다고 가정합니다.
Next.js가 채우는 React의 빈틈
React는 사용자 인터페이스를 만드는 라이브러리입니다. 하지만 React만으로는 서버 측 렌더링(SSR)이나 정적 사이트 생성(SSG) 같은 기능을 구현하기 쉽지 않고, 프로젝트의 구조와 성능 최적화도 직접 설계해야 합니다.
Next.js는 React 기반의 풀스택 웹 프레임워크로, React 애플리케이션을 더 쉽고 효율적으로 만들 수 있도록 여러 기능과 최적화 도구를 내장하고 있습니다. React의 장점을 그대로 살리면서 라우팅과 렌더링, 데이터 페칭, 배포에 공통 규칙을 제공합니다. 서버에서 HTML을 준비하고 메타데이터를 관리하면 첫 화면과 검색 노출에 유리한 구조를 만들 수 있지만, 실제 속도와 검색 순위는 구현과 서비스의 조건에 따라 달라집니다.
Next.js 16에서 달라진 점
Next.js는 꾸준히 발전하며 새 기능을 내놓고 있습니다. 이 교재가 다루는 Next.js 16은 App Router에 React 19.2의 기능을 포함한 React Canary를 통합하고, 번들링과 캐싱의 선택지를 개선했습니다. 이 버전의 기본 동작과 따로 켜야 하는 기능을 구분해 살펴봅시다.
App Router에서는 React 19.2의 useEffectEvent와 Activity 같은 기능을 쓸 수 있습니다. Turbopack은 개발과 프로덕션 빌드의 기본 번들러가 되었으며, React Compiler 지원은 안정화되었지만 별도의 설정으로 켜야 하고 빌드 시간이 늘어날 수도 있습니다. 캐싱은 더 명시적으로 바뀌어, Cache Components를 cacheComponents: true로 켜고 use cache로 캐시할 범위를 정합니다. 16.4부터는 create-next-app의 권장 설정이 이 옵션을 켠 채로 새 프로젝트를 만들지만(Next.js 16.4 발표), 이때도 데이터 페칭은 기본으로 동적이고 캐시할 범위는 use cache로 직접 정하므로 모든 요청이 저절로 캐시되지는 않습니다. 16.4 문서는 cacheComponents: true를 partialPrefetching: true와 함께 적으라고 안내하며, 16.4의 create-next-app도 이 옵션을 켤 때 두 줄을 함께 적습니다. 직접 켜면서 partialPrefetching을 빠뜨리면 경고가 출력됩니다(Next.js 문서의 cacheComponents). 이 교재의 예제는 프로젝트 생성에서 이 옵션을 켜지 않은 구성을 기준으로 합니다. 개발 로그도 요청의 컴파일·렌더링 시간과 빌드 단계별 시간을 나누어 보여 주므로 병목을 찾기 쉬워졌습니다. 이 구분은 Next.js 16 공식 발표의 기본값과 선택 기능을 기준으로 하며, 이 글에서 성능 향상을 직접 측정한 것은 아닙니다.
중요한 것은 최신 버전이니 무조건 빠르다는 생각이 아니라, 어떤 기능을 어떤 상황에서 켜고 끄느냐가 성능을 좌우한다는 사실입니다. 예를 들어 캐싱 정책을 잘못 잡으면 오래된 데이터가 노출될 수 있고, 동적 렌더링의 범위를 지나치게 넓히면 서버 비용이 불필요하게 늘어납니다. 기본 동작과 선택 기능을 구분하고 서비스 조건에 맞게 조정해야 실제 품질을 평가할 수 있습니다.
요구사항을 전략으로 바꾸는 기준
도입하기 전의 질문을 렌더링 전략, 캐싱 정책, 서버와 클라이언트의 구분이라는 세 가지 결정으로 바꿔 봅니다. 같은 서비스 안에서도 라우트마다 답이 달라질 수 있습니다. 아래 표의 LCP(Largest Contentful Paint)는 페이지에 들어온 뒤 화면에 보이는 가장 큰 이미지나 글 덩어리가 그려지기까지의 시간이고, TTFB(Time to First Byte)는 페이지 요청을 시작한 뒤 응답의 첫 바이트가 도착하기까지의 시간입니다(웹 성능 최적화 기본 원칙).
| 요구사항 | 결정하고 확인할 것 |
|---|---|
| 검색 노출·첫 화면·개인화 | HTML을 언제 준비할지 정하고, 첫 진입의 LCP·TTFB와 사용자별 출력 여부를 확인합니다. |
| 데이터 최신성·변경 | 캐시의 수명과 무효화를 맡을 곳을 정하고, 갱신한 뒤 오래된 값을 얼마나 허용할지 확인합니다. |
| 비밀값·DB·상호작용 | 서버에 남길 코드와 브라우저에서 실행할 코드를 나누고, 비밀값의 노출과 브라우저로 보내는 JavaScript를 확인합니다. |
온라인 서점의 상품 상세 화면을 만든다고 해 봅시다. 서버는 상품 ID로 데이터베이스를 조회하고, 공개해도 되는 제목과 가격을 화면에 넘기며, 데이터베이스 접속 자격 증명은 서버에 남깁니다. 브라우저의 “수량 늘리기” 버튼은 사용자의 클릭을 받아 이번 화면의 수량만 바꿉니다. 이 버튼이 있다는 이유로 상품 조회 코드와 비밀값까지 브라우저로 옮길 필요는 없습니다.
첫 진입에서는 서버가 준비한 HTML로 상품 이름을 먼저 보여 줄 수 있고, 버튼이 동작하려면 그 클라이언트 코드가 화면에 연결되는 과정도 필요합니다. 이 과정을 하이드레이션(hydration)이라고 합니다. 첫 HTML과 클라이언트 상호작용, 넘기는 props의 구분은 Next.js 문서의 서버·클라이언트 컴포넌트를 기준으로 합니다.
공개 상품 조회와 브라우저 수량 선택의 실행 경계
상품 서버 페이지가 비공개 DB 자격 증명으로 데이터를 읽고 공개 제목·가격을 초기 출력에 넘긴 뒤, 브라우저의 수량 조작은 지역 상태만 바꾸는 실제 경계를 같은 역할의 상태 변화로 추적할 수 있습니다.
NEXT · FIRST LOAD · PUBLIC PRODUCT
상품 조회는 서버에, 클릭은 브라우저에 둡니다
공개 상품 화면의 첫 진입 예입니다. 서버의 조회·응답 준비와 브라우저의 HTML 표시·상호작용 연결을 구분합니다. 수량 선택은 화면 상태만 바꿉니다.
실선은 요청·조회 결과·응답 또는 이 예제의 처리 순서입니다. 서버에 둔 비밀을 props로 보내면 숨겨지지 않습니다. 공개 필드만 전달하며 서버 저장·결제·정확한 캐시 API는 이 그림의 범위 밖입니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 공개 상품 화면의 첫 HTTP 진입을 단순화한 예이다. 브라우저의 GET 요청을 서버 페이지가 처리하며 서버의 DB 조회는 서버에만 있는 자격 증명을 사용한다. 공개 제목·가격을 고른 뒤 초기 HTML과 클라이언트 경계 데이터·코드를 준비한다. 브라우저는 HTML로 첫 화면을 표시하고 클라이언트 상호작용을 연결한 뒤 수량 버튼으로 화면 상태를 바꾼다. DB 비밀은 전달하지 않으며 props로 넘긴 값은 공개된다. 버튼 수량 변경은 서버 장바구니 저장이나 DB 변경이 아니다. 모바일은 같은 요청·조회·공개 응답·표시·상호작용 순서와 실행 영역을 유지한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 공개 상품 조회와 브라우저 수량 선택의 실행 경계 |
| 6 | 공개 상품 화면의 첫 HTTP 진입을 단순화한 예이다. 브라우저의 GET 요청을 서버 페이지가 처리하며 서버의 DB 조회는 서버에만 있는 자격 증명을 사용한다. 공개 제목·가격을 고른 뒤 초기 HTML과 클라이언트 경계 데이터·코드를 준비한다. 브라우저는 HTML로 첫 화면을 표시하고 클라이언트 상호작용을 연결한 뒤 수량 버튼으로 화면 상태를 바꾼다. DB 비밀은 전달하지 않으며 props로 넘긴 값은 공개된다. 버튼 수량 변경은 서버 장바구니 저장이나 DB 변경이 아니다. 모바일은 같은 요청·조회·공개 응답·표시·상호작용 순서와 실행 영역을 유지한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 첫 요청 브라우저 GET을 서버 page가 받고 서버 자격 증명으로 DB를 읽습니다. | 요청: GET · DB 자격 증명: 서버 내부 · 브라우저: 대기 | 서버가 공개 화면 자료를 조회합니다. |
| 공개 출력 준비 제목·가격을 골라 초기 HTML과 Client 경계 자료·코드를 준비합니다. | 공개값: 제목·가격 · DB 비밀: 전달 안 함 · props: 공개 자료 | 브라우저에 전달할 값이 서버 비밀과 분리됩니다. |
| 첫 표시와 상호작용 브라우저가 HTML로 첫 화면을 표시하고 Client 상호작용을 연결합니다. | 화면: 초기 HTML · Client: 상호작용 연결 | 브라우저가 표시·조작 가능한 상태로 바뀝니다. |
| 수량 지역 변경 수량 버튼은 브라우저 지역 화면 상태를 바꿉니다. | 변경: 지역 수량 · 서버 cart 저장: 없음 · DB 변경: 없음 | 같은 화면 수량만 바뀝니다. |
공개 상품 조회와 브라우저 수량 선택의 실행 경계
상품 서버 페이지가 비공개 DB 자격 증명으로 데이터를 읽고 공개 제목·가격을 초기 출력에 넘긴 뒤, 브라우저의 수량 조작은 지역 상태만 바꾸는 실제 경계를 같은 역할의 상태 변화로 추적할 수 있습니다.
NEXT · FIRST LOAD · PUBLIC PRODUCT
상품 조회는 서버에, 클릭은 브라우저에 둡니다
공개 상품 화면의 첫 진입 예입니다. 서버의 조회·응답 준비와 브라우저의 HTML 표시·상호작용 연결을 구분합니다. 수량 선택은 화면 상태만 바꿉니다.
실선은 요청·조회 결과·응답 또는 이 예제의 처리 순서입니다. 서버에 둔 비밀을 props로 보내면 숨겨지지 않습니다. 공개 필드만 전달하며 서버 저장·결제·정확한 캐시 API는 이 그림의 범위 밖입니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 공개 상품 화면의 첫 HTTP 진입을 단순화한 예이다. 브라우저의 GET 요청을 서버 페이지가 처리하며 서버의 DB 조회는 서버에만 있는 자격 증명을 사용한다. 공개 제목·가격을 고른 뒤 초기 HTML과 클라이언트 경계 데이터·코드를 준비한다. 브라우저는 HTML로 첫 화면을 표시하고 클라이언트 상호작용을 연결한 뒤 수량 버튼으로 화면 상태를 바꾼다. DB 비밀은 전달하지 않으며 props로 넘긴 값은 공개된다. 버튼 수량 변경은 서버 장바구니 저장이나 DB 변경이 아니다. 모바일은 같은 요청·조회·공개 응답·표시·상호작용 순서와 실행 영역을 유지한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 공개 상품 조회와 브라우저 수량 선택의 실행 경계 |
| 6 | 공개 상품 화면의 첫 HTTP 진입을 단순화한 예이다. 브라우저의 GET 요청을 서버 페이지가 처리하며 서버의 DB 조회는 서버에만 있는 자격 증명을 사용한다. 공개 제목·가격을 고른 뒤 초기 HTML과 클라이언트 경계 데이터·코드를 준비한다. 브라우저는 HTML로 첫 화면을 표시하고 클라이언트 상호작용을 연결한 뒤 수량 버튼으로 화면 상태를 바꾼다. DB 비밀은 전달하지 않으며 props로 넘긴 값은 공개된다. 버튼 수량 변경은 서버 장바구니 저장이나 DB 변경이 아니다. 모바일은 같은 요청·조회·공개 응답·표시·상호작용 순서와 실행 영역을 유지한다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 첫 요청 브라우저 GET을 서버 page가 받고 서버 자격 증명으로 DB를 읽습니다. | 요청: GET · DB 자격 증명: 서버 내부 · 브라우저: 대기 | 서버가 공개 화면 자료를 조회합니다. |
| 공개 출력 준비 제목·가격을 골라 초기 HTML과 Client 경계 자료·코드를 준비합니다. | 공개값: 제목·가격 · DB 비밀: 전달 안 함 · props: 공개 자료 | 브라우저에 전달할 값이 서버 비밀과 분리됩니다. |
| 첫 표시와 상호작용 브라우저가 HTML로 첫 화면을 표시하고 Client 상호작용을 연결합니다. | 화면: 초기 HTML · Client: 상호작용 연결 | 브라우저가 표시·조작 가능한 상태로 바뀝니다. |
| 수량 지역 변경 수량 버튼은 브라우저 지역 화면 상태를 바꿉니다. | 변경: 지역 수량 · 서버 cart 저장: 없음 · DB 변경: 없음 | 같은 화면 수량만 바뀝니다. |
위 그림은 공개 상품 화면에 처음 들어올 때, 서버가 조회하고 응답을 준비하는 일과 브라우저가 HTML을 보여 주고 상호작용을 연결하는 일을 나누어 보여 줍니다. 그림의 요약처럼 상품 조회는 서버에, 클릭은 브라우저에 두며, 수량 선택은 화면 상태만 바꿉니다. 브라우저가 GET /products/42를 요청하면 서버의 페이지가 상품 ID로 조회하고, DB 조회는 서버에만 있는 자격 증명을 쓰며, 서버는 공개할 제목과 가격을 골라 첫 HTML을 준비합니다. 브라우저는 그 HTML로 첫 화면을 보여 주고 JavaScript로 상호작용을 연결한 뒤, 수량 버튼을 누르면 화면의 수량만 갱신합니다.
그림 아래의 설명이 짚듯 실선은 요청과 조회 결과, 응답, 그리고 이 예제의 처리 순서를 나타냅니다. 서버에 둔 비밀을 props로 보내면 숨겨지지 않으므로 공개할 필드만 넘기며, 서버 저장이나 결제, 정확한 캐시 API는 이 그림의 범위 밖입니다. 그림의 수량 선택은 화면 상태만 바꾸는 예일 뿐, 장바구니를 서버에 저장하거나 결제하는 흐름까지 구현한 것은 아닙니다.
같은 서점 안에서도 공개 상품 설명과 로그인한 사용자의 장바구니는 요구가 다릅니다. 상품 설명은 정해 둔 최신성의 범위에서 여러 사용자가 함께 쓸 수 있지만, 개인의 장바구니는 사용자를 확인하고 데이터의 범위를 먼저 정해야 합니다. 한 서비스가 Next.js를 쓴다는 이유로 모든 URL에 같은 렌더링과 캐시 정책을 적용하지 않으며, 구체적인 파일과 캐시 API는 뒤의 장에서 이 사례에 연결합니다.
도입을 판단하는 기준
Next.js를 도입할지 빠르게 판단하려면, 서비스에서 검색 노출과 첫 로딩 성능이 핵심 요구사항인지, 라우팅과 데이터 페칭, 배포 규칙을 프레임워크 단위로 통일할 필요가 있는지, 서버 렌더링과 클라이언트 상호작용을 한 프로젝트에서 함께 관리해야 하는지, 팀이 캐싱과 렌더링 전략을 문서로 남겨 운영할 준비가 되어 있는지를 먼저 확인합니다.
프레임워크는 기능 목록보다 운영 기준에 맞춰 고르는 편이 안전합니다. 릴리스마다 성능 회귀를 확인할 측정 지표(LCP, TTFB)를 갖추었는지, 서버 렌더링과 캐싱 정책을 팀 규칙으로 문서화할 수 있는지 점검하고, 기능 요구보다 유지보수 비용이 큰 라우팅과 데이터 페칭, 배포를 먼저 표준화합니다. 처음에는 모든 기능을 한꺼번에 최적화하기보다, 핵심 사용자 시나리오 하나를 기준으로 렌더링 전략을 먼저 정하는 것이 효과적입니다. 이 기준이 있으면 뒤의 장에서 캐싱과 서버 액션을 일관된 규칙으로 정하기 쉽습니다.
팀 규칙에는 URL과 레이아웃을 어느 폴더가 맡는지, 로딩과 오류 UI, 데이터를 바꿀 때의 검증·권한·재검증 순서도 넣습니다. 배포를 맡은 사람은 런타임과 환경 변수, 지역, 로그, 롤백 기준을 함께 정하며, 이것을 설명하기 어렵다면 작은 라우트에서 먼저 검증하고 범위를 넓힙니다.
다음 글로 넘어가기 전에, 브라우저가 JavaScript로 화면을 그리는 클라이언트 측 렌더링(CSR)만 썼을 때 겪은 검색 노출과 첫 로딩의 한계를 실제 사례로 떠올려 보고, 라우팅 규칙을 팀 단위로 맞춰야 했던 경험과 서버와 클라이언트의 구분이 애매해졌던 기능 하나를 적어 두세요. 다음 글에서 두 라우터를 비교할 때 선택 기준과 이어집니다.
정리
- Next.js는 React 위에 라우팅과 렌더링, 데이터 페칭, 배포의 공통 규칙과 최적화 도구를 얹은 풀스택 프레임워크입니다.
- Next.js 16에서 Turbopack은 기본 번들러가 되었고, React Compiler와 Cache Components(
cacheComponents: true,use cache)는 설정으로 켜는 기능이며, 16.4부터create-next-app의 권장 설정은 Cache Components를 켠 채로 프로젝트를 만듭니다. - 도입 전의 요구사항은 렌더링 전략, 캐싱 정책, 서버와 클라이언트의 구분이라는 세 결정으로 바꾸며, 라우트마다 답이 다를 수 있습니다.
- 서버에서 읽은 값이라도 클라이언트 컴포넌트(Client Component)의 props로 넘기면 브라우저에 전달되므로, 공개할 필드만 넘깁니다.
- 프레임워크는 기능 목록보다 측정 지표와 팀 규칙 같은 운영 기준으로 고릅니다.
연습 문제
1. 서버에 남는 코드와 넘기는 값
상품 페이지를 서버 컴포넌트(Server Component)로 작성했는데, 버튼에 필요하지 않은 DB 접속 비밀을 클라이언트 컴포넌트의 props로도 넘겼습니다. 서버에서 읽은 값이라는 이유로 브라우저에서 숨겨질까요? 현재 수량만 기억하는 버튼에 서버의 상품 조회 모듈을 직접 가져와야 할까요?
해설 보기
서버에서 읽었다는 사실만으로 넘긴 값이 숨겨지지는 않습니다. 클라이언트 컴포넌트에 넘기는 props는 브라우저로 전달되는 데이터이므로 공개해도 되는 상품 필드만 골라 넘기고, 접속 비밀은 넘기지 않습니다.
이 버튼에는 수량 상태와 클릭 처리만 필요합니다. 상품 조회와 자격 증명은 서버에 두고, 나중에 서버 저장 기능을 더할 때는 별도의 요청과 검증을 설계합니다. 서버에 둔 코드, 응답으로 공개한 값, 브라우저의 상호작용은 서로 다른 판단입니다.
2. 같은 서비스의 다른 정책
온라인 서점의 공개 상품 설명 페이지와, 로그인한 사용자의 장바구니 페이지에 같은 캐시 정책을 적용해도 될까요?
해설 보기
적용하면 안 됩니다. 공개 상품 설명은 모든 사용자에게 같은 내용이므로, 정해 둔 최신성의 범위 안에서 캐시를 함께 써도 됩니다. 반면 장바구니는 사용자마다 내용이 다르므로, 한 사용자의 결과를 다른 사용자와 함께 캐시하면 남의 장바구니가 보일 수 있습니다.
장바구니는 사용자를 먼저 확인하고 그 사용자의 데이터만 보여 주는 정책이 필요합니다. 이처럼 같은 서비스 안에서도 라우트마다 렌더링과 캐시 정책을 따로 정합니다.
3. 기본값과 선택 기능
Next.js 16으로 프로젝트를 만들기만 하면 모든 페이지의 결과가 저절로 캐시되고 React Compiler도 켜진다고 생각한 동료가 있습니다. 이 생각에서 틀린 부분은 무엇일까요?
해설 보기
Next.js 16에서 기본으로 바뀐 것은 Turbopack 번들러입니다. Cache Components는 cacheComponents: true로 켜는 기능이며, 16.4부터는 create-next-app의 권장 설정이 이 옵션을 켠 채로 프로젝트를 만듭니다. 그래도 데이터 페칭은 기본으로 동적이고 캐시할 범위는 use cache로 직접 정하므로, 모든 요청을 저절로 캐시하지 않습니다. React Compiler도 지원이 안정화되었을 뿐 별도의 설정으로 켜야 합니다.
그래서 성능을 기대할 때는 어떤 기능이 기본값이고 어떤 기능을 켰는지를 먼저 확인하고, 실제 효과는 측정해서 판단합니다.
다음 글 App Router vs Pages Router에서는 두 라우터의 차이를 비교하고, 프로젝트에 맞는 선택 기준을 세웁니다.