Server component === SSR ?
Client component === CSR ?
Next.js app router를 사용해서 미니 프로젝트를 만들일이 있어 공식문서를 보다가 client component와 server component의 개념이 헷갈리기도 하고 각각 어떻게 렌더링이 되는지 궁금해서 글로 정리하고자 합니다.
작성한 글을 모두 Next.js 공식문서를 기반으로 작성했습니다.
Next.js는 서버 컴포넌트를 default로 사용합니다. Server component를 사용하면 서버에서 HTML을 렌더링하기 때문에 빠르게 화면을 볼 수 있다는 장점이 있습니다.
서버에서 Next.js는 React의 API를 사용하여 렌더링을 하는데 렌더링 작업은 개별 route segments(page)와 Suspense boundary가 청크형태로 분할 됩니다.
각 청크는 두 단계로 렌더링이 진행됩니다.
그리고 클라이언트에서는
그림으로 나타내면 아래와 같습니다.
RSC Payload는 렌더링 된 React 서버 컴포넌트 트리의 압축된 바이너리 표현입니다. 클라이언트 상에서 React가 브라우저의 DOM을 업데이트 하기 위해 사용됩니다. RSC Payload는 아래 요소들을 포함하는데
1. 서버 컴포넌트가 렌더링 된 결과물
2. 클라이언트 컴포넌트가 어디에서 렌더링 되어야하는지 표식과 그것들의 자바스크립트 파일들
3. 서버 컴포넌트에서 클라이언트 컴포넌트로 넘겨줘야 하는 props들
즉 위 그림을 토대로 이해한바로 의하면 RSC Payload는 오렌지부분은 이미 렌더링 되어있고 파란색부분은 렌더링 되어야하기 때문에 클라이언트 컴포넌트라는 표식과 파일이 위치한다.
Next.js에서 사용되는 JavaScript instruction 같은 경우 HTML을 렌더링 하기 위해 필요한 명령이나 절차를 의미합니다.
네 맞습니다. 서버컴포넌트는 SSR이 아닙니다.
SSR은 초기 렌더링을 서버에서 처리하고 HTML파일을 반환 후 클라이언트에서 하이드레이션 작업을 통해 상호작용을 추가하는 반면, RSC는 서버에서 생성된 HTML, RSC Payload를 클라이언트로 전송하고 렌더링 및 확장을 수행합니다.
그렇다면 클라이언트 컴포넌트는 무엇일까요? 저는 처음에 이름때문에 CSR렌더링 방식을 수행하는 컴포넌트인줄 알았습니다. 하지만 공식문서를 읽어보니 클라이언트 컴포넌트는 서버에서 prerendered된 UI를 받아오고 JavaScript를 브라우저에서 실행할 수 있는 컴포넌트라고 나와있습니다.
결국 클라이언트 컴포넌트는 렌더링 주체가 전적으로 브라우저인 CSR방식을 사용하지 않는 것을 알 수 있습니다.
Next.js app router는 서버 컴포넌트가 default이기 때문에 클라이언트 컴포넌트같은 경우 파일 상단에 'use client'라는 지시어를 추가해줘야 합니다.
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
'use client'는 서버와 클라이언트 컴포넌트 모듈사이의 바운더리를 선언하기 위해 사용됩니다. 이것의 의미는 'use client'를 명시함으로써, imported되는 모든 모듈들은 (자식 컴포넌트) 클라이언트 번들에 포함이 됩니다.
'use client'는 클라이언트에서 렌더링 될 모든 컴포넌트 상단에 명시하지 않아도 됩니다. ex) Button, Toggle etc...
초기 페이지 로드를 최적화하기 위해 Next.js는 React의 API를 사용하여 클라이언트 및 서버 컴포넌트 모두에 대해 서버에서 정적 HTML을 미리 렌더링합니다. 즉, 사용자가 애플리케이션을 처음 방문하면 클라이언트가 JavaScript 번들을 다운로드하고 구문 분석하고 실행할 때까지 기다릴 필요 없이 즉시 페이지의 콘텐츠를 볼 수 있습니다.
후속 탐색 시 클라이언트 컴포넌트는 서버에서 미리 렌더링 된 HTML없이 클라이언트에서 완전히 렌더링됩니다. 이는 JavaScript 번들이 다운로드되고 구문 분석됨을 의미합니다. 번들이 준비되면 React는 RSC Payload를 사용하여 클라이언트 및 서버 컴포넌트 트리를 조정하고 DOM을 업데이트합니다.
위 그림은 각 컴포넌트마다 어떤 것을 알 수 있는지 정리한 표입니다.
저는 보통 인터렉션이 가능하냐 안하냐에 따라 컴포넌트를 구분합니다.
서버에서 사용할 수 없는 React Context를 사용하거나 데이터를 props로 전달하는 대신, 동일한 데이터에 대한 중복 요청을 걱정할 필요 없이 fetch API 또는 React의 cache 함수를 사용하여 필요한 구성 요소에서 동일한 데이터를 가져올 수 있습니다.
JavaScript 번들 크기를 줄이려면 클라이언트 컴포넌트를 트리 아래로 이동하는 것이 좋습니다.
예를 들어 어떤 Layout에 인터렉티브 요소를 추가해야 할 때 Layout을 클라이언트 컴포넌트로 변경하는 대신, 인터렉티브 요소는 클라이언트 컴포넌트로 만들어 Layout에서 import해옴으로서 전체적인 Layout은 서버 컴포넌트로 유지시킬 수 있습니다.
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
서버 컴포넌트를 클라이언트 컴포넌트에 끼워 넣는 방법
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
이 접근 방식을 사용하면 ClientComponent와 ServerComponent가 분리되어 독립적으로 렌더링될 수 있습니다. 이 경우 ClientComponent가 클라이언트에 렌더링되기 훨씬 전에 하위 ServerComponent가 서버에 렌더링될 수 있습니다.
맨 처음에 제가 렌더링 방식을 비교한 것 기억하시나요? 결과는 모두 틀렸습니다.
Server component !== SSR
Client component !== CSR
Server component와 Client component 모두 RSC Payload를 기반으로 생성 되고 Client component는 하이드레이션을 통해 추가적으로 인터렉션을 위한 작업이 진행됩니다.
개인적으로 각각 컴포넌트가 어떤 컴포넌트인지 알 수 있는 dev tool이 얼른 나왔으면 좋겠네요.
마지막으로 부족한 글 읽어주셔서 감사합니다.
수정되어야할 부분이 있다면 편하게 댓글 달아주세요!