Next.js 데이터 가져오기 순서: 순차와 병렬, 그리고 Suspense 위치
서버 컴포넌트에서 데이터를 읽는 코드는 이렇게 생깁니다.
export default async function ArtistPage({ params }) {
const { username } = await params;
const artist = await getArtist(username);
const albums = await getAlbums(username);
return <Profile artist={artist} albums={albums} />;
}await 을 두 번 쓴 것뿐인데, 두 조회가 각각 300ms 라면 이 페이지는 600ms 를 기다립니다. 두 조회는 서로를 필요로 하지 않는데도 줄을 섭니다.
서버 컴포넌트니까 빠르다는 생각
서버 컴포넌트에서 데이터베이스를 직접 읽으면 API 왕복이 사라지므로 빨라진다고 이해하기 쉽습니다. 실제로 왕복 하나는 사라집니다. 하지만 한 컴포넌트 안에서 await 을 연달아 쓰면, 사라진 왕복보다 큰 대기가 그 자리에 생깁니다.
데이터 가져오기 문서는 이 구분을 순차와 병렬로 나눠 설명합니다. 그리고 한 가지를 먼저 짚습니다. 레이아웃과 페이지는 기본적으로 병렬로 렌더링되므로 각 세그먼트는 가능한 한 빨리 데이터를 가져오기 시작합니다. 문제는 세그먼트 사이가 아니라 한 컴포넌트 안입니다.
However, within any component, multiple
async/awaitrequests can still be sequential if placed after the other.
서로 필요 없는 조회는 함께 시작한다
앞의 코드에서 앨범 조회는 아티스트 정보를 필요로 하지 않습니다. 그렇다면 두 요청을 먼저 시작해 두고 함께 기다리면 됩니다.
const artistData = getArtist(username); // await 하지 않는다
const albumsData = getAlbums(username);
const [artist, albums] = await Promise.all([artistData, albumsData]);문서의 설명대로 요청은 함수를 부르는 순간 시작됩니다. await 은 결과를 기다리는 시점일 뿐입니다. 이 차이를 알고 나면 코드를 읽는 방식이 바뀝니다. await 이 연달아 있으면 「여기서 줄을 서는가」를 먼저 봅니다.
Promise.all 은 하나가 실패하면 전체가 실패한다는 점도 함께 생각해야 합니다. 일부가 실패해도 나머지를 보여 주고 싶다면 Promise.allSettled 를 쓰거나, 아래의 스트리밍으로 나눕니다.
의존하는 조회는 순차가 맞다
모든 조회를 병렬로 만들 수는 없습니다. 플레이리스트를 가져오려면 아티스트 id 가 먼저 필요한 경우처럼, 뒤의 요청이 앞의 결과를 쓰는 구조가 있습니다.
문서는 이때 뒤의 조회를 별도 컴포넌트로 빼고 <Suspense> 로 감싸는 방법을 보여 줍니다.
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>불러오는 중</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
);아티스트 이름은 먼저 보이고, 플레이리스트는 준비되는 대로 채워집니다. 다만 문서는 한계도 적습니다. 이 구조에서도 페이지는 아티스트 조회를 기다린 뒤에야 무언가를 보여 줍니다. 첫 화면을 더 빨리 내보내려면 페이지 전체를 감싸는 경계, 즉 loading.js 를 함께 둡니다.
그리고 순차 구조에서는 첫 요청이 전부를 막는다는 점을 문서가 경고합니다. 첫 조회가 느리면 그 뒤의 스트리밍도 늦게 시작됩니다. 더 줄일 수 없다면 그 결과를 캐시하는 쪽을 검토하라고 안내합니다.
경계를 데이터 가까이 두기
loading.js 만으로 충분하지 않은 이유도 같은 문서에 있습니다.
while
loading.jsworks well for streaming route segments, using<Suspense>closer to the runtime or uncached data access is recommended.
loading.js 는 페이지 전체를 한 덩어리로 감쌉니다. 페이지의 90% 가 캐시된 내용이고 10% 만 매번 새로 읽어야 한다면, 그 10% 때문에 전체가 스켈레톤이 됩니다. 느린 조회를 하는 컴포넌트 가까이에 <Suspense> 를 두면 나머지는 즉시 보입니다.
이 판단은 Suspense 글에서 본 「무엇을 함께 보여 줄 것인가」와 같은 결정입니다. 다만 Next.js 에서는 경계의 위치가 응답을 언제 보내기 시작하는지까지 정합니다.
클라이언트로 값을 흘려보낼 때
문서는 서버에서 만든 Promise 를 클라이언트 컴포넌트에 props 로 넘기고 React 의 use API 로 읽는 방법도 안내합니다. 서버가 응답을 시작한 뒤에도 데이터가 이어서 흘러가는 구조입니다.
// 서버 컴포넌트
const posts = getPosts(); // await 하지 않고 Promise 를 넘긴다
return (
<Suspense fallback={<Skeleton />}>
<Posts posts={posts} />
</Suspense>
);클라이언트 컴포넌트에서 use(posts) 로 읽으면 준비될 때까지 가장 가까운 Suspense 경계가 fallback 을 보여 줍니다. 클라이언트에서 직접 데이터를 가져와야 한다면 SWR 이나 React Query 같은 라이브러리를 쓰라고 문서는 덧붙입니다. 그 라이브러리들은 캐시와 재검증 규칙을 따로 갖고 있습니다.
순서를 점검하는 세 질문
| 질문 | 그렇다면 |
|---|---|
| 이 조회가 앞의 결과를 쓰는가 | 아니면 함께 시작하고 Promise.all 로 기다린다 |
| 느린 조회가 화면 전체를 막는가 | 그 컴포넌트만 <Suspense> 로 감싼다 |
| 첫 조회가 전부를 막는가 | 캐시하거나, 그 조회 없이 그릴 수 있는 부분을 분리한다 |
느린 화면을 만났을 때 저는 먼저 await 이 몇 번 줄을 서는지 셉니다. 대개 라이브러리나 데이터베이스가 아니라 await 의 위치가 원인입니다.
여기까지가 확실한 부분
레이아웃과 페이지가 기본적으로 병렬로 렌더링된다는 점, 한 컴포넌트 안의 연속된 await 이 순차가 된다는 점, 요청이 함수 호출 시점에 시작된다는 점, 순차 구조에서 첫 요청이 전체를 막는다는 경고, loading.js 보다 데이터 가까이에 <Suspense> 를 두라는 권고, Promise 를 클라이언트로 넘겨 use 로 읽는 방법은 Next.js 16 문서 기준입니다. 300ms 같은 숫자는 설명을 위한 가정이고 실제 값은 데이터 소스에 따라 다릅니다.
함께 읽기
- Next.js 병렬 라우트와 인터셉트 라우트: 모달 패턴과 default.js사진 목록에서 사진을 누르면 모달로 크게 보여 주고, 같은 주소를 새 탭에서 열면 전체 페이지로 보여 주는 화면이 있습니다. 주소는 /photo/123 하나인데 보이는 모양이 둘입니다.
- Next.js 다국어 경로 설계: 쿠키와 하위 경로공개하지 않고 내부에서만 여는 제품 카탈로그 데모에 언어 전환을 붙였습니다. 한국어, 영어, 중국어, 일본어, 독일어, 프랑스어 여섯 개입니다. 고른 언어는 쿠키에 저장하고, 쿠키가 없으면 브라우저의 Accept-Language 헤더를 봅니다. 주소는 나누지 않았습니다. /products/<기종> 하나로 여섯 언어를 모…
- Next.js 라우트 핸들러와 서버 함수 구분앱 라우터에서 서버로 무언가를 보내는 방법이 둘입니다. app/api/.../route.ts 에 라우트 핸들러를 만들거나, "use server" 를 붙인 서버 함수를 부르는 것입니다. 폼 하나를 붙일 때마다 어느 쪽을 쓸지 정해야 하는데, 「둘 다 서버에서 도니 아무거나」로 두면 나중에 갈립니다.
- Next.js 이미지 최적화: next/image와 빌드 때 굽기제품 카탈로그 데모를 만들면서 이미지 처리 방식을 정해야 했습니다. 화면에 나가는 사진은 제품 사진 몇 장과 로고뿐이고, 모두 저장소에 들어 있는 파일입니다. 요청마다 달라질 것이 없습니다.
- Next.js 환경 변수: NEXT_PUBLIC_ 인라인과 로드 순서같은 Docker 이미지를 스테이징과 운영에 함께 올리는 구성이 있습니다. 환경 변수는 각 환경의 설정으로 주입합니다. 서버 쪽 값은 환경에 맞게 잘 바뀌는데, 브라우저에서 쓰는 분석 도구 ID 만 두 환경에서 같은 값이 나옵니다. 스테이징의 방문 기록이 운영 통계에 섞입니다.