본문 바로가기

언어(JS,TS)/React

React [Info: Server Components와 Client Components]

1. React Server Components의 등장 배경

클라이언트 중심 렌더링은 초기에는 간단하고 직관적이었습니다. 그러나 앱이 커지고 복잡해지면서, 초기 로드 성능 저하, 번들 크기 증가, 보안 이슈가 반복적으로 문제로 떠올랐습니다. 서버 사이드 렌더링(SSR)과 정적 사이트 생성(SSG)이 등장했지만, 결국 모든 자바스크립트를 클라이언트로 전송한다는 점은 여전히 해결되지 않았습니다.

React 팀은 이러한 구조적 한계를 개선하기 위해 React Server Components(RSC) 를 도입했습니다. 이는 "클라이언트로 굳이 보낼 필요 없는 컴포넌트는 서버에 두자"는 철학에서 출발했습니다.

2. Server Component의 구조와 동작 방식

Server Component는 클라이언트가 아닌 서버에서 렌더링됩니다. 렌더링된 결과는 HTML나 JSON이 아닌 RSC 전용 바이너리 포맷으로 전송됩니다. 이 포맷은 하위 트리 구조, 의존하는 클라이언트 컴포넌트의 위치, props 등을 포함하고 있습니다.

Next.js 13부터 /app 디렉토리 구조는 기본적으로 이 RSC 구조를 따릅니다. page.tsx, layout.tsx 파일은 자동으로 서버 컴포넌트로 인식되며, 명시적으로 클라이언트 컴포넌트로 전환하려면 "use client"를 선언해야 합니다.

3. Server Component의 장점

  • 네트워크 효율: 클라이언트에 필요 없는 로직은 서버에만 존재합니다. 번들 사이즈가 줄어들고, 네트워크 비용도 절감됩니다.
  • 보안: DB 자격 증명, API 키 등 민감 정보는 서버에만 머무르며, 절대 클라이언트에 노출되지 않습니다.
  • 직접적인 데이터 접근: fetch, ORM, 파일시스템 접근이 컴포넌트 내부에서 직접 가능합니다.

4. Server Component의 제약

  • 상호작용 불가: onClick, useState, useEffect 등 브라우저 API는 사용할 수 없습니다.
  • 브라우저 전용 라이브러리 불가: DOM을 필요로 하는 라이브러리(e.g., Chart.js)는 사용할 수 없습니다.

5. Client Component의 역할

클라이언트 컴포넌트는 UI 상호작용을 담당합니다. "use client"를 선언하면 해당 파일과 그 하위 트리 전체가 클라이언트 컴포넌트로 간주됩니다.

주요 특성:

  • useState, useEffect, useRef 등 상태 기반 로직 사용이 가능합니다.
  • 브라우저 이벤트 핸들링 (onClick, onChange, 등) 이 가능합니다.
  • 서드파티 UI 라이브러리 통합이 가능합니다 (예: Zustand, React Query, Recharts 등)

6. Client Component의 단점

  • 성능 이슈: 모든 로직을 클라이언트로 보내므로 초기 로드 속도(eg FCP; First Contentful Paint)가 느려질 수 있습니다.
  • 보안: JS 번들이 클라이언트에 노출되므로, 민감 로직이 섞여 있으면 안 됩니다.

7. Server <-> Client 경계 관리

React는 명시적인 디렉티브 방식("use client")으로 서버와 클라이언트 컴포넌트를 나눕니다. 주의할 점은 다음과 같습니다:

  • "use client"가 선언된 컴포넌트는 하위까지 모두 클라이언트 컴포넌트로 간주됩니다.
  • Server → Client는 props를 통해 데이터 전달이 가능합니다.
  • Client → Server는 직접 호출이 불가능하며, API route나 Server Action을 통해 통신해야 합니다.

8. Next.js에서의 활용 전략

Next.js의 /app 디렉터리는 서버 중심입니다. 기본적으로 페이지, 레이아웃, 로딩 컴포넌트 등은 모두 서버에서 동작합니다. 단, 상호작용이 필요한 UI 요소는 클라이언트 컴포넌트로 분리해야 합니다.

예시

// app/page.tsx (Server Component)
export default async function Page() {
  const posts = await getPosts();
  return (
    <>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      <LikeButton postId={posts[0].id} /> {/* 클라이언트 컴포넌트 */}
    </>
  );
}

// components/LikeButton.tsx (Client Component)
"use client";
import { useState } from "react";
export default function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>;
}

9. 성능 최적화 관점

  • 서버 컴포넌트는 스트리밍 방식으로 HTML을 점진적으로 보내 사용자에게 더 빠른 TTFB(Time To First Byte)를 제공합니다.
  • 클라이언트 컴포넌트는 dynamic()으로 지연 로딩하여 번들 크기를 최소화합니다.
  • API 호출은 서버에서 처리하여 클라이언트-서버 간 왕복 횟수를 줄입니다.

10. 보안과 SEO 관점

  • 서버에서 HTML을 직접 출력하므로 SEO 친화적입니다.
  • 민감한 연산 및 정보는 서버 컴포넌트에 머물러야 합니다. 이는 보안 설계의 기본입니다.

11. 모범 사용 조건

  1. 최대한 서버 컴포넌트를 기본값으로 사용하되, 상호작용만 클라이언트로 분리합니다.
  2. Client Component 내에서는 서버 관련 로직을 절대 넣지 않도록 주의합니다.
  3. 타입은 props 기준으로 Server <-> Client 경계마다 명확히 정의합니다.
  4. 비동기 로직(fetch, DB query 등)은 반드시 Server Component에서만 실행합니다.
  5. UI 상태 로직은 Client Component에서만 실행합니다.
  6. 경계가 명확한 폴더 구조를 구성합니다 (예: components/server, components/client)

12. 느낀 점과 결론

React 생태계는 꾸준히 변화해왔습니다. 이번 Server Component의 등장은 단순 기술 추가를 넘어, 아키텍처 레벨에서의 패러다임 전환에 가깝습니다.

RSC를 처음 접했을 땐 헷갈리고 복잡했지만, 사용하면서 느낀 가장 큰 변화는 "불필요한 로직을 클라이언트로 보내지 않아도 된다"는 점입니다. 이는 성능, 보안, 유지보수 모든 측면에서 의미 있는 진화입니다.

React를 사용하는 입장에서, 이제는 컴포넌트 단위로도 서버와 클라이언트 경계를 나눠야 합니다. 이 개념을 잘 이해하고 실전에서 활용한다면, 대규모 서비스에서도 훨씬 명확하고 성능 좋은 구조를 유지할 수 있다고 생각합니다.


참고 링크

'언어(JS,TS) > React' 카테고리의 다른 글

React [note: 컴포넌트 코드 순서 컨벤션]  (0) 2025.03.31
React [정보 : Link vs <a>태그]  (0) 2022.03.25