React Server Components 도입 검토하며 느낀 점

배경

회사 대시보드 프로젝트를 Next.js 13으로 마이그레이션하면서 Server Components를 본격적으로 검토했다. 기존에는 pages 디렉토리에서 getServerSideProps로 데이터를 fetching하는 방식이었는데, app 디렉토리의 RSC는 컴포넌트 레벨에서 서버 로직을 다룰 수 있다는 점이 달랐다.

기존 방식의 문제

// pages/dashboard.tsx
export async function getServerSideProps() {
  const data = await fetchDashboardData();
  return { props: { data } };
}

export default function Dashboard({ data }) {
  return <DashboardUI data={data} />;
}

페이지 단위로만 서버 데이터를 가져올 수 있어서, 컴포넌트 계층이 깊어지면 props drilling이 발생했다. 또한 클라이언트에서 사용하는 모든 라이브러리가 번들에 포함되었다.

Server Components 적용

// app/dashboard/page.tsx
async function DashboardStats() {
  const stats = await fetchStats(); // 서버에서 직접 호출
  return <StatsCard data={stats} />;
}

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<Loading />}>
        <DashboardStats />
      </Suspense>
    </div>
  );
}

컴포넌트 내부에서 직접 async/await를 사용할 수 있게 되어 데이터 fetching 로직이 훨씬 직관적이었다. Suspense와 조합하면 로딩 상태도 선언적으로 처리 가능했다.

마주친 이슈

가장 헷갈렸던 부분은 'use client' 지시어를 어디에 붙여야 하는지였다. useState나 useEffect를 쓰는 순간 Client Component가 되어야 하는데, 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 어디로 나눌지 고민이 많았다.

또한 서버 컴포넌트에서는 Context API를 사용할 수 없어서, 전역 상태 관리 전략을 다시 짜야 했다. 결국 클라이언트 상태는 최소화하고, 서버에서 가져올 수 있는 데이터는 최대한 서버 컴포넌트에서 처리하는 방향으로 갔다.

결과

번들 사이즈가 약 30% 감소했고, Lighthouse 점수는 78점에서 91점으로 올랐다. 특히 date-fns, lodash 같은 유틸리티 라이브러리를 서버에서만 쓰게 되면서 클라이언트 번들이 가벼워진 효과가 컸다.

아직 베타 단계라 프로덕션 적용은 조심스럽지만, 방향성은 확실히 좋아 보인다. 당분간은 작은 프로젝트에서 경험을 쌓으며 안정화를 지켜볼 예정이다.