Next.js를 공부하면서 인상 깊었던 부분은 서버 컴포넌트 기반의 데이터 페칭과 Streaming 구조였다.
기존 SSR의 한계를 어떻게 보완하는지, 그리고 React 18의 Suspense와 어떻게 연결되는지 정리해본다.
SSR(Server Side Rendering)은 서버에서 완성된 HTML을 생성한 뒤 클라이언트에 전달하는 방식이다.
SSR의 동작 과정은 다음과 같다.
문제는 모든 데이터 fetching이 끝나야 HTML을 전송할 수 있다는 점이다.
API 응답이 느리면 사용자는 아무 화면도 보지 못한 채 기다려야 한다.
즉, SSR은 SEO와 초기 렌더링에는 유리하지만, 데이터 의존성이 큰 페이지에서는 체감 속도가 느려질 수 있다.
이 문제를 보완하는 방식이 바로 Streaming이다.
Streaming은 서버가 HTML을 한 번에 완성해서 보내는 것이 아니라,
준비된 부분부터 작은 청크(chunk) 단위로 나누어 점진적으로 전송하는 방식이다.
기존 SSR과 비교하면 다음과 같다.
HTML은 원래 네트워크를 통해 청크 단위로 전송될 수 있다.
React 18은 이 특성을 활용해 부분적으로 HTML을 생성하고, 준비된 영역부터 먼저 전송할 수 있도록 개선되었다.
이로 인해 다음과 같은 지표 개선을 기대할 수 있다.
특히 여러 API 호출이 동시에 일어나는 Dynamic Page에서 효과적이다.
기본 React(CSR)는 서버에서 거의 비어있는 HTML 파일 하나만 전달한다.
실제 렌더링은 브라우저에서 JavaScript를 실행한 뒤 시작된다.
즉,
Streaming은 서버에서 HTML을 생성하는 SSR 환경이 전제 조건이다.
따라서 CSR 기반 React에서는 기본적으로 Streaming을 사용할 수 없다.
일반적인 React에서는 클라이언트에서 API를 호출한다.
따라서 로딩 상태를 직접 관리해야 한다.
export default function HomePage() {
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(true);
async function getMovies() {
const response = await fetch(API_URL);
const json = await response.json();
setMovies(json);
setIsLoading(false);
}
useEffect(() => {
getMovies();
}, []);
return (
<div>
{isLoading ? (
<h1>Loading...</h1>
) : (
movies.map((movie) => (
<Movie key={movie.id} {...movie} />
))
)}
</div>
);
}
이 방식의 특징은 다음과 같다.
API 호출이 많아질수록 상태 관리 코드가 늘어나고 복잡도가 증가한다.
Next.js 14(App Router)는 React 18의 Streaming + Suspense를 적극적으로 활용한다.
핵심 특징은 다음과 같다.
즉, 로딩 상태를 직접 관리하지 않아도 되는 구조다.
page.tsx와 동일한 폴더에 loading.tsx를 생성하면,
해당 페이지가 로딩 중일 때 자동으로 fallback UI가 표시된다.
async function getMovies() {
const response = await fetch(API_URL);
return response.json();
}
export default async function HomePage() {
const movies = await getMovies();
return (
<div>
{movies.map((movie) => (
<Movie key={movie.id} {...movie} />
))}
</div>
);
}
export default function Loading() {
return <h2>Loading...</h2>;
}
직접 로딩 상태를 관리할 필요가 없다.
Streaming 기반 구조 덕분에 코드가 훨씬 단순해진다.
특정 컴포넌트만 지연 렌더링하고 싶다면 Suspense를 활용할 수 있다.
<Suspense fallback={<Loading />}>
<MovieList />
</Suspense>
여러 API 호출이 있을 경우,
먼저 완료된 컴포넌트부터 렌더링할 수 있다.
이 구조는 “페이지 전체 대기”가 아니라
“준비된 영역부터 점진적 렌더링”이 가능하게 만든다.
예를 들면: