[번역] 구성 패턴

Next.js
2024-03-11

원글 : https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns

리액트 애플리케이션을 빌드할 때, 애플리케이션의 어느 부분을 서버 또는 클라이언트에서 렌더링할지 고려해야 한다.

이 글은 서버 및 클라이언트 컴포넌트를 사용할 때 권장되는 몇 가지 구성 패턴을 다룬다.

서버 및 클라이언트 구성 요소를 언제 사용하나요?

다음은 서버 및 클라이언트 구성 요소의 다양한 사용 사례에 대한 간략한 개요이다.

image

서버 컴포넌트 패턴

클라이언트 쪽 렌더링을 선택하기 전에 데이터 검색, 데이터베이스 및 백엔드 서비스에 대한 액세스와 같은 서버에서 작업을 수행할 수 있다. 아래는 서버 컴포넌트를 사용할 때 몇 가지 일반적인 패턴을 보여준다.

컴포넌트 간의 데이터 공유

서버에서 데이터를 검색할 때 다른 컴포넌트 간에 데이터를 공유해야 할 때가 있다.

예를 들어, 동일한 데이터에 의존하는 레이아웃과 페이지가 있다고 가정해보자.

React Context (서버에서는 사용할 수 없음)를 사용하거나 데이터를 props로 전달하는 대신 fetch 또는 리액트 cache 함수를 사용하여 동일한 데이터에 대해 중복 요청을 신경 쓰지 않고 필요한 컴포넌트 내에서 동일한 데이터를 가져올 수 있다 . 이는 리액트가 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() }
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 는 프라이빗 환경 변수를 빈 string으로 옮겨 놓는다.

그 결과, getData() 는 클라이언트 상에서 임포트해 실행할 수 있지만, 예상대로 동작하지 않는다. 또한 변수를 공개하면 함수가 클라이언트에서 작동하게 되지만 민감한 정보를 클라이언트에 노출하지 않으려는 경우도 있다.

이러한 종류의 클라이언트가 의도하지 않은 서버 코드 사용을 방지하기 위해 server-only 패키지를 사용하여 다른 개발자가 이러한 모듈 중 하나를 실수로 클라이언트 컴포넌트로 가져올 때 빌드 타임 오류를 제공할 수 있다.

타사 패키지 및 공급자 사용

서버 컴포넌트는 리액트의 새로운 기능이므로 써드 파티 패키지와 providers는 useState, useEffect, createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 "use client" 지시문을 추가하기 시작했다.

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

예를 들어 <Carousel /> 컴포넌트가 포함된 가상 acme-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> ) }
'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> ) }
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> ) }

대부분의 타사 컴포넌트는 클라이언트 컴포넌트 내에서 사용할 가능성이 높으므로 래핑할 필요가 없다. 그러나 제공자는 예외이다. 공급자는 리액트 state 와 컨텍스트에 따라 다르며 일반적으로 애플리케이션의 루트에서 필요하다.

컨텍스트 제공자 사용

컨텍스트 공급자는 일반적으로 현재 테마와 같은 글로벌 관심사를 공유하기 위해 애플리케이션의 루트 근처에 표시된다. 리액트 컨텍스트는 서버 컴포넌트에서 지원되지 않으므로 응용 프로그램의 루트에 컨텍스트를 만들려고하면 오류가 발생한다.

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> ) }
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> }
'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> ) }
import ThemeProvider from './theme-provider' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html> <body> <ThemeProvider>{children}</ThemeProvider> </body> </html> ) }

클라이언트 컴포넌트

클라이언트 컴포넌트를 트리 아래로 이동

클라이언트 JavaScript 번들의 크기를 줄이려면 클라이언트 컴포넌트를 컴포넌트 트리 아래로 이동하는 것이 좋다.

예를 들어, 정적 요소(로고, 링크 등)와 상태를 사용하는 인터랙티브한 검색 바를 포함한 레이아웃이 있을 수 있다.

전체 레이아웃을 클라이언트 컴포넌트로 만드는 대신 인터렉티브 logic을 클라이언트 컴포넌트(예: <SearchBar />)로 이동하고 레이아웃을 서버 컴포넌트로 유지한다. 이는, 레이아웃의 모든 컴포넌트 자바스크립트를 클라이언트에 송신할 필요가 없는 것을 의미한다.

// 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> </> ) }
// 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로 클라이언트 컴포넌트에 전달할 수 있다. 서버에서 클라이언트 컴포넌트로 전달되는 Props는 React에서 직렬화 할 수 있어야 한다.

클라이언트 컴포넌트가 serialize할 수 없는 데이터에 의존하는 경우 타사 라이브러리를 사용하여 클라이언트 또는 Route Handler를 통해 서버에서 데이터를 가져올 수 있다.

서버 및 클라이언트 컴포넌트 간의 인터리빙

클라이언트 및 서버 컴포넌트를 인터리브하는 경우 UI를 컴포넌트 트리로 시각화하는 것이 도움이 될 수 있다. 서버 컴포넌트인 루트 레이아웃부터 시작하여 "use client" 지시문을 추가하여 클라이언트에서 컴포넌트의 특정 하위 트리를 렌더링할 수 있다.

이러한 클라이언트 하위 트리 내에서 서버 컴포넌트를 중첩하거나 서버 작업을 호출할 수 있지만 몇 가지 유의해야 할 사항이 있다.

  • 요청 및 응답 수명 주기 동안 코드는 서버에서 클라이언트로 이동한다. 클라이언트에서 서버의 데이터 또는 리소스에 액세스해야 하는 경우 앞뒤로 전환하는 대신 서버에 대한 새 요청을 만들 수 있다.
  • 서버에 대한 새 요청이 발생하면 클라이언트 컴포넌트 내에 중첩된 서버 컴포넌트를 포함하여 모든 서버 컴포넌트가 먼저 렌더링된다. 렌더링된 결과(RSC Payload)에는 클라이언트 컴포넌트의 위치에 대한 참조가 포함된다. 그런 다음 클라이언트에서 리액트는 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 /> </> ) }
'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으로 클라이언트 컴포넌트에 전달

다음 패턴은 지원된다. 서버 컴포넌트를 props으로 클라이언트 컴포넌트에 전달할 수 있다.

일반적인 패턴은 React의 children prop을 사용하여 클라이언트 컴포넌트에 "슬롯"을 만드는 것이다.

다음 예제에서 <ClientComponent>children prop을 허용합니다.

'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} </> ) }
'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>의 하위로 전달할 수 있다.

import ClientComponent from './client-component' import ServerComponent from './server-component' // Next.js에서 페이지는 기본적으로 서버 컴포넌트이다. export default function Page() { return ( <ClientComponent> <ServerComponent /> </ClientComponent> ) }
import ClientComponent from './client-component' import ServerComponent from './server-component' // Next.js에서 페이지는 기본적으로 서버 컴포넌트이다. export default function Page() { return ( <ClientComponent> <ServerComponent /> </ClientComponent> ) }

이 방법에서는 <ClientComponent><ServerComponent>를 분리하여 독립적으로 렌더링할 수 있다. 이 경우 하위의 <ServerComponent>는 클라이언트에서 <ClientComponent>가 렌더링되기 전에 서버에서 렌더링 할 수 있다.

관련 포스트

post thumbnail

[번역] Next.js 14

Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%

2024-03-12
post thumbnail

[번역] 캐싱

Next.js는 렌더링 작업과 데이터 요청을 캐싱하여 어플리케이션의 성능을 향상시키고 비용을 감소시킨다. 기본적으로 Next.js는 성능을 향상시키고 비용을 줄이기 위해 가능한 한

2024-03-12
post thumbnail

[번역] 클라이언트 컴포넌트

클라이언트 컴포넌트는 요청 시 클라이언트에서 렌더링할 수 있는 인터랙티브한 UI를 만들 수 있게 해준다. Next.js에서 클라이언트 렌더링은 opt-in으로, 리액트가 클라이언트

2024-03-11
post thumbnail

[번역] 서버 컴포넌트

리액트 서버 컴포넌트는 서버에서 렌더링되고 선택적으로 캐시된 UI를 만들 수 있게 한다. Next.js에서 렌더링 하는 것은 route segments에 의해 더 나누어져있어서 s

2024-03-11

도로모의 기술 블로그

# Contact : jyw966@naver.com

Copyright © doromo. All Rights Reserved.