๐ Cascading Network Requests (Waterfall Model): ๋๋ฆฐ ๋ก๋ฉ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ ์ ํ
Summary
๋ฐ์ดํฐ ์์ฒญ์ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ Waterfall ๋ชจ๋ธ์ ๋ถํ์ํ ๋คํธ์ํฌ ์ง์ฐ์ ์ ๋ฐํ์ฌ ๋ก๋ฉ ์๊ฐ์ ์ฆ๊ฐ์ํค๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ํดํฉ๋๋ค. Promise.all
์ ํตํ ๋ณ๋ ฌ ์ฒ๋ฆฌ, ๋ฐฑ์๋ Aggregator API/GraphQL ํ์ฉ, ๊ทธ๋ฆฌ๊ณ SSR/SSG์ ๊ฐ์ ์ด๊ธฐ ๋ ๋๋ง ์ ๋ต์ ํตํด ํจ์จ์ ์ธ ๋ฐ์ดํฐ ๋ก๋ฉ์ ๊ตฌํํ์ฌ ์น ์ฑ๋ฅ์ ์ต์ ํํด์ผ ํฉ๋๋ค.
Why Wrong?
๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ณผ์ ์์ ์ด์ ์์ฒญ์ด ์๋ฃ๋์ด์ผ ๋ค์ ์์ฒญ์ ์์ํ๋ ์ข ์์ ์ธ ๊ตฌ์กฐ(Waterfall Model)๋ ๋ถํ์ํ ์ง์ฐ์ ๋ฐ์์์ผ ๋ก๋ฉ ์๊ฐ์ ๋๋ฆฌ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ํดํฉ๋๋ค. ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ ๋, ๊ฐ ๋ฐ์ดํฐ ์์ฒญ์ด ์์ฐจ์ ์ผ๋ก ์ด๋ฃจ์ด์ง๋ฉด ์ ์ฒด ์๋ต ์๊ฐ์ด ๋ชจ๋ ์์ฒญ์ ํฉ์ผ๋ก ๋์ด๋ฉ๋๋ค. ์ด๋ ํนํ ๋คํธ์ํฌ ์ง์ฐ ์๊ฐ(Latency)์ด ๋์ ํ๊ฒฝ์์ ์ฌ์ฉ์์๊ฒ ๊ธด ๋๊ธฐ ์๊ฐ์ ์ ๋ฐํ๋ฉฐ, ์ด๋ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํฉ๋๋ค:
- ์ง๋ ฌํ๋ ์ง์ฐ(Serialization Delay): ๊ฐ ์์ฒญ์ ์ด์ ์์ฒญ์ ์๋ฃ๋ฅผ ๊ธฐ๋ค๋ ค์ผ๋ง ์์๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ํ๋กํ์ ๊ฐ์ ธ์จ ํ ํด๋น ํ๋กํ ID๋ก ๊ฒ์๋ฌผ์ ๊ฐ์ ธ์ค๋ ์์ ์ข ์์ ํธ์ถ์ด ๋ฐ๋ณต๋๋ฉด, ๊ฐ ๋คํธ์ํฌ ์์ฒญ์ ์๋ณต ์๊ฐ(Round Trip Time)์ด ๋์ ๋์ด ์ ์ฒด ๋ก๋ฉ ์๊ฐ์ด ๊ธฐํ๊ธ์์ ์ผ๋ก ๋์ด๋ฉ๋๋ค. ๋ง์ฝ A ์์ฒญ์ 100ms, B ์์ฒญ์ 100ms๊ฐ ๊ฑธ๋ฆฐ๋ค๋ฉด, ์์ฐจ์ ์ผ๋ก๋ ์ด 200ms๊ฐ ์์๋์ง๋ง, ๋ณ๋ ฌ ์ฒ๋ฆฌ ์์๋ ์ต๋ 100ms(๊ฐ์ฅ ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ฒญ)๋ก ๋จ์ถ๋ ์ ์์ต๋๋ค.
- ๋ธ๋ผ์ฐ์ ์์ ํ์ฉ ๋นํจ์จ์ฑ: ํ๋ ๋ธ๋ผ์ฐ์ ๋ ๋์์ ์ฌ๋ฌ ๋คํธ์ํฌ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๋ ๋ฅ๋ ฅ์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๊ทธ๋ฌ๋ Waterfall ๋ชจ๋ธ์ ์ด๋ฌํ ๋ณ๋ ฌ ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ์ ๋๋ก ํ์ฉํ์ง ๋ชปํด ๋คํธ์ํฌ ๋์ญํญ์ด๋ CPU์ ๊ฐ์ ์์์ด ์ ํด ์ํ๋ก ๋จ์์๊ฒ ๋ฉ๋๋ค. ์ด๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฐ์ ์ธ ์ฑ๋ฅ์ ์ ํ์ํฌ ์ ์์ต๋๋ค.
- ํต์ฌ ์น ์งํ(Core Web Vitals) ์ ํ: ํนํ ์น ์ฑ๋ฅ์ ์ค์ํ ์งํ์ธ Largest Contentful Paint (LCP)์ ์ง์ ์ ์ธ ์ํฅ์ ๋ฏธ ๋ฏธ์นฉ๋๋ค. ์ฃผ์ ์ฝํ ์ธ ๋ฅผ ๋ ๋๋งํ๋ ๋ฐ ํ์ํ ๋ฐ์ดํฐ๊ฐ ์์ฐจ์ ์ผ๋ก ๋ก๋๋๋ฉด LCP ์๊ฐ์ด ๊ธธ์ด์ ธ ์ฌ์ฉ์๊ฐ ํ์ด์ง ๋ก๋ฉ์ด ๋๋ฆฌ๋ค๊ณ ๋๋ผ๊ฒ ๋ฉ๋๋ค. ๋ํ, Total Blocking Time (TBT)๊ณผ ๊ฐ์ ์ํธ์์ฉ ์ง์ฐ ์งํ์๋ ๋ถ์ ์ ์ธ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
- ์ฌ์ฉ์ ๊ฒฝํ ์ ํ ๋ฐ ์ดํ๋ฅ ์ฆ๊ฐ: ์ฌ์ฉ์๋ ๋ฐ์ดํฐ ๋ก๋ฉ์ ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๋น ํ๋ฉด, ๋ถ์์ ํ ํ๋ฉด ๋๋ ๋ฌด์๋ฏธํ ๋ก๋ฉ ์คํผ๋๋ง ๋ณด๊ฒ ๋์ด ์ง๋ฃจํจ์ ๋๋ผ๊ฑฐ๋, ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋๋ฆฌ๋ค๊ณ ์ธ์ํ์ฌ ์ดํํ ๊ฐ๋ฅ์ฑ์ด ์ปค์ง๋๋ค. ์ด๋ ์๋น์ค์ ๋ง์กฑ๋์ ์ง๊ฒฐ๋๋ ๋ฌธ์ ์ ๋๋ค.
How to Fix?
๋ฐ์ดํฐ ์์ฒญ ์ ๋ถํ์ํ ์ง๋ ฌํ๋ฅผ ํผํ๊ณ , ๊ฐ๋ฅํ ํ ๋ง์ ์์ฒญ์ ๋ณ๋ ฌ๋ก ์ฒ๋ฆฌํ์ฌ ์ ์ฒด ๋ก๋ฉ ์๊ฐ์ ๋จ์ถํด์ผ ํฉ๋๋ค. ๋ค์์ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๊ตฌ์ฒด์ ์ธ ๋ฐฉ๋ฒ๋ค์ ๋๋ค:
- ์์ฒญ ์์กด์ฑ ๋ถ์ ๋ฐ ๋ณ๋ ฌ ์ฒ๋ฆฌ: ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฐ ๋ฐ์ดํฐ ์์ฒญ์ด ์ ๋ง๋ก ๋ค๋ฅธ ์์ฒญ์ ์์กด์ ์ธ์ง ๋ฉด๋ฐํ ๋ถ์ํฉ๋๋ค. ๋ง์ฝ ์์กด์ฑ์ด ์๋ค๋ฉด, JavaScript์
Promise.all
๋๋Promise.allSettled
๋ฅผ ์ฌ์ฉํ์ฌ ๋์์ ์ฌ๋ฌ ์์ฒญ์ ์์ํ๊ณ ๋ชจ๋ ์์ฒญ์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค. ์ด๋ ๊ฐ๋ณ ์์ฒญ์ ์ง์ฐ ์๊ฐ์ ์จ๊ฒจ ์ ์ฒด ๋ก๋ฉ ์๊ฐ์ ํฌ๊ฒ ๋จ์ถ์ํต๋๋ค.Promise.all
: ๋ชจ๋ Promise๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์ด์ผ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค. ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ํ๊บผ๋ฒ์ ํ์ํ๊ณ , ์ด๋ ํ๋๋ผ๋ ์คํจํ๋ฉด ์ ๋๋ ๊ฒฝ์ฐ์ ์ ํฉํฉ๋๋ค.Promise.allSettled
: ๋ชจ๋ Promise์ ์ฑ๊ณต/์คํจ ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋ชจ๋ Promise๊ฐ ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค. ๊ฐ๋ณ ์์ฒญ์ ์ฑ๊ณต ์ฌ๋ถ๊ฐ ์ ์ฒด ํ๋ก์ธ์ค๋ฅผ ๋ง์ง ์์์ผ ํ ๋ ์ ์ฉํ๋ฉฐ, ๊ฐ ์์ฒญ์ ์ํ๋ฅผ ์ ์ ์์ต๋๋ค.
- ๋ฐฑ์๋ API ์ต์ ํ ๋ฐ ํตํฉ: ํ๋ก ํธ์๋์์ ์ฌ๋ฌ ๋ฒ์ ์์ฒญ์ ๋ณด๋ด์ผ ํ๋ ์๋๋ฆฌ์ค๋ฅผ ์ค์ด๊ธฐ ์ํด ๋ฐฑ์๋ API๋ฅผ ์ต์ ํํฉ๋๋ค.
- Aggregator API ๋์ : ์ฌ๋ฌ ํด๋ผ์ด์ธํธ ์์ฒญ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ํ๋์ ๋ฐฑ์๋ API ํธ์ถ๋ก ๋ฌถ์ด ์ ๊ณตํ๋ Aggregator API ์๋ํฌ์ธํธ๋ฅผ ์ค๊ณํฉ๋๋ค. ์ด๋ ํ๋ก ํธ์๋์์ ์ฌ๋ฌ ๋ฒ์ ์๋ณต(Round Trip)์ ์ค์ฌ ๋คํธ์ํฌ ์ค๋ฒํค๋๋ฅผ ์ต์ํํฉ๋๋ค.
- GraphQL ํ์ฉ: ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ ํ๋๋ฅผ ์ ํํ ๋ช ์ํ์ฌ ํ ๋ฒ์ ์์ฒญ์ผ๋ก ์ฌ๋ฌ ๋ฆฌ์์ค์ ๋ฐ์ดํฐ๋ฅผ ์กฐํฉํ์ฌ ๊ฐ์ ธ์ฌ ์ ์๋ GraphQL์ ๋์ ํ๋ ๊ฒ๋ ๊ฐ๋ ฅํ ํด๊ฒฐ์ฑ ์ ๋๋ค. ์ด๋ N+1 ๋ฌธ์ ํด๊ฒฐ์๋ ํจ๊ณผ์ ์ ๋๋ค.
- ๋ฐ์ดํฐ ์ ์ทจ(Prefetching) ๋ฐ ์ด๊ธฐ ๋ ๋๋ง ์ ๋ต ํ์ฉ: ์ฌ์ฉ์๊ฐ ํน์ ํ์ด์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ฒ ๋ ๊ฐ๋ฅ์ฑ์ด ๋๋ค๊ณ ์์๋ ๋, ๋ฏธ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ์บ์ฑํ๊ฑฐ๋ ์ด๊ธฐ HTML์ ํฌํจ์ํต๋๋ค.
- Resource Hints (
<link rel="preload">
,<link rel="preconnect">
): HTML์<head>
ํ๊ทธ ๋ด์preload
๋preconnect
ํํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฏธ๋ฆฌ ํ์ํ ๋ฆฌ์์ค(์: ํน์ API ์๋ํฌ์ธํธ์ ๋ํ ์ฐ๊ฒฐ)๋ฅผ ๋ก๋ํ๊ฑฐ๋ ์ฐ๊ฒฐ์ ์์ํ๋๋ก ์ง์ํฉ๋๋ค. ์ด๋ ์ค์ ์์ฒญ ์์ ๋๊ธฐ ์๊ฐ์ ์ค์ฌ์ค๋๋ค. - ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR) ๋๋ ์ ์ ์ฌ์ดํธ ์์ฑ(SSG): ์ด๊ธฐ ํ์ด์ง ๋ก๋ฉ์ ํ์์ ์ธ ๋ฐ์ดํฐ๋ ์๋ฒ์์ ๋ฏธ๋ฆฌ ๊ฐ์ ธ์ HTML์ ํฌํจ์์ผ ํด๋ผ์ด์ธํธ๊ฐ ์ถ๊ฐ์ ์ธ ๋คํธ์ํฌ ์์ฒญ ์์ด ๋น ๋ฅด๊ฒ ์ฝํ ์ธ ๋ฅผ ํ์ํ ์ ์๋๋ก ํฉ๋๋ค. ์ด๋ ์ฒซ ํ์ธํธ(First Contentful Paint)์ LCP๋ฅผ ํฌ๊ฒ ๊ฐ์ ํ๋ฉฐ, SEO์๋ ์ ๋ฆฌํฉ๋๋ค.
- Resource Hints (
Before Code (Bad)
// Before: Waterfall Requests (์์ฐจ์ ์์ฒญ) - ๋ถํ์ํ ์ง์ฐ ๋ฐ์
async function fetchUserProfileAndPostsSequential(userId) {
try {
console.log('--- ์์ฐจ์ ์์ฒญ ์์ ---');
// 1. ์ฌ์ฉ์ ํ๋กํ ์ ๋ณด ์์ฒญ
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
console.log('User Profile fetched:', user.name);
// 2. ์ฌ์ฉ์ ํ๋กํ์ด ๋ก๋๋ ํ ํด๋น ์ฌ์ฉ์์ ๊ฒ์๋ฌผ ์์ฒญ (ํ๋กํ ๋ฐ์ดํฐ์ ์์กดํ์ง ์์๋ ๋จ)
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
console.log('User Posts fetched:', posts.length, 'posts');
// 3. ์ฌ์ฉ์ ๊ฒ์๋ฌผ์ด ๋ก๋๋ ํ ๊ฐ ๊ฒ์๋ฌผ์ ๋๊ธ ์ ์์ฒญ (์ฌ๊ธฐ์ ๋ ๋ค๋ฅธ ์ ์ฌ์ N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ)
const commentsCounts = {};
for (const post of posts) {
const commentsResponse = await fetch(`/api/posts/${post.id}/comments/count`);
const countData = await commentsResponse.json();
commentsCounts[post.id] = countData.count;
console.log(`Comments for post ${post.id}:`, countData.count);
}
console.log('--- ์์ฐจ์ ์์ฒญ ์๋ฃ ---');
return { user, posts, commentsCounts };
} catch (error) {
console.error('Error fetching data sequentially:', error);
throw error;
}
}
// ๊ฐ์์ API ํธ์ถ ์ง์ฐ ์๋ฎฌ๋ ์ด์
(๊ฐ 100ms)
// fetch ํจ์๋ฅผ ๋ํํ์ฌ ์ง์ฐ์ ์ถ๊ฐํ๋ ์์
const originalFetch = global.fetch;
global.fetch = async (...args) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(originalFetch(...args));
}, 100); // ๊ฐ fetch์ 100ms ์ง์ฐ ์ถ๊ฐ
});
};
// Example usage
// fetchUserProfileAndPostsSequential(123); // ์ด 300ms + (N * 100ms) ์ด์ ์์
After Code (Good)
// After: Parallel Requests (๋ณ๋ ฌ ์์ฒญ) - ์ฑ๋ฅ ์ต์ ํ
async function fetchUserProfileAndPostsParallel(userId) {
try {
console.log('--- ๋ณ๋ ฌ ์์ฒญ ์์ ---');
// 1. ์ฌ์ฉ์ ํ๋กํ ์ ๋ณด์ ๊ฒ์๋ฌผ ์ ๋ณด๋ฅผ ๋์์ ์์ฒญ (์๋ก ์์กด์ฑ ์๋ ๊ฒฝ์ฐ)
// Promise.all์ ์ฌ์ฉํ์ฌ ๋์์ ์ฌ๋ฌ ๋น๋๊ธฐ ์์
์ ์์ํ๊ณ ๋ชจ๋ ์์
์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆผ
const [userResponse, postsResponse] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/posts`)
]);
const user = await userResponse.json();
const posts = await postsResponse.json();
console.log('User Profile fetched:', user.name);
console.log('User Posts fetched:', posts.length, 'posts');
// 2. ๋ชจ๋ ๊ฒ์๋ฌผ์ ๋๊ธ ์ ์์ฒญ์ ๋ณ๋ ฌ๋ก ์ฒ๋ฆฌ
// N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฐฑ์๋์์ Aggregator API๋ฅผ ์ ๊ณตํ๊ฑฐ๋,
// ํ๋ก ํธ์๋์์ Promise.all๋ก ๋ณ๋ ฌ ์ฒ๋ฆฌ. ์ฌ๊ธฐ์๋ ํ์์ ์์.
const commentsCountPromises = posts.map(post =>
fetch(`/api/posts/${post.id}/comments/count`).then(res => res.json())
);
const commentsCountsArray = await Promise.all(commentsCountPromises);
const commentsCounts = commentsCountsArray.reduce((acc, current, index) => {
acc[posts[index].id] = current.count;
return acc;
}, {});
console.log('All Comments Counts fetched:', Object.keys(commentsCounts).length);
console.log('--- ๋ณ๋ ฌ ์์ฒญ ์๋ฃ ---');
return { user, posts, commentsCounts };
} catch (error) {
console.error('Error fetching data in parallel:', error);
throw error;
}
}
// ๊ฐ์์ API ํธ์ถ ์ง์ฐ ์๋ฎฌ๋ ์ด์
(๊ฐ 100ms)
// (์ Before ์ฝ๋์์ ์ด๋ฏธ ์ค์ ๋์๋ค๊ณ ๊ฐ์ )
// Example usage
// fetchUserProfileAndPostsParallel(123); // ์ด 100ms + (100ms * (N=1์ผ๋)) + ... ์์
// ์ฆ, (๊ฐ์ฅ ์ค๋ ๊ฑธ๋ฆฌ๋ 1๋จ๊ณ ์์ฒญ) + (๊ฐ์ฅ ์ค๋ ๊ฑธ๋ฆฌ๋ 2๋จ๊ณ ์์ฒญ)๋ง ์์