클라이언트 측 렌더링을 선택하기 전에, 데이터를 가져오거나 데이터베이스나 백엔드 서비스에 접근하는 등의 작업을 서버에서 먼저 수행할 수 있다.
서버에서 데이터를 가져올 때, 서로 다른 컴포넌트 간에 데이터를 공유해야하는 경우가 있을 수 있다. 예를 들어, 동일한 데이터에 의존하는 레이아웃과 페이지가 있을 수 있다.
React Context나 데이터를 props로 전달하는 대신, fetch 또는 React의 cache 함수를 사용하여 필요한 컴포넌트에서 동일한 데이터를 가져올 수 있다.
이렇게 하면 동일한 데이터를 위한 중복 요청을 걱정할 필요가 없다. React가 fetch를 확장하여 데이터 요청을 자동으로 메모이즈하고, fetch가 사용 불가능할 경우 cache 함수를 사용할 수 있기 때문이다.
Javascript 모듈은 서버 컴포넌트와 클라이언트 컴포넌트 모듈 간에 공유될 수 있기 때문에, 원래 서버에서만 실행될 의도로 작성된 코드가 클라이언트로 밀려들어갈 수 있다.
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
겉보기에는 getData 함수가 서버와 클라이언트에서 모두 작동하는 것처럼 보인다. 그러나 이 함수에는 API_KEY가 포함되어 있으며, 이는 오직 서버에서만 시행되도록 의도된것이다.
환경변수 API_KEY가 NEXT_PUBLIC 접두어로 시작하지 않기 때문에, 이는 서버에서만 접근할 수 있는 비공개 변수이다. Next.js는 환경 변수가 클라이언트로 유출되는 것을 방지하기 위해, 비동개 환경 변수르 빈 문자열로 대체한다.
그 결과, getData() 함수를 클라이언트에서 가져와 실행할 수 있지만, 예상대로 동작하지 않는다.
변수를 공개하면 클라이언트에서 함수가 작동하겠지만, 이는 민감한 정보를 노출하는 것이므로 바람직하지 않다.
이러한 서버 코드가 의도치 않게 클라이언트에서 사용되는 것을 방지하기 위해, server-only ㅍ패키지를 사용할 수 있다.
이를 통해 개발자가 이러한 모듈을 클라이언트 컴포넌트에서 실수로 가져올 경우 빌드 타임 오류가 발생하도록 할 수 있다.
server-only를 사용하려면 패키지를 설치해야한다.
npm install server-only
그런 다음 해당 패키지를 server-only code 만 포함되어야하는 모듈에 import 한다.
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()
}
이제 getData()를 가져오는 모든 클라이언트 컴포넌트는 해당 모듈이 서버에서만 사용될 수 있다는 빌드 타임 오류를 받는다.
이에 대응하는 client-only 패키지는 클라이언트에서만 실행되어야하는 모듈을 표시하는 데 사용할 수 있다. 예를 들어, window 객체에 접근하는 코드가 이에 해당한다.
서버 컴포넌트는 React의 새로운 기능이므로, 생태계 내의 서드파티 패키지와 프로바이더들도 useState, useEffect, createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 "use client" 지시어를 추가하기 시작한 단계이다.
현재 많은 npm 패키지의 클라이언트 전용 기능을 사용하는 컴포넌트에는 아직 "use client" 지시어가 없다. 이러한 서드파티 컴포넌트는 "use client" 지시어가 있는 클라이언트 컴포넌트 내에서는 정상적으로 작동하지만, 서버 컴포넌트 내에서는 작동하지 않는다.
예를 들어, 가상의 acme-carousel 패키지를 설치했다고 가정해보자. 이 패키지에는 Carousel 컴포넌트가 포함되어있으며, 이 컴포넌트는 useState를 사용하지만 아직 "use client" 지시어가 추가되지 않았다.
이 경우, Carousel을 클라이언트 컴포넌트 내에서 사용하면 정상적으로 동작한다.
'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>
)
그러나 만약 서버컴포넌트 내에서 직접 사용한다면 에러를 보게 된다.
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>
)
Next.js 는 Carousel이 client-only features 인것을 모르기 때문이다.
이것을 수정하기 위해 클라이언트 전용 기능을 사용하는 서드파티 컴포넌트를 직접 만든 클라이언트 컴포넌트로 감싸서 사용하면 된다.
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
이제 Carousel을 Server Component에서 직접 사용할 수 있다.
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
대부분의 서드파티 컴포넌트를 감쌀 필요가 없다. 일반적으로 클라이언트 컴포넌트 내에서 사용되기 때문이다. 하지만 프로바이더(Providers)는 감싸야 할 수 있다.
프로바이더는 React의 상태(state)와 컨텍스트(context)에 의존하며, 일반적으로 애플리케이션 루트에서 필요하기 때문이다.
Context Provider는 전형적으로 애플리케이션의 루트에서 렌더 되어, 현재 테마와 같은 전역 상태를 공유하는데 사용된다.
그러나 React 컨텍스트는 서버 컴포넌트에서 지원되지 않으므로, 애플리케이션의 루트에서 컨텍스트를 생성하려고 하면 오류가 난다.
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>
)
}
해결하기 위해서 Client Component 내에서 context를 생성하고 provider를 render 하라.
'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>
}
이제 서버 컴포넌트는 provider를 직접 사용할 수 있다.
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
루트에서 Provider가 렌더링 되면, 애플리케이션 전반에 걸쳐 있는 모든 클라이언트 컴포넌트가 해당 컨텍스트를 사용할 수 있다.
프로바이더는 가능한 한 트리의 깊은 곳에서 렌더링하는 것이 좋다.
예를 들어 ThemeProvider가 전체 html 문서를 감싸는 대신 {children}만 감싸도록 설계된 것을 확인할 수 있다.
이렇게 하면 Next.js가 서버 컴포넌트의 정적 부분을 더욱 효과적으로 최적화 할 수 있다.
클라이언트 Javascript 번들 크기를 줄일려면, 클라이언트 컴포넌트를 컴포넌트 트리의 더 아래쪽으로 이동시키는 것이좋다.
예를 들어 레이아웃(Layout)에는 로고, 링크 등과 같은 정적 요소가 포함될 수 있으며, 상태를 사용하는 인터랙티브한 검색 창이 포함될 수 있다.
이 경우, 레이아웃 전체를 클라이언트 컴포넌트로 만들기보다는, 인터랙티브한 로직을 별도의 클라이언트 컴포넌트(예: SearchBar) 로 분리하고, 레이아웃을 서버 컴포넌트로 유지하는 것이 좋다.
이렇게하면 레이아웃의 모든 Javascript 를 클라이언트로 전송할 필요 없이, 필요한 부분만 최적화 하여 전송할 수 있다.
// 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>
</>
)
서버 컴포넌트에서 데이터를 가져오는 경우, 데이터를 클라이언트 컴포넌트 props로 전달하고 싶을 수있다. 서버에서 클라이언트 컴포넌트로 전달 되는 Props는 React에 의해 직렬화 가능해야한다.
만약 클라이언트 컴포넌트가 직렬화 할 수 없는 데이터에 의존한다면, 서드파티 라이브러리를 사용하여 클라이언트에서 데이터를 가져오거나, 서버에서 라우트 핸들러(Route Handler)를 통해 데이터를 가져올 수 있다.
클라이언트 컴포넌트와 서버 컴포넌트를 교차 사용할 때, UI를 컴포넌트 트리로 시각화하는 것이 유용할 수 있다.
루트 레이아웃이 서버 컴포넌트인 상태에서, "use client" 지시어를 추가하여 특정 하위 트리의 컴포넌트를 클라이언트에서 렌더링 할 수 있다.
클라이언트 하위 트리 내에서는 여전히 서버 컴포넌트를 중첩하거나 서버 액션을 호출 할 수 있지만 유의사항이 있다.
1. 요청-응답 라이프사이클동안, 코드가 서버에서 클라이언트로 이동한다. 클라이언트에서 서버의 데이터나 리소스에 접근해야 할 경우, 서버로의 새로운 요청을 보내게 되며, 서버와 클라이언트를 상호 전환하는 것이 아니다.
새로운 요청이 서버로 전달되면, 모든 서버 컴포넌트가 먼저 렌더링 된다.
여기에는 클라이언트 컴포넌트 안에 중첩된 서버 컴포넌트도 포함된다. 렌더링 결과 (RSC Payload)는 클라이언트 컴포넌트의 위치를 참조하는 정보를 포함하게 된다. 이후, 클라이언트에서 React가 RSC Payload를 사용하여 서버 컴포넌트와 클라이언트 컴포넌트를 하나의 트리로 합친다.
클라이언트 컴포넌트는 서버 컴포넌트 이후에 렌더링 되므로, 서버 컴포넌트를 클라이언트 컴포넌트 모듈에 가져올 수 없다. 대신, 서버 컴포넌트를 클라이언트 컴포넌트의 props로 전달 할 수 있다.
Server Component를 Client Component 에 import 할 수 없다.
'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 />
</>
)
}
Server Components를 Client Component 의 Props로 전달 할 수 있다.
이 일반적인 패턴은 React 의 children prop 을 사용하여 클라이언트 컴포넌트에서 "슬롯(slot)"을 만드는 것이다.
'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}
</>
)
}
ClientComponent는 children이 서버 컴포넌트의 결과로 채워질 것임을 알지 못한다. ClientComponent의 유일한 책임은 children이 어디에 배치될지 결정하는 것이다.
부모 서버 컴포넌트에서는 ClientComponent와 ServerComponent를 모두 가져와서 ClientComponent의 자식으로 ServerComponent를 전달 할 수있다.
// 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는 서로 분리되어 독립적으로 렌더링 된다. 이 경우, 자식인 ServerComponent는 클라이언트에서 ClientComponent가 렌더링되기 훨씬 이전에 서버에서 렌더링 될 수 있다.