React 18 Server Components에서 데이터 페칭 패턴 정리
배경
회사 대시보드 프로젝트를 Next.js 14로 마이그레이션하면서 Server Components를 본격적으로 도입했다. 기존에는 클라이언트 컴포넌트에서 useEffect로 데이터를 페칭했지만, 이제는 서버에서 직접 데이터를 가져올 수 있게 되었다.
기존 패턴의 문제점
// 기존 방식
function Dashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/dashboard')
.then(res => res.json())
.then(setData)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <div>{data.title}</div>;
}
이 패턴은 클라이언트에서 추가 요청이 발생하고, waterfall 문제가 있었다.
Server Components 적용
// app/dashboard/page.tsx
async function Dashboard() {
const data = await fetch('https://api.example.com/dashboard', {
next: { revalidate: 60 }
}).then(res => res.json());
return <div>{data.title}</div>;
}
서버 컴포넌트에서는 async/await를 직접 사용할 수 있다. 컴포넌트가 비동기 함수가 되는 것이 처음엔 낯설었지만, 코드가 훨씬 간결해졌다.
병렬 데이터 페칭
여러 API를 호출할 때는 Promise.all을 활용했다.
async function Page() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return (
<>
<UserProfile user={user} />
<PostList posts={posts} />
<CommentSection comments={comments} />
</>
);
}
클라이언트 인터랙션 분리
서버 컴포넌트는 상태를 가질 수 없으므로, 인터랙션이 필요한 부분만 클라이언트 컴포넌트로 분리했다.
// Server Component
async function PostList() {
const posts = await fetchPosts();
return posts.map(post => <PostCard key={post.id} post={post} />);
}
// Client Component
'use client';
function PostCard({ post }) {
const [liked, setLiked] = useState(false);
return (
<div>
<h3>{post.title}</h3>
<button onClick={() => setLiked(!liked)}>좋아요</button>
</div>
);
}
결과
초기 로딩 속도가 약 40% 개선되었고, 클라이언트 번들 크기도 줄었다. 다만 서버와 클라이언트 컴포넌트의 경계를 명확히 하는 것이 초반에는 혼란스러웠다. 팀 내 컨벤션을 정하고 파일명에 .client.tsx, .server.tsx 같은 접미사를 붙여 구분하기로 했다.