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의 어떤 구체적인 문제들을 해결했는지, 그리고 왜 그 해결 방식이 자연스러웠는지를 살펴봅니다.