[이제와서 시작하는 Next.js 마스터하기 #3] Server Components와 데이터 페칭 완벽 가이드
“서버에서 실행되는 React 컴포넌트?” - 네! Next.js 16의 Server Components는 게임 체인저입니다. 더 빠르고, 더 안전하고, 더 쉽게 데이터를 가져올 수 있습니다!
🎯 이 글에서 배울 내용
- Server Components vs Client Components (제대로 이해하기)
- 데이터 페칭 3가지 방법
- 병렬 vs 순차 데이터 페칭
- 데이터베이스 직접 접근하기
- 실전 패턴과 Best Practices
예상 소요 시간: 45분 사전 지식: #1 처음 시작하는 Next.js, #2 App Router와 라우팅
🤔 Server Components가 뭔가요? (진짜 쉽게)
🍔 레스토랑으로 이해하는 Server vs Client Components
전통적인 React (Client Component만 사용):
1
2
3
4
1. 손님(브라우저)이 레스토랑 방문
2. 빈 접시와 레시피북(JavaScript)을 받음
3. 손님이 직접 요리를 만듦 (클라이언트에서 렌더링)
4. 시간 오래 걸림 + 손님 힘듦 😓
Next.js Server Components:
1
2
3
4
1. 손님(브라우저)이 레스토랑 방문
2. 주방(서버)에서 이미 완성된 요리를 받음
3. 바로 먹음! (서버에서 렌더링된 HTML)
4. 빠르고 편함! 😊
📊 한눈에 비교하기
| 특징 | Server Component | Client Component |
|---|---|---|
| 실행 위치 | 서버 | 브라우저 |
| 번들 크기 | 0 (클라이언트에 안 보냄) | JavaScript 파일에 포함 |
| 데이터 접근 | 데이터베이스 직접 접근 가능 | API 호출 필요 |
| 상호작용 | 불가 (onClick 등 안 됨) | 가능 |
| React Hooks | 사용 불가 | useState, useEffect 등 사용 |
| 사용 시점 | 기본값 (99% 경우) | 필요할 때만 |
🖥️ Server Components 깊이 이해하기
1. Server Component는 기본값
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/posts/page.js
// 별도 선언 없으면 자동으로 Server Component!
async function PostsPage() {
// 서버에서 API로 데이터 가져오기
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
return (
<div>
<h1>게시물 목록</h1>
{posts.slice(0, 10).map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default PostsPage;
왜 좋은가요?
✅ 빠른 초기 로딩
- HTML이 서버에서 완성되어 옴
- 사용자가 즉시 콘텐츠 확인 가능
✅ 작은 번들 크기
- 컴포넌트 코드가 클라이언트에 안 보내짐
- JavaScript 파일 크기 감소
✅ 데이터베이스 직접 접근
- API 엔드포인트 만들 필요 없음
- 보안 키를 서버에만 보관
✅ SEO 친화적
- 검색 엔진이 완성된 HTML을 봄
2. 언제 Server Component를 쓰나요?
✅ 이런 경우에 사용하세요:
- 데이터를 서버에서 가져올 때
- 서드파티 라이브러리 사용 (번들 크기 절약)
- 환경 변수나 API 키 사용
- 정적인 콘텐츠 표시
실전 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ✅ Server Component (추천)
async function ProductList() {
// API로 상품 데이터 가져오기
const response = await fetch('https://fakestoreapi.com/products');
const products = await response.json();
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<div key={product.id} className="border p-4 rounded">
<h3>{product.title}</h3>
<p className="text-2xl font-bold">${product.price}</p>
<p className="text-gray-600">{product.description}</p>
</div>
))}
</div>
);
}
💻 Client Components 완벽 이해하기
1. Client Component는 필요할 때만
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/components/Counter.js
'use client'; // ← 이 한 줄이 Client Component로 만듦!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
2. 언제 Client Component를 쓰나요?
✅ 이런 경우에만 사용하세요:
- 사용자 상호작용 (onClick, onChange 등)
- React Hooks 사용 (useState, useEffect 등)
- 브라우저 API 사용 (localStorage, window 등)
- 이벤트 리스너 필요
실전 예시:
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
32
33
34
// ✅ Client Component (필요함)
'use client';
import { useState, useEffect } from 'react';
export default function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// 사용자 입력에 따라 검색
useEffect(() => {
if (query) {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="검색..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
3. 컴포넌트 합성 패턴
Server와 Client를 조합하는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app/blog/page.js (Server Component)
import SearchBar from '@/components/SearchBar'; // Client
import PostList from '@/components/PostList'; // Server
async function BlogPage() {
// 서버에서 데이터 가져오기
const posts = await getPosts();
return (
<div>
<h1>블로그</h1>
{/* Client Component */}
<SearchBar />
{/* Server Component */}
<PostList posts={posts} />
</div>
);
}
핵심 원칙:
- Server Component 안에 Client Component 넣기: ✅ 가능
- Client Component 안에 Server Component 넣기: ❌ 직접은 불가
- 대신 Client Component에 Server Component를 children으로 전달: ✅ 가능!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 올바른 패턴
// app/layout.js (Server)
import ClientWrapper from './ClientWrapper';
import ServerSidebar from './ServerSidebar';
export default function Layout({ children }) {
return (
<ClientWrapper>
<ServerSidebar /> {/* children으로 전달 */}
{children}
</ClientWrapper>
);
}
// ClientWrapper.js
'use client';
export default function ClientWrapper({ children }) {
return (
<div className="interactive-wrapper">
{children} {/* Server Component가 여기 들어감 */}
</div>
);
}
📥 데이터 페칭 방법
1. fetch API 사용 (추천)
Next.js는 fetch를 확장했습니다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/posts/page.js
async function PostsPage() {
// 기본: 캐싱됨 + 자동 재검증
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
fetch 옵션 (Next.js 16):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 캐싱 비활성화 (항상 최신 데이터)
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store'
});
// 2. 60초마다 재검증
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
});
// 3. 태그 기반 재검증
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
2. 여러 관련 데이터 한 번에 가져오기
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
// app/posts/page.js
async function PostsPage() {
// 게시물 목록 가져오기
const postsResponse = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await postsResponse.json();
// 사용자 정보 가져오기
const usersResponse = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await usersResponse.json();
// 사용자 ID로 매핑
const userMap = Object.fromEntries(users.map(u => [u.id, u]));
return (
<div>
{posts.slice(0, 10).map(post => {
const author = userMap[post.userId];
return (
<article key={post.id} className="mb-6 p-4 border rounded">
<h2 className="text-xl font-bold">{post.title}</h2>
<p className="text-gray-600 text-sm">
작성자: {author?.name || '알 수 없음'}
</p>
<p className="mt-2">{post.body}</p>
</article>
);
})}
</div>
);
}
3. 쿼리 파라미터로 필터링하기
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
32
// app/users/[id]/posts/page.js
async function UserPostsPage({ params }) {
const { id } = await params;
// 특정 사용자의 게시물만 가져오기
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${id}`
);
const posts = await response.json();
// 사용자 정보도 가져오기
const userResponse = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
const user = await userResponse.json();
return (
<div>
<h1>{user.name}의 게시물</h1>
<p className="text-gray-600">{user.email}</p>
<div className="mt-6 space-y-4">
{posts.map(post => (
<article key={post.id} className="p-4 border rounded">
<h2 className="font-bold">{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
</div>
);
}
⚡ 병렬 vs 순차 데이터 페칭
🐢 문제: 순차 페칭 (느림)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 나쁜 예: 순차적으로 데이터 가져오기
async function SlowPage() {
// 1번 요청 (2초)
const user = await fetch('/api/user').then(r => r.json());
// 2번 요청 (2초) - 1번이 끝날 때까지 대기
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
// 총 4초 걸림! 😱
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
🚀 해결: 병렬 페칭 (빠름)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 좋은 예: 병렬로 데이터 가져오기
async function FastPage() {
// Promise.all로 동시에 실행
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
// 총 2초만 걸림! (가장 느린 요청 기준) 😊
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
🎯 순차가 필요한 경우
두 번째 요청이 첫 번째 결과에 의존할 때:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function UserPostsPage({ params }) {
const { userId } = await params;
// 1. 사용자 정보 먼저 가져오기
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
// 2. 그 사용자의 포스트 가져오기 (user.id 필요)
const posts = await fetch(`/api/posts?authorId=${user.id}`).then(r => r.json());
return (
<div>
<h1>{user.name}의 포스트</h1>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
🎨 실전 패턴
1. 데이터 페칭 + 로딩 UI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/posts/page.js
import { Suspense } from 'react';
import PostList from '@/components/PostList';
import PostListSkeleton from '@/components/PostListSkeleton';
export default function PostsPage() {
return (
<div>
<h1>게시물</h1>
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// components/PostList.js (Server Component)
async function PostList() {
// 시간이 걸리는 데이터 페칭
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
export default PostList;
2. 여러 컴포넌트에서 병렬 페칭
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
// app/dashboard/page.js
import { Suspense } from 'react';
import UserInfo from '@/components/UserInfo';
import RecentPosts from '@/components/RecentPosts';
import Analytics from '@/components/Analytics';
export default function Dashboard() {
return (
<div className="grid grid-cols-2 gap-4">
{/* 각 컴포넌트가 독립적으로 데이터 페칭 */}
<Suspense fallback={<Skeleton />}>
<UserInfo /> {/* 1초 걸림 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentPosts /> {/* 2초 걸림 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<Analytics /> {/* 3초 걸림 */}
</Suspense>
</div>
);
}
// 결과: 3초 후 모두 표시 (순차면 6초!)
3. 에러 처리와 함께
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/posts/[id]/page.js
import { notFound } from 'next/navigation';
async function PostDetailPage({ params }) {
const { id } = await params;
try {
const post = await fetch(`https://api.example.com/posts/${id}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
});
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
} catch (error) {
// 404 페이지로 이동
notFound();
}
}
🎨 실전 예제: 완전한 블로그 페이지 만들기
이번엔 실제로 동작하는 블로그 페이지를 만들어봅시다!
완성된 코드 (복사해서 바로 사용 가능!)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// app/blog/page.js
async function BlogPage() {
// 1. 게시물과 사용자 정보를 병렬로 가져오기
const [postsRes, usersRes] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users')
]);
const posts = await postsRes.json();
const users = await usersRes.json();
// 2. 사용자 정보를 ID로 빠르게 찾을 수 있게 Map 생성
const userMap = new Map(users.map(u => [u.id, u]));
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-4xl font-bold mb-8">블로그</h1>
<div className="space-y-6">
{posts.slice(0, 10).map(post => {
const author = userMap.get(post.userId);
return (
<article
key={post.id}
className="p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition"
>
{/* 제목 */}
<h2 className="text-2xl font-semibold mb-2">
{post.title}
</h2>
{/* 작성자 정보 */}
<div className="flex items-center gap-2 text-sm text-gray-600 mb-4">
<span>✍️ {author?.name}</span>
<span>•</span>
<span>📧 {author?.email}</span>
</div>
{/* 본문 */}
<p className="text-gray-700 leading-relaxed">
{post.body}
</p>
{/* 댓글 수 표시 (다음 섹션에서 구현) */}
<div className="mt-4 text-sm text-gray-500">
💬 댓글 보기
</div>
</article>
);
})}
</div>
</div>
);
}
export default BlogPage;
코드 설명 (초보자용)
1. 병렬 데이터 페칭
1
const [postsRes, usersRes] = await Promise.all([...]);
- 두 API를 동시에 호출 (순차보다 2배 빠름!)
Promise.all()은 모든 요청이 완료될 때까지 기다림
2. Map 자료구조 사용
1
const userMap = new Map(users.map(u => [u.id, u]));
- 사용자를 ID로 빠르게 찾기 위한 자료구조
userMap.get(1)→ O(1) 시간 복잡도 (매우 빠름!)
3. 깔끔한 UI 구성
1
className="p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition"
- Tailwind CSS로 카드 스타일 적용
hover:shadow-lg로 마우스 올리면 그림자 커짐
🚀 실습: 직접 만들어보기
app/blog/page.js파일 생성- 위 코드 복사해서 붙여넣기
- 개발 서버 실행:
npm run dev http://localhost:3000/blog접속!
축하합니다! 🎉 실제로 동작하는 블로그 페이지를 만들었습니다!
💡 추가 도전 과제
도전 1: 각 게시물에 댓글 수 표시하기
힌트: JSONPlaceholder의 /comments?postId=1 엔드포인트 사용
1
2
3
4
5
6
7
8
9
10
11
12
// 각 게시물의 댓글 수 가져오기
const commentsRes = await fetch('https://jsonplaceholder.typicode.com/comments');
const comments = await commentsRes.json();
// 게시물 ID별로 댓글 수 계산
const commentCounts = comments.reduce((acc, comment) => {
acc[comment.postId] = (acc[comment.postId] || 0) + 1;
return acc;
}, {});
// 표시할 때
<div>💬 댓글 {commentCounts[post.id] || 0}개</div>
도전 2: 검색 기능 추가하기
힌트: Client Component로 검색창 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app/blog/components/SearchBar.js
'use client';
import { useState } from 'react';
export default function SearchBar({ onSearch }) {
const [query, setQuery] = useState('');
return (
<input
type="text"
value={query}
onChange={(e) => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
placeholder="게시물 검색..."
className="w-full p-3 border rounded-lg"
/>
);
}
📝 데이터베이스 연동은 어떻게 하나요?
실제 프로젝트에서는 외부 API 대신 자체 데이터베이스를 사용하게 됩니다.
데이터베이스가 필요한 이유
외부 API의 한계:
- ❌ 데이터를 수정할 수 없음
- ❌ 원하는 구조로 데이터 저장 불가
- ❌ 속도 제한 (Rate Limit)
- ❌ 서비스 중단 위험
자체 데이터베이스의 장점:
- ✅ 완전한 데이터 제어
- ✅ 빠른 응답 속도
- ✅ 원하는 구조로 설계 가능
- ✅ 보안 강화
다음 단계
데이터베이스 연동은 #10 데이터베이스 연동 실전 포스트에서 자세히 다룰 예정입니다!
다룰 내용:
- PostgreSQL 설치 및 설정
- Prisma ORM 사용법
- 데이터 모델링
- CRUD 작업 구현
- 실전 예제
지금은 fetch API로 충분합니다! 개념을 이해하는 게 먼저입니다. 😊
🔍 자주 묻는 질문 (FAQ)
Q1: Server Component에서 useState를 쓰고 싶어요
답: Server Component에서는 useState를 쓸 수 없습니다!
해결책 1: Client Component로 만들기
1
2
3
4
5
6
7
8
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
해결책 2: 컴포넌트 분리
1
2
3
4
5
6
7
8
9
10
11
// Server Component (부모)
async function Page() {
const data = await fetchData();
return (
<div>
<ServerContent data={data} />
<ClientCounter /> {/* Client Component */}
</div>
);
}
Q2: Client Component에서 API 데이터를 가져오려면?
답: useEffect와 useState를 사용하세요!
Server Component에서는 간단
1
2
3
4
5
6
// Server Component (async 가능)
async function PostList() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return <div>{/* posts 표시 */}</div>;
}
Client Component에서는 useEffect 필요
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
32
33
34
35
// components/PostList.js (Client)
'use client';
import { useEffect, useState } from 'react';
export default function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 컴포넌트가 마운트될 때 데이터 가져오기
fetch('https://jsonplaceholder.typicode.com/posts')
.then(r => r.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(error => {
console.error('Error:', error);
setLoading(false);
});
}, []); // 빈 배열: 한 번만 실행
if (loading) return <div>로딩 중...</div>;
return (
<div>
{posts.slice(0, 5).map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
</div>
))}
</div>
);
}
💡 Tip: 가능하면 Server Component를 사용하세요! 더 간단하고 빠릅니다.
Q3: async/await를 컴포넌트에서 쓸 수 있나요?
답: Server Component에서만 가능합니다!
✅ Server Component (가능)
1
2
3
4
5
// 함수를 async로 선언
async function Page() {
const data = await fetch('...').then(r => r.json());
return <div>{data.title}</div>;
}
❌ Client Component (불가)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use client';
// async는 안 됨!
async function ClientPage() { // 에러!
const data = await fetch('...');
}
// 대신 useEffect + useState 사용
function ClientPage() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('...').then(r => r.json()).then(setData);
}, []);
}
Q4: 데이터가 캐싱되는 건 좋은 건가요?
답: 상황에 따라 다릅니다!
✅ 캐싱이 좋은 경우:
- 자주 변하지 않는 데이터 (상품 목록, 블로그 글 등)
- 서버 부하를 줄이고 싶을 때
- 빠른 응답이 중요할 때
1
2
// 기본값: 캐싱됨
const data = await fetch('https://api.example.com/products');
❌ 캐싱을 피해야 하는 경우:
- 실시간 데이터 (주식 가격, 채팅 등)
- 사용자별 데이터 (프로필, 장바구니 등)
- 항상 최신 데이터가 필요할 때
1
2
3
4
// 캐싱 비활성화
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
});
Q5: 언제 Server, 언제 Client Component를 쓰나요?
답: 이 플로우차트를 따라가세요!
1
2
3
4
5
6
7
8
9
10
11
데이터를 서버에서 가져오나요?
├─ Yes → Server Component
└─ No ↓
사용자와 상호작용하나요? (클릭, 입력 등)
├─ Yes → Client Component ('use client')
└─ No ↓
브라우저 API를 사용하나요? (localStorage 등)
├─ Yes → Client Component
└─ No → Server Component (기본값)
경험 법칙:
- 의심스러우면 Server Component 사용
- 필요할 때만 ‘use client’ 추가
- 대부분의 컴포넌트는 Server여야 함!
💡 초보자를 위한 Best Practices
1. Server Component를 기본으로
1
2
3
4
5
6
7
8
// ✅ 좋음: 대부분 Server Component
app/
├── page.js # Server (기본)
├── blog/
│ ├── page.js # Server
│ └── components/
│ ├── PostList.js # Server
│ └── LikeButton.js # Client (필요시만!)
2. 작은 Client Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 좋음: Client 부분만 분리
// app/blog/[id]/page.js (Server)
async function PostPage({ params }) {
const { id } = await params;
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 작은 Client Component */}
<LikeButton postId={id} />
</article>
);
}
// components/LikeButton.js (Client)
'use client';
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
// 작은 상호작용만 담당
}
3. 병렬 데이터 페칭 활용
1
2
3
4
5
6
7
8
9
10
// ✅ 좋음: Promise.all 사용
async function DashboardPage() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
// 동시에 실행되어 빠름!
}
4. 에러 처리는 필수
1
2
3
4
5
6
7
8
9
// ✅ 좋음: try-catch 추가
async function Page() {
try {
const data = await fetchData();
return <div>{data.title}</div>;
} catch (error) {
return <div>데이터를 불러올 수 없습니다</div>;
}
}
🎯 오늘 배운 내용 정리
✅ 핵심 개념
- Server Components
- 서버에서 실행
- 번들 크기 0
- 데이터베이스 직접 접근 가능
- Client Components
- 브라우저에서 실행
- 상호작용 가능
- useState, useEffect 사용
- 데이터 페칭
- fetch API (Next.js가 확장함)
- ORM (Prisma 등)
- 외부 CMS/API
- 성능 최적화
- 병렬 페칭 (Promise.all)
- Suspense로 부분 로딩
- 적절한 캐싱 전략
🚀 다음 단계
다음 포스트에서는:
- 캐싱 전략 심화
- revalidate와 태그
- Next.js 16의 “use cache” directive
를 배워보겠습니다!
📚 시리즈 네비게이션
이전 글
다음 글
- #4 캐싱과 성능 최적화 (다음 포스트에서 계속!)
🔗 참고 자료
“Server Components는 처음엔 낯설지만, 익숙해지면 돌아갈 수 없습니다!” - React의 미래입니다! 🚀