현대 웹 애플리케이션의 세계는 속도와 사용자 경험, 그리고 검색 엔진 가시성이라는 세 가지 거대한 축을 중심으로 회전합니다. 사용자는 즉각적인 반응성을 원하고, 비즈니스는 검색 엔진 결과 페이지(SERP) 최상단 노출을 갈망합니다. 이 치열한 전장에서 React는 컴포넌트 기반 아키텍처를 통해 개발자 경험을 혁신했지만, 태생적으로 클라이언트 사이드 렌더링(Client-Side Rendering, CSR)이라는 한계를 안고 있었습니다. 바로 이 지점에서 Next.js와 서버 사이드 렌더링(Server-Side Rendering, SSR)이 구원투수처럼 등장합니다. 이 글은 단순한 기술 설명을 넘어, 왜 SSR이 필요하게 되었는지, Next.js가 React 생태계에서 어떻게 SSR의 표준을 정립했는지, 그리고 이것이 개발자와 비즈니스에 어떤 의미를 갖는지 그 본질을 깊이 파고듭니다.
클라이언트 사이드 렌더링(CSR)의 명과 암
SSR을 이해하기 위해서는 먼저 그 반대편에 있는 CSR의 본질을 정확히 알아야 합니다. React, Vue, Angular와 같은 대부분의 모던 JavaScript 프레임워크는 기본적으로 CSR 방식을 채택합니다. CSR의 작동 방식은 비교적 간단하지만, 그 안에 명확한 장점과 치명적인 단점이 공존합니다.
CSR의 작동 매커니즘: 브라우저에 위임된 렌더링
사용자가 CSR 기반의 웹사이트에 접속하면 다음과 같은 과정이 순차적으로 일어납니다.
- 최초 요청: 사용자의 브라우저가 서버에 페이지를 요청합니다.
- 서버 응답: 서버는 거의 비어있는 HTML 파일과 거대한 JavaScript 번들 파일(예:
app.js)의 경로를 응답으로 보냅니다. 이 HTML 파일은 보통<div id="root"></div>와 같은 뼈대만 가지고 있습니다. - JavaScript 다운로드 및 실행: 브라우저는 응답받은 HTML을 파싱하고, 그 안에 포함된 JavaScript 파일 링크를 발견하여 다운로드를 시작합니다.
- API 요청 및 데이터 수신: 다운로드된 JavaScript 코드가 실행되면서, 화면을 그리는 데 필요한 데이터를 얻기 위해 API 서버에 추가적인 네트워크 요청을 보냅니다.
- 가상 DOM 생성 및 렌더링: API로부터 데이터를 수신하면, JavaScript는 이 데이터를 기반으로 React 코드를 실행하여 가상 DOM(Virtual DOM)을 생성하고, 실제 DOM에 이를 렌더링하여 비로소 사용자에게 의미 있는 화면을 보여줍니다.
이 과정을 텍스트 다이어그램으로 표현하면 다음과 같습니다.
Client (Browser) Server
| |
| 1. Request page (e.g., /products/123) |
|----------------------------------------------->|
| |
| 2. Respond with minimal HTML + JS bundle link |
|<-----------------------------------------------|
| |
| 3. Parse HTML, Request JS bundle |
|----------------------------------------------->|
| |
| 4. Respond with app.js |
|<-----------------------------------------------|
| |
| 5. Execute JS, Oh! I need data! |
| Request data from API (e.g., /api/products/123)
|----------------------------------------------->| (API Server)
| |
| 6. Respond with JSON data |
|<-----------------------------------------------| (API Server)
| |
| 7. Render content to DOM using data. |
| User finally sees the page. |
| |
CSR의 빛: 풍부한 상호작용과 SPA 경험
CSR의 가장 큰 장점은 첫 페이지 로딩이 완료된 이후의 사용자 경험에 있습니다. 모든 렌더링 로직과 코드가 이미 브라우저에 있기 때문에, 페이지 전환이나 사용자 인터랙션이 발생할 때 서버와 통신할 필요 없이(데이터만 비동기적으로 교환) 즉각적으로 화면이 갱신됩니다. 이는 마치 데스크톱 애플리케이션을 사용하는 듯한 부드러운 경험, 즉 단일 페이지 애플리케이션(Single Page Application, SPA)의 핵심입니다. 서버는 초기에 리소스를 전달하고 나면 주로 API 역할에만 집중하므로 서버 부하가 감소하는 효과도 있습니다.
CSR의 그림자: 초기 로딩과 검색 엔진 최적화(SEO)의 악몽
하지만 이 화려한 경험 뒤에는 어두운 그림자가 존재합니다. 바로 '초기 로딩' 문제입니다.
- 느린 초기 로딩 속도 (TTV/TTI): 사용자는 5번 단계가 완료될 때까지 의미 있는 콘텐츠를 전혀 볼 수 없습니다. 화면에는 로딩 스피너나 흰 화면만 덩그러니 놓여있게 됩니다. 이를 TTV(Time to View) 또는 TTI(Time to Interactive)가 느리다고 표현합니다. JavaScript 번들 크기가 크거나, 사용자 네트워크 환경이 느릴수록 이 '깜빡임' 또는 '지연' 현상은 더욱 심각해집니다.
- 검색 엔진 최적화(SEO)의 어려움: Googlebot과 같은 검색 엔진 크롤러는 기본적으로 HTML 문서를 분석하여 페이지의 콘텐츠를 이해합니다. 하지만 CSR 페이지의 초기 HTML은 내용이 거의 비어있습니다. 물론 최근 Googlebot은 JavaScript를 실행하여 동적으로 생성된 콘텐츠를 어느 정도 색인할 수 있지만, 여전히 완벽하지 않으며 모든 검색 엔진이 이를 지원하는 것도 아닙니다. 또한, JavaScript 실행에 실패하거나 시간이 오래 걸리면 검색 엔진은 페이지의 핵심 콘텐츠를 놓칠 수 있어 SEO에 치명적입니다. 비즈니스 관점에서 이는 잠재 고객을 잃는 것과 같습니다.
결론적으로, CSR은 애플리케이션과 같은 높은 상호작용이 필요한 내부 관리 도구나 웹 서비스에는 적합할 수 있지만, 첫인상이 중요하고 검색을 통한 유입이 필수적인 콘텐츠 중심의 웹사이트(뉴스, 블로그, 이커머스 등)에는 근본적인 한계를 드러냅니다.
서버 사이드 렌더링(SSR), 해답의 등장
CSR의 문제를 해결하기 위한 대안으로 등장한 것이 바로 서버 사이드 렌더링(SSR)입니다. SSR은 이름에서 알 수 있듯이, 렌더링의 주체를 클라이언트(브라우저)에서 서버로 옮기는 방식입니다. 이는 웹의 초창기 모델(PHP, JSP, ASP 등)과 유사해 보이지만, 모던 JavaScript 프레임워크와 결합하여 훨씬 더 정교하게 작동합니다.
SSR의 작동 매커니즘: 서버에서 완성되는 첫 화면
SSR 기반의 웹사이트에 사용자가 접속하면 프로세스는 다음과 같이 달라집니다.
- 최초 요청: 사용자의 브라우저가 서버에 페이지를 요청합니다. (CSR과 동일)
- 서버 단 처리:
- 서버(주로 Node.js 환경)는 요청을 받고, 해당 페이지를 렌더링하는 데 필요한 데이터를 자체적으로 API 서버에 요청하거나 데이터베이스에서 직접 조회합니다.
- 가져온 데이터를 기반으로 서버 환경에서 React 코드를 실행하여 렌더링해야 할 컴포넌트를 HTML 문자열로 변환합니다.
- 서버 응답: 서버는 이렇게 완성된, 모든 콘텐츠가 포함된 HTML 파일을 브라우저에 응답으로 보냅니다.
- 초기 렌더링: 브라우저는 즉시 콘텐츠가 채워진 HTML을 받아 사용자에게 보여줍니다. JavaScript가 다운로드되기 전에도 사용자는 페이지의 내용을 읽을 수 있습니다. (매우 빠른 FCP - First Contentful Paint)
- JavaScript 다운로드 및 Hydration: HTML 렌더링과 동시에, 브라우저는 페이지를 동적으로 만들기 위한 JavaScript 번들을 다운로드합니다. 다운로드가 완료되면, JavaScript(React)는 기존에 렌더링된 정적 HTML 위에 이벤트 핸들러 등을 연결하여 상호작용이 가능한 '살아있는' 페이지로 만드는 과정을 거칩니다. 이 과정을 "Hydration(수화)"이라고 합니다.
이를 다이어그램으로 표현하면 CSR과의 차이가 명확해집니다.
Client (Browser) Server (Next.js)
| |
| 1. Request page (e.g., /products/123) |
|----------------------------------------------->|
| |
| | 2. Server fetches data
| | (from API or DB)
| | and renders React
| | components to HTML string.
| |
| 3. Respond with FULLY RENDERED HTML |
| + JS bundle link. |
|<-----------------------------------------------|
| |
| 4. Browser immediately renders the HTML. |
| User sees content instantly! |
| |
| 5. In parallel, browser requests JS bundle. |
|----------------------------------------------->|
| |
| 6. Respond with app.js |
|<-----------------------------------------------|
| |
| 7. JS executes. React "hydrates" the static |
| HTML, attaching event listeners. |
| Page becomes interactive. |
| |
SSR의 진정한 가치: FCP와 SEO
SSR이 제공하는 핵심적인 이점은 다음과 같습니다.
- 빠른 초기 콘텐츠 표시(FCP): 사용자는 더 이상 빈 화면을 보며 기다릴 필요가 없습니다. 서버에서 생성된 HTML이 도착하는 즉시 콘텐츠를 볼 수 있어 체감 성능이 극적으로 향상됩니다. 이는 특히 콘텐츠 소비가 목적인 웹사이트에서 사용자 이탈률을 낮추는 데 결정적인 역할을 합니다.
- 완벽한 검색 엔진 최적화(SEO): 검색 엔진 크롤러는 처음부터 완벽한 콘텐츠가 담긴 HTML 문서를 받게 됩니다. JavaScript 실행 여부와 관계없이 페이지의 모든 텍스트와 메타 태그를 정확하게 분석하고 색인할 수 있습니다. 이는 검색 결과 상위 노출을 위한 필수 조건입니다.
하지만 SSR에도 트레이드오프는 존재합니다. 모든 요청마다 서버에서 렌더링 과정을 거쳐야 하므로 서버 부하가 CSR에 비해 증가합니다. 또한, 서버에서 데이터를 가져와 렌더링하는 시간만큼 첫 바이트를 받는 시간(TTFB, Time To First Byte)이 길어질 수 있습니다. 아키텍처 또한 CSR에 비해 복잡해집니다. 그럼에도 불구하고, 사용자 경험과 비즈니스적 가치 측면에서 SSR이 주는 이점은 이러한 단점을 상쇄하고도 남습니다.
React만으로 SSR을 구현하기 어려운 이유
React는 본질적으로 UI를 렌더링하기 위한 라이브러리일 뿐, 서버 환경이나 라우팅, 데이터 페칭에 대한 규약은 포함하고 있지 않습니다. 이론적으로는 순수 React만으로도 SSR을 구현할 수 있습니다. React는 react-dom/server라는 패키지를 제공하며, 이 패키지의 renderToString 또는 renderToPipeableStream 같은 함수를 사용하면 React 컴포넌트를 HTML 문자열로 변환할 수 있습니다.
하지만 이것은 빙산의 일각에 불과합니다. 실제 프로덕션 수준의 SSR 환경을 구축하려면 개발자가 직접 처리해야 할 복잡한 문제들이 산더미처럼 쌓여있습니다.
- 서버 환경 구축: Express.js나 Koa.js 같은 Node.js 프레임워크를 사용해 직접 웹 서버를 구축하고, 들어오는 모든 요청을 처리할 로직을 작성해야 합니다.
- 라우팅 동기화: 클라이언트 사이드 라우팅(e.g.,
react-router-dom)과 서버 사이드 라우팅 로직을 완벽하게 일치시켜야 합니다. 특정 URL 요청이 들어왔을 때, 서버는 어떤 컴포넌트를 렌더링해야 할지 정확히 알아야 합니다. 이는 매우 까다롭고 오류가 발생하기 쉬운 부분입니다. - 서버 측 데이터 페칭: 컴포넌트를 렌더링하기 전에 필요한 데이터를 서버에서 미리 가져와야 합니다. 각 라우트에 맞는 데이터를 어떻게, 어디서 가져올지에 대한 표준화된 방법이 없으므로 개발자가 직접 규칙을 만들어야 합니다.
- 코드 스플리팅: 대규모 애플리케이션에서는 모든 코드를 하나의 거대한 번들로 만들 수 없습니다. 라우트 기반으로 코드를 분할(Code Splitting)해야 하는데, SSR 환경에서 이를 제대로 작동시키려면 서버에서 렌더링한 컴포넌트에 해당하는 JavaScript 청크만 클라이언트에 보내도록 정교한 설정이 필요합니다.
- 상태 관리와 Hydration: 서버에서 데이터를 페칭하여 렌더링했다면, 이 상태(State)를 클라이언트에도 전달해줘야 합니다. 서버에서 렌더링된 HTML과 클라이언트에서 초기 렌더링된 결과물이 정확히 일치해야 Hydration 과정에서 오류가 발생하지 않습니다. 이를 위해 서버의 최종 상태를 `window` 객체에 담아 HTML에 주입하고, 클라이언트의 스토어(Redux, Zustand 등)가 이 초기 상태를 받아 사용하도록 설정해야 합니다.
- 개발 및 운영 환경 설정: Webpack, Babel 설정, HMR(Hot Module Replacement), 프로덕션 빌드 최적화 등 개발과 배포에 필요한 수많은 도구들을 SSR 환경에 맞게 직접 구성해야 합니다.
이 모든 것을 밑바닥부터 구축하는 것은 엄청난 시간과 노력을 요구하며, 프로젝트의 본질적인 비즈니스 로직 개발에 집중하기 어렵게 만듭니다. 바로 이 고통스러운 지점에서 Next.js가 등장합니다.
Next.js, React SSR의 표준을 제시하다
Next.js는 Vercel이 만든 React 기반의 "오피니언 프레임워크(Opinionated Framework)"입니다. 이는 '의견이 있다'는 뜻으로, 프레임워크가 개발자에게 특정 문제(예: SSR)를 해결하는 최선의 방법을 정해놓고 강력하게 제안한다는 의미입니다. 개발자는 복잡한 SSR 설정의 수렁에 빠지는 대신, Next.js가 제공하는 잘 닦인 길을 따라가기만 하면 됩니다.
Next.js는 위에서 언급한 SSR 구현의 모든 어려움을 아름답게 추상화하여 해결합니다.
- 파일 시스템 기반 라우팅:
pages디렉터리에 파일을 생성하면(e.g.,pages/about.js) 그 파일 경로가 곧 URL(/about)이 됩니다. 서버와 클라이언트 라우팅이 자동으로 동기화되어 개발자는 라우팅 설정에 대해 고민할 필요가 없습니다. - 표준화된 데이터 페칭: 페이지 단위로 서버 측에서 데이터를 어떻게 가져올지에 대한 명확한 API, 즉 `getServerSideProps`를 제공합니다.
- 자동 코드 스플리팅: 페이지별로 코드가 자동으로 분할되어, 사용자가 특정 페이지에 접속할 때 해당 페이지만의 JavaScript 코드만 다운로드합니다.
- 최적화된 개발 및 빌드 환경: HMR을 지원하는 개발 서버, 프로덕션 최적화 빌드 등 모든 것이 기본적으로 내장되어 있습니다.
npm run dev,npm run build,npm run start세 가지 명령어로 모든 것이 해결됩니다.
핵심 API: `getServerSideProps`
Next.js에서 SSR을 구현하는 가장 대표적인 방법은 `pages` 디렉터리 내의 페이지 컴포넌트 파일에서 `getServerSideProps` 함수를 `export`하는 것입니다.
이 함수의 특징은 다음과 같습니다.
- 서버에서만 실행: 이 함수 내부의 코드는 오직 서버 사이드에서만 실행됩니다. 브라우저(클라이언트)에는 절대 전송되지 않습니다. 따라서 이 함수 안에서는 파일 시스템에 직접 접근하거나, 데이터베이스에 직접 쿼리를 날리거나, 외부에 노출되면 안 되는 API 키를 사용하는 등의 작업이 안전하게 가능합니다.
- 페이지 렌더링 전 실행: 사용자가 해당 페이지에 대한 요청을 보낼 때마다, Next.js는 페이지 컴포넌트를 렌더링하기에 앞서 `getServerSideProps`를 먼저 실행합니다.
- Props 전달: 이 함수가 객체를 반환하면(`return { props: { ... } }`), 이 객체 내부의 `props` 값이 페이지 컴포넌트의 `props`로 주입됩니다.
다음은 `getServerSideProps`를 사용한 간단한 예시입니다. 특정 상품의 상세 정보를 보여주는 페이지를 만든다고 가정해 봅시다.
// pages/products/[id].js
// 이 페이지 컴포넌트는 서버에서 전달받은 product 데이터를 props로 받습니다.
function ProductDetailPage({ product }) {
if (!product) {
return <div>상품 정보를 불러오는 데 실패했습니다.</div>;
}
return (
<div>
<h1>{product.name}</h1>
<p>가격: {product.price}원</p>
<p>설명: {product.description}</p>
</div>
);
}
// 이 함수는 매 요청마다 서버에서 실행됩니다.
export async function getServerSideProps(context) {
// context 객체에는 요청에 대한 다양한 정보가 담겨 있습니다.
// params에는 [id].js와 같은 다이나믹 라우트의 파라미터가 들어있습니다.
const { params } = context;
const { id } = params;
try {
// 외부 API를 호출하여 상품 데이터를 가져옵니다.
// 이 fetch는 Node.js 환경에서 실행됩니다.
const res = await fetch(`https://api.example.com/products/${id}`);
// 데이터가 없는 경우를 처리합니다. notFound: true를 반환하면 404 페이지를 보여줍니다.
if (!res.ok) {
return { notFound: true };
}
const product = await res.json();
// 가져온 데이터를 props 객체에 담아 반환합니다.
// 이 props가 ProductDetailPage 컴포넌트로 전달됩니다.
return {
props: {
product,
},
};
} catch (error) {
console.error('데이터를 가져오는 중 오류 발생:', error);
// 에러 발생 시 처리
return {
props: {
product: null,
},
};
}
}
export default ProductDetailPage;
위 코드에서 사용자가 `/products/123` URL에 접속하면, Next.js 서버는 다음 단계를 수행합니다.
- URL을 분석하여 `id`가 `123`임을 파악합니다.
- `getServerSideProps` 함수를 `context.params.id`에 `123`을 담아 실행합니다.
- 함수 내부에서 `https://api.example.com/products/123`으로 API 요청을 보냅니다.
- 응답받은 JSON 데이터를 `product` 변수에 저장합니다.
- `{ props: { product: { ... } } }` 형태의 객체를 반환합니다.
- Next.js는 이 `product` 데이터를 `ProductDetailPage` 컴포넌트의 `props`로 전달하여 서버에서 HTML을 렌더링합니다.
- 완성된 HTML을 사용자 브라우저로 전송합니다.
이 모든 복잡한 과정이 단 하나의 `getServerSideProps` 함수로 깔끔하게 추상화되었습니다. 이것이 바로 Next.js가 React 개발자들에게 SSR을 대중화시킨 핵심적인 이유입니다.
SSR을 넘어: Next.js의 진화하는 렌더링 전략
Next.js는 SSR로 시작했지만, 여기에 머무르지 않고 웹 애플리케이션의 다양한 요구사항에 맞춰 렌더링 전략을 지속적으로 발전시켜왔습니다. SSR이 모든 상황에 대한 만병통치약은 아니기 때문입니다. 때로는 더 빠르고 효율적인 방법이 필요합니다.
정적 사이트 생성 (Static Site Generation, SSG)
블로그 게시물, 제품 소개 페이지, 마케팅 랜딩 페이지처럼 내용이 자주 바뀌지 않는 페이지가 있습니다. 이런 페이지를 매번 요청 시마다 서버에서 렌더링하는 것은 비효율적입니다. 이럴 때 사용하는 것이 바로 SSG입니다.
SSG는 `getServerSideProps` 대신 `getStaticProps` 함수를 사용합니다. `getStaticProps`는 빌드 타임(build time)에 단 한 번만 실행됩니다. 빌드 과정에서 데이터를 미리 가져와 각 페이지를 HTML 파일로 생성해두고, 사용자가 요청하면 이미 만들어진 HTML 파일을 즉시 제공합니다. 이는 CDN(Content Delivery Network)에 캐싱하기에 이상적이며, 서버 부하 없이 가장 빠른 속도를 보장합니다.
// pages/posts/[slug].js
// ... Post 컴포넌트 ...
// 빌드 시점에 실행됩니다.
export async function getStaticProps({ params }) {
const post = await getPostData(params.slug); // 빌드 시점에 데이터를 가져옴
return {
props: {
post,
},
};
}
// 다이나믹 라우트의 경우, 빌드 시점에 어떤 경로들을 미리 생성할지 알려줘야 합니다.
export async function getStaticPaths() {
const paths = getAllPostSlugs(); // 모든 포스트의 slug 목록을 가져옴
return {
paths, // 예: [ { params: { slug: 'post-1' } }, { params: { slug: 'post-2' } } ]
fallback: false, // paths에 없는 경로는 404 처리
};
}
점진적 정적 재생성 (Incremental Static Regeneration, ISR)
SSG는 매우 빠르지만, 콘텐츠가 업데이트되면 사이트 전체를 다시 빌드해야 하는 단점이 있습니다. ISR은 SSG의 장점을 유지하면서 이 문제를 해결합니다. `getStaticProps`에서 `revalidate` 옵션을 설정하면, 지정된 시간(초)이 지난 후 첫 번째 사용자의 요청이 있을 때 백그라운드에서 페이지를 다시 생성합니다. 다른 사용자들은 일단 기존의 캐시된 페이지를 보고, 재생성이 완료된 후부터 새로운 페이지를 보게 됩니다.
export async function getStaticProps() {
const data = await fetchSomeData();
return {
props: {
data,
},
// 60초마다 페이지를 재생성할 기회를 줍니다.
revalidate: 60,
};
}
렌더링 전략 비교
Next.js가 제공하는 주요 렌더링 전략을 표로 정리하면 다음과 같습니다.
| 전략 | 데이터 페칭 시점 | 주요 장점 | 주요 단점 | 사용 사례 |
|---|---|---|---|---|
| CSR (Client-Side Rendering) | 클라이언트에서 (useEffect) | 풍부한 상호작용, SPA 경험 | 느린 초기 로딩, SEO 취약 | 관리자 대시보드, 웹 앱 |
| SSR (Server-Side Rendering) | 서버에서 (매 요청 시) | 뛰어난 SEO, 빠른 FCP | 서버 부하, 느릴 수 있는 TTFB | 사용자 맞춤형 뉴스피드, 이커머스 상세 |
| SSG (Static Site Generation) | 서버에서 (빌드 시) | 가장 빠른 속도, 서버 부하 없음 | 데이터 변경 시 재빌드 필요 | 블로그, 문서, 마케팅 페이지 |
| ISR (Incremental Static Regen.) | 서버에서 (주기적 재생성) | SSG의 속도 + 데이터 최신성 | 일부 사용자는 이전 데이터를 볼 수 있음 | 준-실시간 데이터가 필요한 대시보드, 소셜 미디어 피드 |
App Router와 서버 컴포넌트: SSR의 새로운 패러다임
Next.js 13부터 도입된 App Router는 React 서버 컴포넌트(React Server Components, RSC)라는 개념을 중심으로 렌더링 패러다임을 한 단계 더 진화시켰습니다. 기존의 SSR이 '페이지' 단위로 서버에서 렌더링했다면, 이제는 '컴포넌트' 단위로 렌더링 위치(서버 또는 클라이언트)를 결정할 수 있게 되었습니다.
서버 컴포넌트의 본질
App Router 내에서 생성되는 컴포넌트는 기본적으로 서버 컴포넌트입니다. 서버 컴포넌트의 혁신적인 특징은 다음과 같습니다.
- 서버에서만 렌더링: 서버 컴포넌트는 빌드 시 또는 요청 시에 오직 서버에서만 실행되고 렌더링됩니다. 최종 결과물은 HTML과 유사한 정적인 형태로 클라이언트에 전달됩니다.
- 제로 번들 사이즈: 서버 컴포넌트의 코드는 클라이언트의 JavaScript 번들에 포함되지 않습니다. 이는 클라이언트가 다운로드해야 할 JavaScript의 양을 획기적으로 줄여 초기 로딩 성능을 극대화합니다.
- 직접적인 데이터 접근: 서버 컴포넌트는 `async/await`를 직접 사용할 수 있습니다. `getServerSideProps`나 `getStaticProps` 같은 별도의 API 없이, 컴포넌트 내에서 바로 데이터베이스에 접근하거나 비동기 작업을 처리할 수 있습니다.
// app/products/[id]/page.js
// 이것은 서버 컴포넌트입니다.
async function getProductData(id) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
// 컴포넌트 함수 자체가 async가 될 수 있습니다!
export default async function ProductPage({ params }) {
const product = await getProductData(params.id);
return (
{product.name}
{/* 이 컴포넌트는 서버에서 렌더링되어 정적인 UI만 클라이언트로 전달됩니다. */}
);
}
클라이언트 컴포넌트와의 공존
물론 `useState`, `useEffect`와 같은 훅을 사용하거나, `onClick`과 같은 이벤트 핸들러를 통해 사용자와 상호작용해야 하는 컴포넌트도 필요합니다. 이런 컴포넌트들은 파일 상단에 'use client' 지시어를 추가하여 클라이언트 컴포넌트로 만들 수 있습니다. 클라이언트 컴포넌트는 기존의 React 컴포넌트처럼 작동하며, 코드가 클라이언트 번들에 포함됩니다.
App Router의 핵심은 서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 품을 수 있다는 점입니다. 이를 통해 개발자는 페이지의 정적인 부분(헤더, 푸터, 콘텐츠 영역)은 서버 컴포넌트로 만들어 성능을 최적화하고, 동적인 상호작용이 필요한 부분(좋아요 버튼, 댓글 입력창)만 클라이언트 컴포넌트로 분리하여 '최소한의 JavaScript'만 클라이언트에 보낼 수 있습니다. 이는 SSR의 개념을 더욱 세분화하고 정교하게 만든, 진정한 하이브리드 렌더링의 시작이라고 할 수 있습니다.
SSR 선택의 기준: 언제, 왜 사용해야 하는가?
Next.js가 다양한 렌더링 전략을 제공하는 만큼, 개발자는 이제 어떤 전략을 선택할지에 대한 행복한 고민에 빠지게 됩니다. 선택의 기준은 기술 자체가 아니라, 만들고자 하는 서비스의 특성과 비즈니스 요구사항에 있어야 합니다.
SSR이 빛을 발하는 경우
- 콘텐츠가 사용자별로 동적이거나 매우 자주 변경될 때: 개인화된 대시보드, 실시간 주식 정보, 소셜 미디어 피드처럼 모든 사용자에게 다른 내용을 보여주거나 데이터가 수시로 바뀌는 페이지는 매 요청마다 새로운 HTML을 생성하는 SSR이 적합합니다.
- SEO가 절대적으로 중요할 때: 뉴스 기사, 이커머스 상품 상세 페이지, 블로그 등 검색을 통한 유입이 비즈니스의 성패를 좌우하는 경우, SSR은 가장 확실한 SEO 보장 수단입니다.
- 항상 최신 데이터를 보여줘야 할 때: 항공편 예약 현황이나 좌석 정보처럼 사용자가 보는 데이터가 항상 최신 상태여야 하는 경우, 요청 시 데이터를 가져오는 SSR이 SSG나 ISR보다 안전합니다.
SSR 대신 다른 전략을 고려해야 할 경우
- 정적인 콘텐츠가 대부분일 때: 회사 소개 페이지, 캠페인 랜딩 페이지, 개인 블로그 등 내용이 거의 바뀌지 않는다면, 가장 빠른 속도를 제공하는 SSG가 최적의 선택입니다.
- SEO가 중요하지 않고 상호작용이 극도로 많을 때: Figma나 Google Docs와 같은 웹 기반 애플리케이션, 또는 회사의 내부 관리자 페이지처럼 검색 노출이 필요 없고 복잡한 상태 관리가 중요한 경우, 전통적인 CSR(SPA) 방식이 더 효율적일 수 있습니다. (Next.js 내에서도 클라이언트 컴포넌트만으로 페이지를 구성할 수 있습니다.)
- 서버 비용과 관리 부담을 최소화하고 싶을 때: SSR은 요청을 처리할 Node.js 서버가 항상 실행 중이어야 합니다. 반면 SSG로 빌드된 결과물은 정적 파일 호스팅(Vercel, Netlify, S3 등)을 통해 매우 저렴하고 간단하게 배포할 수 있습니다.
결국, 정답은 없습니다. 성공적인 애플리케이션은 페이지의 특성에 따라 SSR, SSG, CSR을 적절히 혼합하여 사용합니다. Next.js의 위대함은 바로 이 모든 선택지를 하나의 프레임워크 안에서 유연하게 조합할 수 있도록 해준다는 데 있습니다.
결론: 렌더링 전략은 비즈니스 전략이다
서버 사이드 렌더링(SSR)은 단순히 기술적인 선택의 문제가 아닙니다. 그것은 사용자가 우리 서비스를 어떻게 처음 만나는지, 검색 엔진이 우리 비즈니스를 어떻게 평가하는지를 결정하는 핵심적인 '전략'입니다. CSR이 제공하는 매끄러운 인터랙션의 세계와 SSR이 보장하는 빠른 첫인상 및 가시성 사이에서, Next.js는 개발자들이 더 이상 양자택일의 굴레에 갇히지 않도록 해방시켜 주었습니다.
Next.js는 복잡한 SSR 설정을 추상화하여 React 개발의 생산성을 극대화했고, SSG, ISR, 그리고 최신 서버 컴포넌트에 이르기까지 웹의 요구사항에 맞춰 끊임없이 진화해왔습니다. 이제 개발자들은 애플리케이션의 각 부분에 가장 적합한 렌더링 방식을 마치 레고 블록처럼 조합하여, 성능과 사용자 경험, 그리고 비즈니스 목표라는 세 마리 토끼를 모두 잡을 수 있는 강력한 도구를 손에 쥐게 되었습니다.
따라서 Next.js와 React를 사용한 현대 웹 개발에서 '어떤 렌더링 전략을 사용할 것인가'라는 질문은 '우리의 사용자와 비즈니스에 가장 중요한 가치는 무엇인가'라는 질문과 동의어가 되었습니다. 기술의 본질을 이해하고 그에 맞는 최적의 전략을 선택하는 것, 이것이 바로 성공적인 웹 프로덕트를 만드는 개발자의 핵심 역량일 것입니다.