[Next.js] Server and Client Composition Pattern

서버 vs 클라이언트 컴포넌트

Next.js 문서에서는 아래와 같은 기준으로 서버와 클라이언트 컴포넌트를 사용하는 것을 제시하고 있습니다.

 

서버 컴포넌트 패턴

서버의 경우 데이터 불러오기, 백엔드 접속 그리고 데이터베이스 접근과 같은 업무를 수행하는데 사용됩니다. 다음은 서버에서 어떠한 작업을 하는 지에 대한 패턴을 보도록 하겠습니다.

 

컴포넌트 간의 데이터 공유

하나의 페이지를 구성하는 여러 컴포넌트가 존재하는데, 이들은 하나의 데이터를 기반으로 렌더링이 되는 컴포넌트들 입니다.

이러한 경우, 서버에서는 fetch 또는 리액트 cache 기능을 사용하여 재반복 되는 요청을 지속적으로 불러오는 형식으로 공유가 가능합니다. (캐시가 되어진 데이터를 불러오는 것이므로 중복성 문제가 발생하지 않습니다.)

해당 방식이 가능한 이유는 fetch 요청을 수행할 시 자동적으로 요청에 대한 응답을 캐싱하고 이후 fetch가 안될 때, 자동으로 캐시에 있는 데이터를 가져오도록 리액트가 설계를 해두었기에 가능합니다.

 

클라이언트의 서버 코드 사용 제한

모든 자바스크립트는 서버나 클라이언트 상관 없이 사용할 수 있도록 설계되어져 있습니다. 그런데 여기서, 서버에서만 사용 가능한 코드로 작성을 하였을 때는 어떻게 해야 클라이언트가 서버 코드를 사용하지 못하게 할 수 있을지 알아 보도록 합시다.

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
  return res.json()
}

위의 코드는 데이터 요청을 보내는 코드로 서버와 클라이언트 둘 다 사용 가능한 코드 입니다. 여기서 주의 깊게 봐야 할 점은 환경 변수 인데, API_KEY 자체를 사용하는 것으로 이것은 서버에서만 환경 변수를 사용 가능하게 되어져 있습니다. 다른 말로는 클라이언트에서 해당 메소드를 실행 하게 되면 API_KEY에 대한 값이 없으므로 오류가 발생 하게 됩니다.

 

해당 코드를 클라이언트에서도 사용 가능하게 하려고 하면 NEXT_PUBLIC이라는 접두사를 환경 변수에 입력을 하게 되면 사용 가능하게 되어집니다. 하지만 이것은 서버에서만 사용 가능한 코드로 볼 수 없게 되어집니다. 그렇다면 서버 전용 코드로 만들기 위해서는 어떤 작업이 필요한지 보도록 하겠습니다.

 

먼저 아래 모듈을 설치 합니다

npm install server-only
 

이후 페이지 상단에 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()
}

반대로, 클라이언트 전용 코드를 만들고자 하면 client-only 모듈을 사용하면 됩니다. 그리고 해당 코드에서는 window 오브젝트 사용이 가능하게 되어집니다.

 

Third-party Packages 사용

다른 모듈에서 제공하는 클라이언트 컴포넌트를 서버 컴포넌트에서 사용하고자 하면 어떻게 해야 할지 알아 보도록 하겠습니다. 아래 <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>
  )
}

위의 컴포넌트는 useState를 사용하기 위해서 클라이언트 컴포넌트로 지정된 상태입니다. 단순하게 바로 해당 컴포넌트를 사용하는 서버 컴포넌트에서 호출하여 사용해보면 아래와 같이 에러가 발생합니다.

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>
  )
}

그렇다면, 실제로 사용하기 위해서는 어떻게 해야 하는지 코드로 보겠습니다.

'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
import Carousel from './carousel'
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

위 코드와 같이 외부에서 제공되는 컴포넌트를 한번 더 클라이언트 컴포넌트로 감싸주고 다시 서버에 사용하게 되면 해당 문제를 해결 할 수 있습니다.

 

Provider 사용 방식

React Context의 경우 서버 컴포넌트에서 사용이 불가능 하므로, 해당 컴포넌트를 사용하는 컴포넌트를 클라이언트 컴포넌트로 지정하여 사용해야 합니다.

 

에러 발생 코드

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>
  )
}

수정된 코드

'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>
}

 

Client Component 심화

클라이언트 구성 요소를 트리의 아래로 이동

  1. 클라이언트 측 JavaScript 번들 크기를 줄이기 위해, 가능한 클라이언트 측 로직을 트리의 깊은 부분으로 이동시키는 것이 좋습니다.
  2. 예를 들어, 레이아웃 컴포넌트가 정적인 요소(로고, 링크 등)와 상태를 사용하는 인터랙티브 검색 바를 포함하는 경우, 전체 레이아웃을 클라이언트 구성 요소로 만드는 대신, 검색 바만 클라이언트 구성 요소로 분리하고 레이아웃은 서버 구성 요소로 유지합니다.
  3. 이렇게 하면 레이아웃의 모든 JavaScript를 클라이언트로 보내지 않아도 됩니다.

 

서버에서 클라이언트 구성 요소로 데이터 전달

  1. 서버 구성 요소에서 데이터를 가져와 클라이언트 구성 요소로 전달하려면, 전달되는 props가 React에 의해 직렬화 가능해야 합니다.
  2. 만약 클라이언트 구성 요소가 직렬화할 수 없는 데이터에 의존한다면, 클라이언트 측에서 데이터를 가져오거나, 서버의 라우트 핸들러를 통해 데이터를 가져와야 합니다.
// 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>
    </>
  )
}

구성 요소 트리 시각화

UI를 구성 요소 트리로 시각화하면 도움이 됩니다. 루트 레이아웃은 서버 구성 요소로 시작하며, 특정 하위 트리는 "use client" 지시문을 추가하여 클라이언트에서 렌더링할 수 있습니다.

 

클라이언트 하위 트리 내 서버 구성 요소

클라이언트 하위 트리 내에서도 서버 구성 요소를 중첩하거나 서버 액션을 호출할 수 있습니다. 그러나 몇 가지 주의 사항이 있습니다:

 

서버-클라이언트 간 데이터 액세스

요청-응답 주기 동안, 코드는 서버에서 클라이언트로 이동합니다. 클라이언트에서 서버 데이터나 리소스에 액세스하려면 새로운 서버 요청을 해야 합니다.

 

새로운 서버 요청이 이루어지면, 모든 서버 구성 요소가 먼저 렌더링됩니다. 렌더링 결과(RSC Payload)는 클라이언트 구성 요소의 위치에 대한 참조를 포함합니다. 클라이언트에서는 React가 RSC Payload를 사용하여 서버와 클라이언트 구성 요소를 하나의 트리로 결합합니다.

 

서버-클라이언트 간 구성 요소 불러오기 제한

클라이언트 구성 요소가 렌더링된 후에 서버 구성 요소를 다시 불러올 수 없습니다. 즉, 클라이언트 구성 요소 모듈에서 서버 구성 요소를 직접 가져올 수 없습니다. 대신, 서버 구성 요소를 클라이언트 구성 요소에 props로 전달할 수 있습니다.

 

 

지원하지 않는 패턴 - 서버 컴포넌트를 클라이언트 컴포넌트에서 호출

'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 />
    </>
  )
}

 

지원하는 패턴 - 클라이언트 컴포넌트의 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}
    </>
  )
}

클라이언트 컴포넌트는 서버 컴포넌트가 어디에 배치 되어져야 할지 위치를 지정해주고 있습니다.

// 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>
  )
}

'Frontend > Next.js' 카테고리의 다른 글

[Next.js] Routing 2부  (0) 2024.06.22
[Next.js] Routing 1부  (0) 2024.06.22
[Next.js] Client Components  (0) 2024.06.22
[Next.js] Server Components  (0) 2024.06.22
[Next.js] 왜 두 번 렌더링 되는 것일까?  (1) 2024.02.28