Next.js로 이전해 클라이언트‑서버 워터폴 문제 해결
출처: Dev.to
마이그레이션 이후 성능 역설
You’ve done it. You moved your React application from Vite to Next.js to take advantage of Server Components, better SEO, and optimized routing.
React 애플리케이션을 Vite에서 Next.js로 마이그레이션해 서버 컴포넌트 활용, 더 나은 SEO, 최적화된 라우팅을 위해 이동했습니다.
하지만 Chrome의 네트워크 탭을 열면 익숙하고 답답한 모습—단계식으로 올라가는 요청들의 계단—을 보게 됩니다.
서버 전용으로 설계된 프레임워크로 전환했음에도 불구하고 여전히 클라이언트-서버 워터폴을 겪고 있을 수 있습니다. 이는 애플리케이션이 다음 요청을 시작하기 전에 하나의 네트워크 요청이 끝날 때까지 기다리는 경우 발생합니다.
이 가이드에서는 마이그레이션 후 워터폴이 왜 지속되는지와 Next.js App Router 아키텍처를 활용해 데이터를 realmente 재구성하는 방법을 살펴봅니다.
일반적인 Vite 기반 Single Page Application (SPA)에서는 데이터 가져오기가 보통 useEffect 훅이나 TanStack Query와 같은 라이브러리 안에 포함됩니다.
컴포넌트 A가 마운트되고 fetchUser를 트리거합니다.
컴포넌트 A가 로드 완료 후 Component B를 렌더링합니다.
그다음 Component B는 fetchOrders를 트리거합니다.
이것은 전형적인 워터폴입니다. 직접 Next.js 클라이언트 컴포넌트(‘use client’)로 코드를 마이그레이션하면 동작은 동일합니다. 브라우저에서 첫 번째 데이터 바이트가 요청되기 전에 large JavaScript 번들을 전달해야 하므로 문제가 됩니다.
가장 쉬운 해결책은 useEffect에서 비동기 Server Component으로 fetch 로직을 이동하는 것입니다. 서버에서 데이터를 가져오므로 워터폴이 데이터 소스(데이터베이스 또는 API)로 더 가깝게 이동하여 모바일 브라우저에서 라운드트립보다 훨씬 낮은 지연 시간을 얻을 수 있습니다.
// 이전: Vite 스타일의 클라이언트 컴포넌트
'use client';
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/stats').then(res => res.json()).then(setData);
}, []);
if (!data) return;
return;
}
// 이후: Next.js 스타일의 서버 컴포넌트
async function Dashboard() {
const res = await fetch('https://api.example.com/stats');
const data = await res.json();
return;
}
마이그레이션 중 흔한 실수는 클라이언트 워터폴을 서버 워터폴로 전환하는 것입니다.
여러 개의 독립적인 데이터 요구 사항이 있다면 한 번에 하나씩 기다리지 마세요.
// ❌ Slow: Sequential
const user = await getUser();
const posts = await getPosts(); // Doesn't start until getUser finishes
// ✅ Fast: Parallel
const [user, posts] = await Promise.all([
getUser(),
getPosts()
]);
Promise.all을 사용하면 두 요청이 동시에 시작됩니다. 초기 마이그레이션 구조를 ViteToNext.AI와 같은 도구로 자동화했다면, 상위 수준 페이지 컴포넌트에서 논리적으로 가능한 경우 병렬 fetching이 구현되어 있는지 수동으로 검토해야 합니다.
useHook과 Suspense 활용
때때로 데이터는 페이지 전체 렌더링을 차단하지 않으면서 가능한 한 일찍 가져오고 싶습니다. 여기에서 스트리밍이 등장합니다.
Page 컴포넌트의 최상위 수준에서 데이터를 await하지 않고, Promise를 Client Component에 전달하고 React의 새로운 use 훅(use) 혹은 Server Component을 Suspense 바운더리 안에 감싸세요.
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
{/* ... */}
</Suspense>
);
}
앱 라우터에서는 fetch 호출이 자동으로 메모화됩니다. 레이아웃과 페이지 모두에서 동일한 데이터를 필요로 한다면 Next.js는 단일 요청만 수행합니다.
하지만 fetch가 아닌 요청(예: ORM을 사용한 데이터베이스 호출)에서는 React의 cache 함수를 사용해 컴포넌트 트리 전체에 걸친 중복 워터폴을 방지할 수 있습니다.
import { cache } from 'react';
export const getGlobalUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } });
});
Vite에서 Next.js로 마이그레이션하는 것은 첫 번째 단계일 뿐입니다. 클라이언트-서버 워터폴을 realmente 해결하려면 ‘컴포넌트 기반 데이터 가져오기’ 마인드셋을 ‘라우트 기반 데이터 가져오기’로 전환해야 합니다.
- 데이터를 소스에 가깝게(fetch)하기 위해 Server Component를 사용하세요.
- 독립적인 요청에는 Promise.all을 사용하세요.
- Suspense와 스트리밍을 활용해 UI가 반응성 있게 유지하세요.
- 반복적인 데이터베이스 호출을 방지하기 위해 메모화를 사용하세요.
이러한 패턴을 따르면 느린 SPA를 고성능 서버 최적화된 애플리케이션으로 변환하여 사용자에게 훨씬 더 나은 경험을 제공할 수 있습니다.
프레임워크 전환 자동화를 위한 추가 읽기: ViteToNext.AI