[Next.js] App Router vs Pages Router: 데이터 패칭과 렌더링 방식 완벽 정리
이번에 새로 참여하게 된 프로젝트에서 Next.js라는 프레임워크를 사용하기로 했다. 서버 컴포넌트라는 낯선 개념이 등장하여, 이를 공부하고 정리해보았다.
Next.js에는 구버전인 페이지 라우터 방식과,
Next.js 13부터 새롭게 도입된 앱 라우터 방식이 있다.
컴포넌트를 렌더링할 때
- 페이지 라우터는 SSR(Server Side Rendering) 기반
- 앱 라우터는 RSC(React Server Component) 기반이라고 볼 수 있다.
여기서 중요한 포인트는,
SSR과 RSC 모두 렌더 전에 데이터 요청이 가능하지만,
SSR은 페이지 단위, RSC는 컴포넌트 단위라는 점이다.
페이지 라우터에서의 데이터 패칭 (SSR)
페이지 라우터의 경우, 컴포넌트에서 사용할 데이터를 패치하려면
데이터 요청용 함수와 응답 데이터를 넘겨받아 사용하는 함수를 별도로 작성해야 한다.
예를 들어:
// pages/profile.tsx
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
return { props: { data } };
}
export default function ProfilePage({ data }) {
return <h1>{data.name}</h1>;
}
위 코드처럼, 페이지 컴포넌트 자체는 async가 안 되고,
대신 getServerSideProps를 통해 데이터를 받고 props로 넘겨줘야 한다.
앱 라우터에서의 데이터 패칭 (RSC)
앱 라우터에서는 훨씬 직관적이다.
컴포넌트 내부에서 await로 바로 fetch 요청을 보낼 수 있다.
즉, 컴포넌트가 렌더링되기 전에 서버에서 데이터를 먼저 처리하고,
완성된 데이터를 포함한 HTML을 브라우저에 전달해준다.
예시:
// app/profile/page.tsx
export default async function ProfilePage() {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
return <h1>{data.name}</h1>;
}
굳이 getServerSideProps 같은 거 안 쓰고,
그냥 컴포넌트 안에서 fetch 쓰면 된다.
→ 컴포넌트가 서버에서 실행되기 때문에 가능한 방식.
React(CSR) 방식과의 차이
React에서는 useEffect를 이용해 컴포넌트 내부에서 fetch를 호출할 수 있다.
하지만 이건 실행 위치와 시점이 완전히 다르다.
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <p>로딩 중...</p>;
return <h1>{data.name}</h1>;
}
- 실행 위치: 브라우저(클라이언트)
- 실행 시점: 렌더 후 (마운트된 이후)
- 따라서, 빈 컴포넌트가 먼저 한 번 렌더링되고,
그 후에야 API 요청이 일어나 다시 렌더링된다.
서버 컴포넌트는 실행 시점이 다르다
export default async function Page() {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
return <h1>{data.name}</h1>;
}
이런 서버 컴포넌트는 렌더링 전에 서버에서 실행되고,
브라우저는 그냥 <h1>홍길동</h1>처럼 HTML만 받아서 보여준다.
→ useState, useEffect 같은 JS 코드를 브라우저에 보낼 필요가 없음
→ 번들 사이즈 줄어듦 + 통신 오버헤드 줄어듦 + SEO 최적화
정리
React (useEffect) → 렌더 후 데이터 요청 (CSR)
Client Component (App Router) → 렌더 후 데이터 요청 (CSR)
Server Component (App Router) → 렌더 전 데이터 요청 (RSC)
Page-level SSR (Pages Router) → 렌더 전 데이터 요청 (SSR)
- SSR: 페이지 단위 렌더 전 데이터 요청
- RSC: 컴포넌트 단위 렌더 전 데이터 요청
- CSR: 렌더 후 클라이언트에서 데이터 요청
그럼 동적 기능은?
버튼 클릭, 폼 입력 같은 상호작용은 당연히 브라우저에서 실행돼야 한다.
이럴 때 사용하는 게 바로 클라이언트 컴포넌트('use client')다.
'use client';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>
클릭 수: {count}
</button>;
}
이건 브라우저에서 실행돼야 하니까 자바스크립트 코드가 클라이언트에 전송된다.
최종 정리
Next.js의 App Router는
서버 컴포넌트(RSC)와 클라이언트 컴포넌트를 혼합해서 쓸 수 있는 구조다.
- 서버 컴포넌트: 렌더 전 서버에서 데이터 준비 → JS 번들 없음 → 빠름 + SEO 유리
- 클라이언트 컴포넌트: 렌더 후 브라우저에서 실행되는 동적 기능 담당
즉, App Router에서는 SSR + RSC + CSR을 상황에 맞게 조합해서 사용할 수 있다.