반응형
4. Composition Patterns
- React 애플리케이션을 만들 때, 어떻게 구분하여 Server 혹은 Client에 렌더링할 지 고려해야 한다.
- Server Components와 Client Components의 여러 조합 패턴을 소개한다.
4-1. When to use Server and Client Components

- Server Components: Data Fetch, backend 리소스 접근, 민감한 정보 숨기기, 서버에 종속이 되어 있고 Client의 Javascript 번들 크기를 줄이고 싶을 때.
- Client Components: 상호작용성과 Event listener가 있을 때, state와 effect 등 hook을 사용할 때, browser API가 필요할 때, state와 effect 또는 browser API에 의존적인 Custom hook을 사용할 때
4-2. Server Components Partterns
- 클라이언트 측 렌더링을 선택하기 전에 서버에서 Data Fetching, database 접근 등과 같이 백엔드 서비스를 사용하고 싶을 때 쓸 수 있는 Server Components 패턴이 있다.
1. Sharing data between components
- 서버에서 Data Fetching 시 데이터를 다른 컴포넌트와 공유하고 싶은 상황이 있을 수 있다.
- React Context를 사용하거나 데이터를 props로 넘겨주는 대신, fetch 또는 React cache 함수를 이용해 같은 데이터를 필요로 하는 컴포넌트에서 중복 요청을 걱정하지 않고 fetching 할 수 있다.
- React는 fetch를 자동적으로 memize 하기 때문이며, fetch가 사용 불가능할 때는 React의 cache를 사용하면 된다.
2. Keeping Server-only Code out of the Client Environment
- Javascript로 작성된 모듈은 Server Components와 Client Components 간 공유가 가능하므로, 의도치 않게 서버에서 실행되어야 할 코드가 Client에서 실행되는 상황이 발생할 수 있다.
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
- 위 코드는 언뜻 보기에 Server와 Client에서 동작하는 함수처럼 보일 수 있지만, API_KEY라는 민감한 정보가 포함되어있으므로 Server에서 실행하도록 작성된 함수이다.
- NEXT_PUBLIC 접두사가 붙지 않은 환경 변수 접근이므로 Server에서만 접근할 수 있고, Client에서 호출 시 노출을 막고자 빈 문자열을 사용하게 된다.
- 결과적으로 Client에서 사용이 가능하지만, 원하는 결과를 얻을 수 없으며 환경 변수 접근을 NEXT_PUBLIC을 통해 사용하면 민감한 정보가 Client에 노출되게 된다.
- Server 전용 코드의 의도되지 않은 Client에서의 사용을 방지하기 위해서 server-only 패키지를 사용하면, build time에 에러가 발생시켜 막을 수 있다.
npm install server-only // 패키지 설치
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
- server-only 패키지와 비슷하게 client-only 패키지도 존재하며, window 객체와 같이 Client에서만 실행되어야 하는 코드에서 사용할 수 있다.
3. Using Third-party Packages and Providers
- Server Components가 React의 신규 기능으로 등장한 뒤, third-party packages와 Providers의 환경 설정에서 "use client" 지시어를 추가할 것을 안내하고 있다.
- 보통 Client에서 실행되는 useState, useEffect 등과 같은 기능이 포함되어 있는 경우.
- 현재 npm package의 대다수가 Client-only 기능에서 지시어가 빠져 있으므로, 지시어를 붙여야 정상적으로 동작하고 Server Components에서는 동작하지 않는다.
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}
- "use client" 지시어로 Client Components임을 지정하면, 정상 동작.
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
- 기본 Server Components에서 사용하는 경우, Client 관련 API 정보를 알 수 없어 에러가 발생.
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
- 이렇게 별도 모듈로 분리해서 Client 환경에서 실행되는 모듈임을 "use client" 지시어로 지정하고.
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
- 이렇게 Server Components에서 사용하면 정상적으로 사용할 수 있다.
- 대부분의 third-party 모듈은 Client Components에서 사용될 것이기 때문에 문제가 되지 않지만, Providers는 보통 State, Context와 관계가 있고 애플리케이션 root에서 불러오기 때문에 예외적으로 처리가 필요하다.
4. Using Context Providers
- Context providers는 말 그대로 전체적인 관심사를 애플리케이션에 공유하기 위해 root 근처에서 렌더링된다.
- React Context가 Server Components에서 지원되지 않으므로, 애플리케이션 root에서 에러가 발생할 수 있다.
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
- 이를 해결하기 위해선, 마찬가지로 해당 context와 렌더링을 Client Components에서 수행해야 한다.
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
- Context 관련 코드가 Client Components 내부로 이동했기 때문에, 경계가 생겨 에러가 발생하지 않는다.
- 애플리케이션 전반에 걸쳐 Context를 Client Components들 끼리 공유할 수 있게 된다.
4-3. Client Components
1. Moving Client Components Down the Tree
- Client의 Javascript 번들 크기를 줄이기 위해서, Client Components는 컴포넌트 트리의 가능한 가장 깊게 위치 시키는 것이 좋다.
- 예를 들어, 정적 리소스를 포함하는 레이아웃이 있고, 상태를 이용해서 상호 작용가능한 검색창이 있을 때 레이아웃 전체를 Client Components로 만들기보다 검색창만 상호 작용 로직을 포함시켜 Client Components로 만들고 레이아웃은 Server Components로 유지시키는 것이 좋다.
- 이렇게 하면 레이아웃의 레이아웃 컴포넌트의 모든 Javascript를 Client에 전송하지 않아도 된다.
// 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>
</>
)
}
- 레이아웃 컴포넌트와 logo 컴포넌트는 정적으로 제공되므로 Javascript를 전달하지 않고, SearchBar 컴포넌트만 Javascript를 전달한다.
2. Passing Props from Server to Client Components(Serialization)
- 만약 Server Components에서 데이터를 fetching한 뒤, Client Components에 props로 전달하고 싶을 수 있다.
- 이 때, props는 직렬화가 가능해야하고 만약 불가능하다면 Route Handler 또는 타사 라이브러리를 이용해 데이터를 fetching 해야 한다.
4-4. Interleaving Server and Client Components (혼합 사용)
- Server Components와 Client Components를 혼용하여 사용하면, 컴포넌트 트리 형태로 UI를 시각화하는 데 도움이 된다.
- Server Component인 root layout부터 시작해서 "use client" 지시어를 활용해서 Client에서 특정 서브트리를 렌더링 할 수 있다.
- 이렇게 하면, 이 서브트리 하위에서도 Server Actions를 호출하거나 Server Components를 중첩할 수 있다. 그러나 몇 가지 주의해야 할 점이 있다.
- 요청-응답 생명주기 과정에서 코드는 server -> client로 동작한다. 만약 Client에서 Server의 데이터 및 리소스에 접근하고자 한다면 새로운 요청을 서버에 보내야 한다.
- Server Components를 요청하면, 모든 Server Components가 먼저 렌더링되고 (Client Components가 중첩되어 있는 컴포넌트도 포함) 렌더링 결과(RSC Payload)에 Client Components의 위치 정보를 포함하여 Client에서 Server Components와 Client Components를 하나의 트리로 재조정 한다.
- Client Components는 Server Components 이후에 렌더링되므로, Client Components에서 Server Components를 import 하여 사용할 수 없다. 왜냐하면, 이미 Server Components를 렌더링 했는데, 이후 Client Components에서 import한다면 다시 서버에 요청을 보내야 한다. 대신, Server Components를 props로 Client Components에 내려주어야 한다.
1. Unsupported Pattern: Importing Server Components into Client Components
'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 />
</>
)
}
- Client Components에서 Server Components를 import하는건 불가능하다.
2. Supported Pattern: Passing Server Components to Client Components as Props
'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}
</>
)
}
- 일반적으로 사용하는 children slot은 Client Components에 Props로 전달되는 패턴이다.
- slot 개념을 활용해, 무엇이 위치할 지는 모르지만 어디에 위치시킬 지만 결정하면 된다.
// 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>
)
}
- 부모 컴포넌트에서 Client Component의 children props로 Server Components를 전달하면, 앞서 정해진 위치에 Server Components가 그려진다.
- 이렇게 하면 Client Components와 Server Components가 분리되어 독립적으로 렌더링될 수 있고, 부모 컴포넌트인 Client Components에서 재 렌더링이 발생하더라도 자식 컴포넌트인 Server Components는 렌더링이 발생하지 않는다.
처음 뵙겠습니다.
Next.js 사용하면서 처음 본 개념이거나, 이해하기 어려운 부분(?)은 붉은색 볼드처리 해봤다.
Preference
Rendering: Composition Patterns | Next.js
Recommended patterns for using Server and Client Components.
nextjs.org