| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- 성공
- github
- 넥스트js
- 파이썬 클래스
- Kotlin If
- 파이썬 반복문
- activate 오류
- git
- 다중조건문
- 클래스 속성
- Kotlin else if
- Variable declaration
- Kotlin 클래스 속성정의
- python Django
- django virtualenv
- 도전
- Kotlin Class
- 파이썬 제어문
- 좋은글
- Kotlin 조건문
- 파이썬
- 강제 타입변환
- 장고 가상환경
- Kotlin 클래스
- Python Class
- 자바 기본타입
- 파이썬 장고
- NextJs
- 희망
- Python
- Today
- Total
키모스토리
#34. Data Fetching Pattern 본문
Data Fetching Pattern 정리
Sequential Data Fetching vs Parallel Data Fetching (Server Component 기준)
Next.js(App Router)의 Server Component 환경에서는 fetch()를 서버에서 실행할 수 있고, 그 결과를 JSX로 바로 렌더링할 수 있습니다.
이때 “여러 데이터를 가져오는 방식”은 크게 두 가지 패턴으로 설명할 수 있습니다.
- Sequential Data Fetching(순차 요청): A를 받은 후 B를 요청
- Parallel Data Fetching(병렬 요청): A/B/C를 동시에 요청 후 결과를 모아서 렌더링
아래는 jsonplaceholder API를 활용한 실습 예제를 기준으로 각각을 설명합니다.
1) Sequential Data Fetching (순차 데이터 페칭)
✅ 목적
**“블로그 게시물 목록을 먼저 가져오고, 각 게시물의 작성자 정보를 이후에 가져오는 구조”**를 구현합니다.
즉, Posts → Author 흐름이 “순차적으로” 이어집니다.
✅ 예제 구조
- posts-sequential/page.tsx
- posts 목록을 먼저 fetch → 목록 렌더링
- 각 post 카드 내부에서 <AuthorInfo userId={post.userId} /> 호출
- <Suspense>로 작성자 영역만 로딩 처리
- posts-sequential/author.tsx
- userId로 /users/{id} 호출 → 작성자 이름 출력
✅ 예제 소스
//폴더구조
/app/
/posts-sequential
page.tsx
author.tsx
/app/posts-sequential/page.tsx
import { resolve } from "path";
import AuthorInfo from "./author";
import { Suspense } from "react";
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export default async function PostsSequential() {
// await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts: Post[] = await response.json();
const filteredPosts = posts.filter((post) => post.id % 10 === 1);
// console.log(filteredPosts);
return (
<div className="p-4 max-w-7xl mx-auto">
<h1 className="tet-3xl font-extrabold mb-8">Blog Posts</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{filteredPosts.map((post) => (
<div key={post.id} className="bg-white shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-3 text-gray-800 leading-tight">
{post.title}
</h2>
<p className="text-gray-600 mb-4 leading-relaxed">{post.body}</p>
<p className="text-gray-600">
<Suspense
fallback={
<div className="text-sm text-gray-500">Loading author...</div>
}
>
<AuthorInfo userId={post.userId}></AuthorInfo>
</Suspense>
</p>
</div>
))}
</div>
</div>
);
}
/app/posts-sequential/author.tsx
type Author = {
id: number;
name: string;
username: string;
email: string;
phone: string;
};
export default async function AuthorInfo({ userId }: { userId: number }) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user: Author = await response.json();
console.log(user);
return (
<div className="text-sm text-gray-500">
Written by:{" "}
<span className="font-semibold text-gray-700 hover:text-gray-900 transition-colors">
{user.name}
</span>
</div>
);
}

✅ 코드 흐름(실행 순서)
- page.tsx에서 /posts를 먼저 호출하여 posts 목록을 가져옵니다.
- 화면에 posts 카드들이 렌더링됩니다.
- 각 카드 내부에서 AuthorInfo가 실행되며 /users/{userId}를 호출합니다.
- 작성자 정보가 도착하면 해당 카드의 “Written by” 영역이 채워집니다.
✅ 이 패턴의 핵심 포인트
1) “렌더링을 먼저 하고, 필요한 부분만 추가로 채운다”
Suspense를 사용했기 때문에, 게시글 본문은 빠르게 보여주고 작성자 영역만 “Loading author…”로 대체되었다가 나중에 채워집니다.
2) 네트워크 요청이 많아질 수 있다 (N+1 문제)
게시글이 10개라면:
- posts 1번 + users 10번 → 총 11번 요청
이 구조가 커지면 성능/요청 수가 부담이 될 수 있습니다.
✅ 언제 유리한가?
- 페이지 전체를 기다리기보다 “일단 콘텐츠를 보여주고” 부분적으로 채우고 싶을 때
- 각 아이템의 부가 정보(작성자, 댓글, 통계 등)가 독립적으로 로딩되어도 UX가 자연스러울 때
- 스트리밍/서스펜스 기반 UI를 활용하고 싶을 때
2) Parallel Data Fetching (병렬 데이터 페칭)
✅ 목적
**“사용자 프로필 페이지에서 유저 정보 + 글 목록 + 앨범 목록을 동시에 가져와서 렌더링”**합니다.
주어진 예제는 아래 3개를 병렬로 수행합니다.
- /users/{id} → 사용자 정보
- /posts?userId={id} → 사용자 글 목록
- /albums?userId={id} → 사용자 앨범 목록
✅ 예제 구조
- user-parallel/[id]/page.tsx
- getUserInfo(id)
- getUserPosts(id)
- getUserAlbums(id)
- 위 3개를 Promise.all()로 묶어서 한 번에 처리
✅ 예제 소스
//폴더구조
/app/user-parallel/
/[id]
page.tsx
loading.tsx
page.tsx
type User = {
id: number;
name: string;
username: string;
email: string;
phone: string;
};
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
type Album = {
userId: number;
id: number;
title: string;
};
// 유저정보 조회용 fetch
async function getUserInfo(id: string): Promise<User> {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) throw new Error("Failed to fetch user");
return (await res.json()) as User;
}
// 유저 POST 조회용 fetch
async function getUserPosts(userId: string) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
return res.json();
}
// 유저 ALBUM 조회용 fetch
async function getUserAlbums(userId: string) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const res = await fetch(
`https://jsonplaceholder.typicode.com/albums?userId=${userId}`
);
return res.json();
}
export default async function UserProfile({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// 병렬조회요청 (Parallel Data Fetching)
const [user, posts, albums] = await Promise.all([
getUserInfo(id),
getUserPosts(id),
getUserAlbums(id),
]);
return (
<div className="p-4 max-w-7xl mx-auto">
<h1 className="text-3xl font-extrabold mb-8">
User Profile - {user.name}
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h2 className="text-2xl font-bold mb-4">Posts</h2>
<div className="space-y-4">
{posts.map((post: Post) => (
<div key={post.id} className="bg-white shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-3 text-gray-800 leading-tight">
{post.title}
</h2>
<p className="text-gray-600 mb-4 leading-relaxed">
{post.body}
</p>
</div>
))}
</div>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">Album</h2>
<div className="space-y-4">
{albums.map((album: Album) => (
<div key={album.id} className="bg-white shadow-md rounded-lg p-6">
<p className="text-gray-700">{album.title}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
}

✅ 코드 흐름(실행 순서)
- page.tsx 진입 시점에 3개의 fetch 요청을 “동시에” 시작합니다.
- 가장 느린 요청이 끝날 때까지 기다립니다. (Promise.all 특성)
- user/posts/albums가 모두 준비되면 한 번에 렌더링합니다.
✅ 이 패턴의 핵심 포인트
1) 총 대기시간이 “합”이 아니라 “최대값”에 가까워진다
예를 들어,
- user: 200ms
- posts: 1000ms
- albums: 1000ms
이면 병렬에서는 최종 대기 시간이 대략 1000ms 수준이 됩니다.
반대로 순차라면
200 + 1000 + 1000 = 2200ms처럼 “합산”될 가능성이 큽니다.
2) 페이지 완성도를 한 번에 보장한다
병렬 페칭은 보통 페이지 전체의 데이터가 다 모인 뒤 렌더링되므로,
“프로필/글/앨범이 동시에 갖춰진 완성 화면”을 만들기 좋습니다.
✅ 언제 유리한가?
- 페이지 렌더링에 필요한 데이터들이 서로 의존성이 없을 때
- “유저 정보 + 글 + 앨범”처럼 각각 독립 API이고, 한 번에 모아서 화면을 구성할 때
- 서버에서 데이터 수집을 최적화하여 TTFB/전체 로딩 시간을 줄이고 싶을 때
3) 두 패턴 비교 요약
✅ 개념 차이
- Sequential: A를 받아야 B를 할 수 있거나, 일부 영역만 늦게 채우고 싶을 때
- Parallel: 서로 독립이면 동시에 요청해서 전체 시간을 줄일 때
✅ UX 관점
- Sequential + Suspense: “컨텐츠 먼저 → 부가정보 나중” 스트리밍 UX에 강함
- Parallel: “한 번에 완성된 화면”을 안정적으로 보여주기 좋음
✅ 성능 관점(요청 수/대기시간)
- Sequential은 리스트에서 자주 N+1 문제가 발생할 수 있습니다.
- Parallel은 대기 시간이 최대 지연 요청 기준으로 수렴합니다.
4) 실무 팁 (예제에 바로 적용 가능한 포인트)
✅ (1) 리스트 + 작성자 정보는 “병렬 + 배치”도 고려
Sequential 예제는 학습용으로 좋지만, posts 수가 많아지면 작성자 요청이 많아집니다.
실무에서는 다음을 고려합니다.
- 작성자 정보를 posts와 함께 내려주는 API(서버에서 조인/합치기)
- userId들을 모아 /users를 한 번 가져온 뒤 매핑해서 쓰기
- 또는 Next.js Route Handler로 내부 API를 만들어 서버에서 합쳐 반환하기
✅ (2) Promise.all()은 “하나라도 실패하면 전부 실패”
Parallel에서 Promise.all()은 한 요청이 실패하면 전체가 reject 됩니다.
실무에서는 “부분 실패 허용”이 필요하면 Promise.allSettled()를 고려합니다.
결론
- Sequential Data Fetching은 “부분 스트리밍 렌더링”과 “의존성 있는 호출”에 강하고, Suspense와 함께 쓰면 UX가 좋아집니다.
- Parallel Data Fetching은 “독립 데이터 동시 요청”으로 전체 대기 시간을 줄이고, 완성된 화면을 빠르게 제공하는 데 유리합니다.
'Web Devlopment > NextJs' 카테고리의 다른 글
| #33. Redering section Summary (0) | 2025.12.30 |
|---|---|
| #32. Interleaving Server and Client Components (0) | 2025.12.30 |
| #31. 서드파티 UI 패키지, Context Providers (0) | 2025.12.30 |
| #30. Streaming, Server/Client Composition Patterns (0) | 2025.12.30 |
| #29. generateStaticParams / dynamicParams (0) | 2025.12.30 |
