본문 바로가기

넥스트

[next.js] 16. 컴포지션 패턴

반응형
본 내용은 next.js 공식 사이트를 한글로 번역하여 정리한 것입니다. 정보 변경, 오역 등의 문제가 있을 수 있으니 자세한 내용은 공식 사이트를 활용해주세요

 

 

Rendering: Composition Patterns | Next.js

When building React applications, you will need to consider what parts of your application should be rendered on the server or the client. This page covers some recommended composition patterns when using Server and Client Components. Here's a quick summar

nextjs.org



여기서는 React 애플리케이션을 빌드할 때 Server 및 Client 구성 요소를 어떻게 고려해야 하는지에 대한 권장 구성 패턴에 대해 설명하고 있습니다. 아래는 Server 및 Client 구성 요소 사용 사례에 대한 간략한 요약입니다.


 언제 Server 및 Client 구성 요소를 사용해야 하나요?

작업 Server Component Client Component
데이터 가져오기 (Fetch data)
백엔드 리소스에 직접 액세스 (Access backend resources directly)
민감한 정보를 서버에 보관 (access tokens, API keys )
대규모 종속성을 서버에 유지하거나 클라이언트 측 JavaScript 축소 (Keep large dependencies on the server / Reduce client-side JavaScript)
상호 작용성 및 이벤트 리스너 추가 (onClick(), onChange() )
상태 및 라이프사이클 효과 사용 (useState(), useReducer(), useEffect() )
브라우저 전용 API 사용 (Use browser-only APIs)
상태, 효과 또는 브라우저 전용 API에 의존하는 사용자 정의 훅 사용 (Use custom hooks that depend on state, effects, or browser-only APIs)
React Class 컴포넌트 사용 (Use React Class components)

 Server 구성 요소 패턴

  컴포넌트 간 데이터 공유

서버에서 데이터를 가져올 때, 같은 데이터에 의존하는 레이아웃 및 페이지와 같은 다른 컴포넌트 간에 데이터를 공유해야 할 경우, React Context(서버에서 사용 불가)를 사용하거나 데이터를 프롭스로 전달하는 대신 fetch나 React의 캐시 함수를 사용하여 필요한 컴포넌트에서 동일한 데이터를 가져올 수 있습니다. React는 자동으로 데이터 요청을 메모화하기 때문에 동일한 데이터에 대한 중복 요청을 걱정하지 않아도 됩니다.

(개인정리) 다른 컴포넌트 간에 데이터 공유 시 Context API 나 fetch , 캐시 함수를 사용해 필요한 컴포넌트에서 동일 데이터터를 가져올 수 있다. 리액트는 자동으로 데이터 요청을 캐싱 처리하므로 동일 데이터에 대한 중복 요청에 대한 문제를 걱정할 필요 없다.

 

    서버 전용 코드를 클라이언트 환경에서 분리

JavaScript 모듈은 서버 및 클라이언트 구성 요소 모듈 간에 공유될 수 있으므로, 처음부터 서버에서 실행되도록 의도된 코드가 클라이언트로 스니핑되지 않도록 주의해야 합니다. 예를 들어 다음과 같은 데이터 가져오기 함수를 살펴보겠습니다.

// lib/data.ts

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

 

getData 함수는 처음에 서버에서만 실행되도록 의도된 API 키를 포함하고 있습니다. 따라서 환경 변수 API_KEY가 NEXT_PUBLIC로 접두사가 붙지 않았기 때문에 이 변수는 서버에서만 액세스할 수 있는 개인 변수입니다. 민감한 정보를 클라이언트에 노출하고 싶지 않을 수도 있으므로 이러한 비정상적인 클라이언트 사용을 방지하기 위해 server-only 패키지를 사용할 수 있습니다. 이 패키지를 사용하면 클라이언트 구성 요소에서 이러한 모듈 중 하나를 실수로 가져오면 빌드 시간 오류가 발생하도록 할 수 있습니다.

 

 server-only 를 사용하려면먼저 패키지를 설치하십시오.

npm install server-only
// lib/data.js

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 개체에 액세스하는 코드와 같이 클라이언트에서만 실행되는 코드가 포함된 모듈을 표시하는 데 사용할 수 있습니다.

(개인정리) 위에서 선언된 함수는 서버 컴포넌트에서만 사용되도록 목적을 가지고 만들어졌다. 따라서 클라이언트 컴포넌트에서 이러한 서버 컴포넌트 전용 모듈(함수)를 가져오는 실수를 하게 되는 것을 예방할 수 있도록 server-only 패키지를 Next.js 에서 제공해준다. 이를 사용하면 해당 행위를 하게 되는 순간 빌드 시간에 오류를 띄워 준다.

  타사 패키지 및 프로바이더 사용

서버 컴포넌트는 새로운 React 기능이므로, 이러한 컴포넌트에서 useState, useEffect 및 createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 "use client" 지시어를 추가하기 시작한 생태계의 타사 패키지 및 프로바이더가 나타나기 시작했습니다.

현재, 많은 npm 패키지에서 클라이언트 전용 기능을 사용하는 컴포넌트에는 아직 "use client" 지시어가 없습니다. 이러한 타사 컴포넌트는 "use client" 지시어가 있으므로 클라이언트 컴포넌트 내에서 예상대로 작동하지만 서버 컴포넌트 내에서는 작동하지 않습니다.

예를 들어 가상의 acme-carousel 패키지를 설치했다고 가정해보겠습니다. 이 패키지에는 <Carousel /> 컴포넌트가 있습니다. 이 컴포넌트는 useState를 사용하지만 아직 "use client" 지시어가 없습니다.

<ClientComponent> 내에서 <Carousel />를 사용하면 예상대로 작동합니다.

// app/gallery.tsx
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>
 
      {/* 클라이언트 컴포넌트 내에서 사용하므로 작동합니다 */}
      {isOpen && <Carousel />}
    </div>
  )
}

그러나 서버 컴포넌트 내에서 직접 사용하려고 하면 오류가 발생합니다.

// app/page.tsx
import { Carousel } from 'acme-carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/* 오류: `useState`는 서버 컴포넌트 내에서 사용할 수 없습니다 */}
      <Carousel />
    </div>
  )
}


이것은 Next.js가 <Carousel />이 클라이언트 전용 기능을 사용한다는 것을 알지 못하기 때문입니다.

이를 해결하기 위해 클라이언트 전용 기능을 사용하는 타사 컴포넌트를 자체 클라이언트 컴포넌트로 래핑할 수 있습니다.

// app/carousel.tsx
import { Carousel } from 'acme-carousel'
 
export default Carousel

 

이제 서버 컴포넌트 내에서 <Carousel />을 직접 사용할 수 있습니다.

// app/page.tsx
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/* 작동합니다. Carousel은 클라이언트 컴포넌트입니다 */}
      <Carousel />
    </div>
  )
}

대부분의 타사 컴포넌트를 래핑할 필요는 없을 것으로 예상됩니다. 대부분의 경우 이러한 컴포넌트를 클라이언트 컴포넌트 내에서 사용하게 될 것이기 때문입니다. 그러나 프로바이더와 같은 경우에는 루트에서 React 상태와 컨텍스트를 사용하므로 일반적으로 애플리케이션의 루트에 필요합니다.


컨텍스트 프로바이더 사용

컨텍스트 프로바이더는 일반적으로 애플리케이션의 루트 근처에 렌더링되어 현재 테마와 같은 전역 관심사를 공유하기 위해 사용됩니다. React 컨텍스트는 서버 컴포넌트에서 지원되지 않으므로 애플리케이션의 루트에서 컨텍스트를 생성하려고 하면 오류가 발생합니다.

이를 해결하기 위해 컨텍스트를 만들고 그 프로바이더를 클라이언트 컴포넌트 내에서 렌더링하십시오.

// app/theme-provider.tsx
import { createContext } from 'react'
 
export const ThemeContext = createContext({}) // 초기 컨텍스트 생성
 
export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

이제 서버 컴포넌트에서 프로바이더를 직접 렌더링할 수 있습니다. 이 프로바이더가 루트에 렌더링되면 애플리케이션의 모든 다른 클라이언트 컴포넌트에서 이 컨텍스트를 사용할 수 있습니다.

// app/layout.tsx
import ThemeProvider from './theme-provider'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

프로바이더를 가능한 깊은 곳에서 렌더링하는 것이 좋습니다. ThemeProvider가 전체 <html> 문서를 래핑하는 대신 {children}만 래핑하도록 한 것을 볼 수 있습니다. 이렇게 하면 Next.js가 서버 컴포넌트의 정적 부분을 최적화하기가 더 쉬워집니다.


  라이브러리 작성자를 위한 조언

비슷한 방식으로 다른 개발자가 사용할 패키지를 생성하는 라이브러리 작성자는 패키지의 클라이언트 진입점을 표시하기 위해 "use client" 지시어를 사용할 수 있습니다. 이렇게 하면 패키지 사용자가 래핑 경계를 만들 필요 없이 패키지 컴포넌트를 서버 컴포넌트 내에서 직접 가져올 수 있습니다.

패키지를 보다 효율적으로 사용할 수 있도록 패키지 내에서 "use client"를 더 깊게 사용하도록 할 수도 있습니다. 이렇게 하면 가져온 모듈이 서버 컴포넌트 모듈 그래프의 일부가 됩니다.

"use client" 지시어를 제거하는 번들러가 있을 수 있으므로 React Wrap Balancer 및 Vercel Analytics 리포지토리에서 "use client" 지시어를 포함하도록 esbuild를 구성하는 예제를 찾을 수 있습니다.


 클라이언트 구성 요소

  Client 구성 요소를 트리 아래로 이동시키기

클라이언트 JavaScript 번들 크기를 줄이기 위해 Client 구성 요소를 컴포넌트 트리 아래로 이동하는 것이 좋습니다. 예를 들어, 레이아웃에는 정적 요소(로고, 링크 등)와 상태를 사용하는 상호 작용 검색 바와 같은 부분이 포함될 수 있습니다. 레이아웃 전체를 Client 구성 요소로 만들기 대신 인터랙티브 로직을 Client 구성 요소(예: <SearchBar />)로 이동하고 레이아웃은 Server 구성 요소로 유지하면 됩니다.

(개인 정리) 레이아웃 전체를 클라이언트 컴포넌트로 지정하기 보다는 상태에 따른 동적인 기능을 수행하는 요소는 하위의 별도의 클라이언트 컴포넌트로 이동하는 것이 좋으며, 레이아웃은 서버 컴포넌트로서 유지하는 것이 좋다.
// 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>
    </>
  )
}

 

  Server에서 Client로 데이터 전달 (직렬화)

Server 구성 요소에서 데이터를 가져오면 이 데이터를 props로 Client 구성 요소로 전달할 수 있어야 합니다. Server에서 Client 구성 요소로 전달되는 props는 React에서 직렬화 가능해야 합니다. 클라이언트 구성 요소가 직렬화할 수 없는 데이터에 의존하는 경우 타사 라이브러리를 사용하여 클라이언트에서 데이터를 가져 오거나 Route Handler 를 통해 서버에서 데이터를 가져올 수 있습니다 .

(개인정리) 직렬화는 객체를 바이트 스트림으로 변환하는 과정을 의미한다. 여기서 바이트 스트림은 8비트의 바이트 단위로 프로그램으로 흘러가는 데이터의 흐름을 의미한다. 쉽게 말해, 전달되는 데이터가 순서대로 프로그램에 전달되도록 보장하는 데이터의 흐름이다. 여기서 순서대로 흘러가는 데이터의 흐름의 기본적 단위가 바이트이며, 1 바이트는 비트가 8개 모여 형성된다.

 

  Server 및 Client 구성 요소(서버 및 클라이언트 컴포넌트)  교차(인터리빙) 사용

Server와 Client 구성 요소를 교차로 사용할 때, UI를 컴포넌트 트리로 시각화하는 것이 도움이 될 수 있습니다. 루트 레이아웃부터 시작하여 Server 구성 요소로 간주하고 "use client" 지시문을 추가하여 일부 컴포넌트 서브트리를 클라이언트에 렌더링할 수 있습니다.

클라이언트 서브트리 내에서 Server 구성 요소를 중첩하거나 Server 작업을 호출할 수 있지만 몇 가지 사항을 염두에 두어야 합니다. 

- 요청/응답 수명 주기 동안 코드는 서버에서 클라이언트로 이동합니다. 클라이언트에 있는 동안 서버의 데이터나 리소스에 액세스해야 하는 경우 앞뒤로 전환하지 않고 서버에 새로운 요청 을 하게 됩니다 .

- 새 요청이 서버로 전송되면 모든 Server 구성 요소가 먼저 렌더링되며, 이러한 구성 요소는 Client 구성 요소 내부에 중첩됩니다. 클라이언트에서는 RSC(렌더링된 서버 컴포넌트) 페이로드를 사용하여 Server 및 Client 구성 요소를 단일 트리로 조정합니다.

- Client 구성 요소는 Server 구성 요소 뒤에 렌더링되므로 Server 구성 요소를 Client 구성 요소 모듈로 가져올 수 없습니다. 대신 Server 구성 요소를 Client 구성 요소에 프롭스로 전달할 수 있습니다.

 

지원되지 않는 패턴

다음 패턴은 지원되지 않습니다. 서버 구성 요소를 클라이언트 구성 요소로 가져올 수 없습니다.

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

 

지원되는 패턴

서버 컴포넌트를 클라이언트 컴포넌트의 프로퍼티로 전달할 수 있습니다. React children 프로퍼티를 사용하여 클라이언트 컴포넌트에 "슬롯"을 생성하는 것이 일반적인 패턴입니다. 다음 예에서 <ClientComponent>는 children 프로퍼티를 허용합니다.

'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>를 모두 가져오고 <ServerComponent>를 <ClientComponent>의 자식으로 전달할 수 있습니다.

// app/page.tsx

// 이 패턴이 작동합니다:
// 서버 컴포넌트를 클라이언트 컴포넌트의 자식 또는 프로퍼티로 전달할 수 있습니다.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// Next.js의 페이지는 기본적으로 서버 컴포넌트입니다
export default function Page() {
       return (
           <ClientComponent>   //클라이언트 컴포넌트의 children 프로퍼티의 값으로 담깁니다.
              </servercomponent >
           </ClientComponent>
    )
}

이 접근 방식을 사용하면 <ClientComponent>와 <ServerComponent>가 분리되고 독립적으로 렌더링할 수 있습니다. 이 경우 자식 <ServerComponent>는 서버에서 렌더링될 수 있으며, <ClientComponent>가 클라이언트에서 렌더링되기 훨씬 이전에 렌더링될 수 있습니다.

다음 사항을 유의하세요
"콘텐츠를 위로 올리기" 패턴은 부모가 렌더링될 때 중첩된 자식 컴포넌트를 다시 렌더링하지 않기 위해 사용되었습니다.
children 프로퍼티에만 국한되지 않습니다. JSX를 전달하기 위해 임의의 프로퍼티를 사용할 수 있습니다.

 

 

반응형