junyeokk
Blog
Next.js·2025. 10. 09

React Server Component

Server Component란

React Server Component(RSC)는 서버에서만 실행되는 React 컴포넌트다. Next.js App Router에서는 모든 컴포넌트가 기본적으로 Server Component다.

Server Component와 Client Component 비교

위 예시에서 파란색 박스는 Server Component, 초록색 박스는 Client Component다. Server Component는 서버에서만 렌더링되어 브라우저로 JavaScript가 전송되지 않는다. Client Component는 인터랙션을 위해 JavaScript가 필요하므로 브라우저로 전송된다.

Server Component는 서버에서 렌더링을 완료한 뒤 결과 HTML만 클라이언트로 전송한다. JavaScript 번들에 포함되지 않으므로 번들 크기가 줄어든다.

typescript
import { db } from '@/lib/database'

export default async function ProductList() {
  const products = await db.query('SELECT * FROM products')

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

컴포넌트 함수를 async로 선언하고 데이터베이스를 직접 조회할 수 있다. 이 코드는 서버에서만 실행되므로 데이터베이스 연결 정보가 클라이언트에 노출되지 않는다.

Server Component의 제약사항

Server Component는 서버에서만 실행되기 때문에 클라이언트 전용 기능을 사용할 수 없다.

  • 상태 관리 훅: useState, useReducer
  • 생명주기 훅: useEffect, useLayoutEffect
  • 이벤트 핸들러: onClick, onChange
  • 브라우저 전용 API: window, localStorage, document
  • React Context: useContext

이런 기능이 필요하면 Client Component로 만들어야 한다.

Client Component

Client Component는 브라우저에서 실행되는 React 컴포넌트다. 파일 최상단에 'use client' 디렉티브를 추가하면 된다.

typescript
'use client'

import { useState } from 'react'

export default function LikeButton() {
  const [likes, setLikes] = useState(0)

  return (
    <button onClick={() => setLikes(likes + 1)}>
      좋아요 {likes}
    </button>
  )
}

'use client'는 서버와 클라이언트 코드 사이의 경계를 선언한다. 이 디렉티브가 있는 파일과 그 파일이 import하는 모든 모듈은 클라이언트 번들에 포함된다.

언제 무엇을 사용할지

작업Server ComponentClient Component
데이터 가져오기
백엔드 리소스 직접 접근
민감한 정보를 서버에 보관
상태 관리 (useState)
이벤트 핸들러
브라우저 전용 API

기본적으로 Server Component를 사용하고, 인터랙션이 필요한 부분만 Client Component로 분리한다.

조합 패턴

Server Component에서 Client Component로 데이터 전달

Server Component는 Client Component에 props로 데이터를 전달할 수 있다. 단, 직렬화 가능한 값만 전달할 수 있다.

typescript
// page.tsx (Server Component)
import LikeButton from './like-button'

export default async function PostPage({ params }) {
  const post = await getPost(params.id)

  return (
    <article>
      <h1>{post.title}</h1>
      <LikeButton initialLikes={post.likes} />
    </article>
  )
}
typescript
// like-button.tsx (Client Component)
'use client'

import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <button onClick={() => setLikes(likes + 1)}>
      좋아요 {likes}
    </button>
  )
}

Client Component 안에 Server Component 넣기

Client Component는 Server Component를 직접 import할 수 없다. children prop을 통해 받아야 한다.

typescript
// modal.tsx (Client Component)
'use client'

export default function Modal({ children }) {
  return <div className="modal">{children}</div>
}
typescript
// page.tsx (Server Component)
import Modal from './modal'
import ServerContent from './server-content'

export default function Page() {
  return (
    <Modal>
      <ServerContent />
    </Modal>
  )
}

주의사항

Client Component는 서버와 클라이언트 양쪽에서 실행된다

Server Component는 서버에서만 실행되지만, Client Component는 양쪽에서 실행된다. 서버에서 초기 HTML 생성을 위해 1번, 클라이언트에서 하이드레이션을 위해 1번 실행된다.

따라서 window, localStorage 같은 브라우저 전용 API는 useEffect 안에서 사용해야 한다.

typescript
'use client'

export default function MyComponent() {
  // 에러: 서버에서 실행 시 window가 없음
  const width = window.innerWidth

  // useEffect는 클라이언트에서만 실행됨
  useEffect(() => {
    const width = window.innerWidth
  }, [])
}

Client Component는 트리 말단에 배치

상위에 Client Component가 있으면 그 하위의 모든 컴포넌트가 JavaScript 번들에 포함된다. 인터랙션이 필요한 최소한의 부분만 Client Component로 만든다.

typescript
// 비효율적: 전체가 Client Component
'use client'

export default function Page() {
  const [open, setOpen] = useState(false)

  return (
    <div>
      <Header />
      <Content />
      <Modal open={open} />
    </div>
  )
}
typescript
// 효율적: 필요한 부분만 Client Component
export default function Page() {
  return (
    <div>
      <Header />
      <Content />
      <ModalButton />  {/* 이것만 Client Component */}
    </div>
  )
}