2016년의 풍경
Next.js가 처음 공개된 것은 2016년 10월입니다. 당시 프론트엔드 생태계는 격변기를 지나고 있었습니다. React가 등장한 지 3년째였고, SPA (Single Page Application) 패러다임이 빠르게 자리잡고 있었지만, 실제로 프로덕션에서 React 앱을 만드는 일은 생각보다 복잡했습니다.
그 시절 React 앱을 만들려면 어떤 과정을 거쳐야 했는지 떠올려봅시다.
- Webpack을 직접 설정해야 했습니다.
entry,output,loaders,plugins, 모든 것을 수동으로 구성해야 했고, 한 가지만 틀려도 빌드가 실패했습니다 - Babel도 직접 설정했습니다. JSX를 JavaScript로 변환하고, 최신 문법을 구형 브라우저용으로 트랜스파일하는 설정을 개발자가 직접 관리해야 했습니다
- 라우팅 라이브러리를 선택해야 했습니다. React Router가 있었지만, 버전마다 API가 크게 바뀌어 유지보수가 어려웠습니다
- 서버 사이드 렌더링은 훨씬 더 복잡했습니다. React를 서버에서 렌더링하려면 Express 서버를 직접 띄우고,
ReactDOMServer.renderToString을 호출하고, 클라이언트 번들과 서버 번들을 별도로 만들고, hydration을 처리해야 했습니다
요약하면 설정 지옥이었습니다. "React 앱 하나 만들기"까지 도달하는 데 며칠이 걸리는 일도 드물지 않았습니다.
문제는 SSR이었다
설정 지옥의 정점은 서버 사이드 렌더링이었습니다. 순수 CSR만 한다면 webpack 설정만 잘 하면 됐지만, SSR을 하려는 순간 복잡도가 한 단계 더 올라갔습니다.
왜 SSR이 필요했을까요? 당시 SPA의 한계가 명확했기 때문입니다.
SEO가 약했습니다. 검색 엔진 크롤러는 JavaScript를 제대로 실행하지 못했고, JavaScript로만 렌더링되는 SPA의 콘텐츠는 검색 결과에 잘 노출되지 않았습니다. 블로그, 커머스, 마케팅 사이트 같은 영역에서는 치명적인 문제였습니다.
첫 paint가 느렸습니다. 사용자가 URL을 열면 빈 HTML이 먼저 오고, 그 다음 거대한 JavaScript 번들이 다운로드되고, 파싱되고, 실행되어야 비로소 콘텐츠가 보였습니다. 모바일 환경에서는 이 시간이 수 초씩 걸렸습니다.
소셜 미디어 공유가 제대로 안 됐습니다. Open Graph 메타 태그는 서버가 HTML에 포함시켜야 Facebook, Twitter 같은 크롤러가 읽을 수 있었습니다. SPA는 JavaScript로 메타 태그를 동적으로 넣었지만, 크롤러는 그걸 못 봤습니다.
이 문제들의 해결책은 분명했습니다. 서버에서 HTML을 만들어 보내자. 하지만 React로 SSR을 하려면 위에서 말한 복잡한 과정을 모두 직접 구현해야 했고, 그 결과물도 미묘한 버그가 많았습니다.
Next.js의 등장, Zero Config라는 약속
2016년, Vercel (당시 이름은 Zeit) 의 창업자 Guillermo Rauch가 이끄는 팀이 Next.js를 공개했습니다. 그들이 내세운 설계 원칙은 단순했습니다.
- Zero setup. Use the filesystem as an API, 설정 없이 파일 시스템을 API로 사용한다
- Only JavaScript. Everything is a function, 오직 JavaScript. 모든 것은 함수다
- Automatic server rendering and code splitting, 자동 서버 렌더링과 코드 분할
- Data fetching is up to the developer, 데이터 패칭은 개발자의 몫이다
이 원칙들은 당시 React 생태계가 겪고 있던 문제를 정확히 겨냥했습니다. pages/ 폴더에 React 컴포넌트 파일을 만들기만 하면, 그 파일이 곧 라우트가 되었습니다. Webpack 설정도, Babel 설정도, 서버 설정도 필요 없었습니다.
// pages/about.js, 이게 전부였습니다
import React from "react";
export default () => <h1>About us</h1>;이 파일 하나만 만들면 /about 경로로 서버 사이드 렌더링되는 React 페이지가 자동으로 생성되었습니다. 개발자는 컴포넌트 코드를 작성하는 일에만 집중할 수 있었습니다.
당시로서는 혁신이었습니다. 복잡한 설정을 걷어내고, 개발자 경험을 근본적으로 개선한 프레임워크였습니다.
데이터 패칭의 진화
초기 Next.js의 데이터 패칭은 getInitialProps라는 정적 메서드를 사용했습니다.
import React from "react";
export default class extends React.Component {
static async getInitialProps() {
const res = await fetch("https://api.example.com/user/123");
const data = await res.json();
return { username: data.profile.username };
}
render() {
return <h1>{this.props.username}</h1>;
}
}getInitialProps는 서버와 클라이언트 양쪽에서 실행되었습니다. 서버 렌더링 시에는 서버에서, 클라이언트 라우팅 시에는 클라이언트에서 실행되어 props를 채웠습니다. 단순하고 이해하기 쉬웠지만 한계가 있었습니다.
- 코드가 어디서 실행되는지 명확하지 않았습니다. 같은 함수가 서버와 클라이언트에서 실행되니, 서버 전용 라이브러리를 쓰기도 애매하고, 환경 변수 접근도 복잡했습니다
- 자동 정적 최적화 (Automatic Static Optimization) 가 어려웠습니다.
getInitialProps가 있는 페이지는 무조건 SSR되어야 했는데, 실제로는 데이터가 정적이어서 빌드 시에 한번만 렌더링해도 되는 경우가 많았습니다
이 한계를 해결하기 위해 Next.js 9.3 (2020년 3월) 에서 **getServerSideProps**와 **getStaticProps**가 도입되었습니다.
// 빌드 시에 한번만 실행되어 정적 HTML 생성
export async function getStaticProps() {
return { props: { data: "..." } };
}
// 요청마다 서버에서 실행되어 동적 SSR
export async function getServerSideProps() {
return { props: { data: "..." } };
}이 API는 서버 전용이라는 것이 명확했습니다. 개발자는 "이 페이지를 정적으로 할지 동적으로 할지"를 명시적으로 선택할 수 있었고, Next.js는 빌드 시에 각 페이지를 최적의 방식으로 처리할 수 있었습니다.
이때 revalidate 옵션으로 **Incremental Static Regeneration (ISR)**의 베타가 함께 도입되었고, Next.js 9.5 (2020년 7월) 에서 안정화되었습니다. "정적이지만 일정 주기로 재생성"이라는 새로운 카테고리였습니다.
export async function getStaticProps() {
return {
props: { data: "..." },
revalidate: 60, // 60초마다 재생성
};
}ISR은 Next.js가 처음 제시한 독창적인 개념이었습니다. 정적과 동적 사이의 스펙트럼을 넓혔고, 많은 앱이 캐싱과 성능의 균형을 맞출 수 있게 되었습니다.
그런데 Pages Router는 한계에 부딪혔다
Next.js는 2016년부터 pages/ 디렉터리 기반의 라우팅 (Pages Router) 을 사용했습니다. 파일 하나가 곧 라우트 하나가 되는 단순한 모델이었습니다. 작은 앱에서는 완벽하게 동작했지만, 앱이 커지면서 한계가 드러나기 시작했습니다.
레이아웃 공유가 어려웠습니다. 여러 페이지가 같은 헤더, 사이드바, 푸터를 공유해야 하는 경우, 개발자는 _app.js라는 특수 파일에 전역 레이아웃을 직접 작성해야 했습니다. 그런데 이 방식으로는 중첩 레이아웃 (예: 대시보드 전용 레이아웃 안에 설정 페이지 레이아웃) 을 표현하기 어려웠습니다.
페이지 전환 시 레이아웃이 리렌더링되었습니다. Pages Router에서는 페이지를 이동할 때 공통 레이아웃까지 함께 리렌더링되었습니다. 사이드바의 스크롤 위치나 입력 상태가 페이지를 이동할 때마다 초기화되는 문제가 있었습니다.
로딩/에러 상태 처리가 개발자 책임이었습니다. 데이터를 기다리는 동안 로딩 UI를 보여주려면 useState 와 useEffect로 상태를 관리해야 했고, 에러가 나면 ErrorBoundary를 직접 설정해야 했습니다.
번들 크기를 제어하기 어려웠습니다. 페이지 단위로만 코드 분할이 일어났고, 컴포넌트 단위로 "이 부분은 클라이언트에서, 저 부분은 서버에서" 같은 세밀한 제어는 불가능했습니다.
이런 한계들은 작은 불편으로 시작해서, 앱이 커질수록 큰 문제가 되었습니다. Next.js 팀은 근본적인 개편이 필요하다고 판단했습니다.
App Router의 등장
**Next.js 13 (2022년 10월)**에서 App Router가 베타로 공개되었습니다. 단순한 API 변경이 아니라 라우팅 시스템 전체를 다시 설계한 것이었습니다.
App Router는 app/ 디렉터리를 사용하며, 각 라우트 폴더에 특수 파일들을 두는 방식입니다.
page.tsx, 그 라우트에 렌더링될 UIlayout.tsx, 해당 라우트와 하위 라우트가 공유할 레이아웃loading.tsx, Suspense 경계에 연결된 로딩 UIerror.tsx, Error Boundary에 연결된 에러 UInot-found.tsx, 404 UI
이 단순한 파일 규칙이 Pages Router의 여러 한계를 한번에 해결했습니다. 중첩 레이아웃은 중첩된 폴더 구조로 자연스럽게 표현되고, 로딩과 에러 상태는 파일만 만들면 자동으로 처리됩니다. 그리고 레이아웃은 페이지 이동 시 리렌더링되지 않습니다.
하지만 App Router의 진짜 핵심은 라우팅 구조가 아니었습니다. 바로 React Server Components라는 새로운 렌더링 모델 위에 구축되었다는 점입니다.
**Next.js 13.4 (2023년 5월)**에서 App Router는 안정화되었고, 지금은 새 프로젝트의 기본 선택지가 되었습니다.
시리즈에서 다룰 것들
Next.js는 2016년부터 2026년까지 10년 동안 계속 진화해왔습니다. 그 발전의 각 단계에는 "어떤 문제가 있었고, 어떻게 해결했는가"라는 명확한 이유가 있습니다. 이 시리즈는 그 이유들을 따라가며 Next.js의 현재 모습을 이해해보는 것을 목표로 합니다.
- 2편 App Router와 파일 기반 라우팅, Pages Router의 한계를 App Router가 어떻게 해결했는지
- 3편 Server Components, 왜 다시 서버로, RSC가 해결한 문제와 클라이언트/서버 경계
- 4편 데이터 패칭과 캐싱의 진화,
getServerSideProps에서fetch확장, Cache Components까지 - 5편 렌더링 전략, SSR, SSG, ISR, PPR, 각 전략이 등장한 이유와 최신 Cache Components 모델
- 6편 Server Actions, mutation의 재발명, 폼과 데이터 변경을 다루는 새로운 방식
- 7편 Next.js 16과 Turbopack, webpack을 대체한 여정과 16.2의 최신 기능
다음 글에서는 App Router의 파일 기반 라우팅이 Pages Router의 어떤 구체적인 문제들을 해결했는지, 그리고 왜 그 해결 방식이 자연스러웠는지를 살펴봅니다.