React Server Component
서버에서만 실행되는 컴포넌트로 JavaScript 번들 크기를 줄이고, 데이터베이스 직접 접근과 민감 정보 보호를 가능하게 하는 React의 새로운 렌더링 모델이다.
Server Component란
React Server Component(RSC)는 서버에서만 실행되는 React 컴포넌트다. Next.js App Router에서는 모든 컴포넌트가 기본적으로 Server Component다.
위 예시에서 파란색 박스는 Server Component, 초록색 박스는 Client Component다. Server Component는 서버에서만 렌더링되어 브라우저로 JavaScript가 전송되지 않는다. Client Component는 인터랙션을 위해 JavaScript가 필요하므로 브라우저로 전송된다.
Server Component는 서버에서 렌더링을 완료한 뒤 결과 HTML만 클라이언트로 전송한다. JavaScript 번들에 포함되지 않으므로 번들 크기가 줄어든다.
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' 디렉티브를 추가하면 된다.
'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 Component | Client Component |
|---|---|---|
| 데이터 가져오기 | ✓ | |
| 백엔드 리소스 직접 접근 | ✓ | |
| 민감한 정보를 서버에 보관 | ✓ | |
상태 관리 (useState) | ✓ | |
| 이벤트 핸들러 | ✓ | |
| 브라우저 전용 API | ✓ |
기본적으로 Server Component를 사용하고, 인터랙션이 필요한 부분만 Client Component로 분리한다.
조합 패턴
Server Component에서 Client Component로 데이터 전달
Server Component는 Client Component에 props로 데이터를 전달할 수 있다. 단, 직렬화 가능한 값만 전달할 수 있다.
// 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>
)
}
// 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을 통해 받아야 한다.
// modal.tsx (Client Component)
'use client'
export default function Modal({ children }) {
return <div className="modal">{children}</div>
}
// 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 안에서 사용해야 한다.
'use client'
export default function MyComponent() {
// 에러: 서버에서 실행 시 window가 없음
const width = window.innerWidth
// useEffect는 클라이언트에서만 실행됨
useEffect(() => {
const width = window.innerWidth
}, [])
}
Client Component는 트리 말단에 배치
상위에 Client Component가 있으면 그 하위의 모든 컴포넌트가 JavaScript 번들에 포함된다. 인터랙션이 필요한 최소한의 부분만 Client Component로 만든다.
// 비효율적: 전체가 Client Component
'use client'
export default function Page() {
const [open, setOpen] = useState(false)
return (
<div>
<Header />
<Content />
<Modal open={open} />
</div>
)
}
// 효율적: 필요한 부분만 Client Component
export default function Page() {
return (
<div>
<Header />
<Content />
<ModalButton /> {/* 이것만 Client Component */}
</div>
)
}
왜 Server Component인가
기존에도 SSR이 있었지만, SSR은 초기 HTML만 서버에서 만들고 하이드레이션을 위해 전체 JavaScript를 클라이언트로 보내야 했다. Server Component는 근본적으로 다르다. 서버에서 실행된 컴포넌트의 JavaScript는 아예 클라이언트로 전송되지 않는다.
getServerSideProps/getStaticProps 패턴은 페이지 단위로만 데이터를 가져올 수 있었고, 컴포넌트별 독립적인 데이터 페칭이 불가능했다. Server Component는 컴포넌트 단위로 서버 실행 여부를 결정할 수 있어서, 데이터가 필요한 컴포넌트가 직접 데이터를 가져오는 colocation이 가능하다.
React Query 같은 클라이언트 캐싱 라이브러리도 좋은 해법이지만, 초기 번들에 라이브러리 코드가 포함되고 클라이언트에서 추가 요청이 발생한다. Server Component는 이 두 가지를 모두 제거한다.
정리
- 기본값이 Server Component이므로 인터랙션이 필요한 최소 단위만 'use client'로 분리한다
- Client Component는 서버와 클라이언트 양쪽에서 실행되므로 브라우저 API는 useEffect 안에서 사용해야 한다
- children prop 패턴으로 Client Component 안에 Server Component를 조합할 수 있다