React Essentials 번역 및 요약

윤뿔소·2023년 6월 22일
9

Next.js의 React Essentials에 대해서 번역하고 요약하겠다.
필자 본인 생각으로는 Next.js를 이해하려면 여기는 무조건 읽고 중요한 부분이 제일 많은 항목이라 생각한다.

React Essentials(필수사항)

Next.js로 애플리케이션을 구축하려면 서버 컴포넌트와 같은 React의 새로운 기능에 익숙해져야 합니다. 이 페이지에서는 서버 컴포넌트와 클라이언트 컴포넌트의 차이점, 언제 사용해야 하는지, 그리고 권장되는 패턴에 대해 설명합니다.

React에 익숙하지 않다면, React 문서를 참조하는 것도 추천합니다. 학습에 도움이 될 좋은 자료들은 다음과 같습니다:

서버 컴포넌트

서버 컴포넌트와 클라이언트 컴포넌트는 개발자들이 서버와 클라이언트를 아우르는 애플리케이션을 구축할 수 있게 해주며, 클라이언트 측 앱의 풍부한 상호작용성과 전통적인 서버 렌더링의 향상된 성능을 결합합니다.

서버 컴포넌트에서 생각해보기

React가 UI 구축에 대한 생각 방식을 어떻게 바꾸었는지와 유사하게, React 서버 컴포넌트는 서버와 클라이언트를 활용하는 하이브리드 애플리케이션을 구축하는 새로운 멘탈 모델을 소개합니다.

React가 전체 애플리케이션을 클라이언트 측에서 렌더링하는 대신 (예: 싱글 페이지 애플리케이션의 경우), React는 이제 컴포넌트의 목적에 따라 어디에서 렌더링할지 선택할 수 있는 유연성을 제공합니다.

예를 들어, 애플리케이션의 페이지를 고려해보세요:

페이지를 더 작은 컴포넌트로 분할하면, 대부분의 컴포넌트는 상호작용하지 않고 서버 컴포넌트로서 서버에서 렌더링될 수 있다는 것을 알 수 있습니다. 상호작용하는 UI의 작은 부분에 대해서는 클라이언트 컴포넌트를 사용할 수 있습니다. 이것은 Next.js의 서버 우선 접근법과 일치합니다.

왜 서버 컴포넌트인가?

그렇다면, 왜 서버 컴포넌트인가요? 클라이언트 컴포넌트보다 사용하는 이점은 무엇인가요?

서버 컴포넌트는 개발자들이 서버 인프라를 더 잘 활용할 수 있게 해줍니다. 예를 들어, 데이터를 가져오는 작업을 서버로 이동시키고, 데이터베이스에 가깝게 위치시킬 수 있습니다. 또한, 이전에 클라이언트 JavaScript 번들 크기에 영향을 미치는 큰 의존성을 서버에 유지할 수 있습니다. 이로 인해 성능이 향상됩니다. 서버 컴포넌트는 React 애플리케이션을 작성하는 것을 PHP나 Ruby on Rails와 같이 느끼게 하지만, React와 컴포넌트 모델의 힘과 유연성을 가지고 UI를 템플릿화합니다.

서버 컴포넌트를 사용하면 초기 페이지 로드가 빨라지고, 클라이언트 측 JavaScript 번들 크기가 줄어듭니다. 기본 클라이언트 측 런타임은 캐시 가능하고 크기가 예측 가능하며, 애플리케이션이 커짐에 따라 증가하지 않습니다. 추가적인 JavaScript는 애플리케이션에서 클라이언트 컴포넌트를 통해 클라이언트 측 상호작용이 사용될 때만 추가됩니다.

Next.js로 라우트가 로드될 때, 초기 HTML은 서버에서 렌더링됩니다. 이 HTML은 브라우저에서 점진적으로 향상되어, 클라이언트가 애플리케이션을 인수하고 상호작용을 추가할 수 있게 합니다. 이는 Next.js와 React 클라이언트 측 런타임을 비동기적으로 로드함으로써 이루어집니다.

서버 컴포넌트로의 전환을 쉽게 하기 위해, App 라우터 내의 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 이는 특별 파일과 공동 위치한 컴포넌트를 포함합니다. 이를 통해 추가 작업 없이 자동으로 이들을 채택할 수 있으며, 기본적으로 훌륭한 성능을 얻을 수 있습니다. 또한, 'use client' 지시문을 사용하여 클라이언트 컴포넌트에 선택적으로 참여할 수 있습니다.

클라이언트 컴포넌트

클라이언트 컴포넌트는 애플리케이션에 클라이언트 측 상호작용을 추가할 수 있게 해줍니다. Next.js에서, 이들은 서버에서 사전 렌더링되고 클라이언트에서 채워집니다(Hydrated). 페이지 라우터의 컴포넌트가 항상 작동하는 방식처럼 클라이언트 컴포넌트를 생각할 수 있습니다.

"use client" 지시문

"use client" 지시문은 서버와 클라이언트 컴포넌트 모듈 그래프 사이의 경계를 선언하는 관례입니다.

'use client'
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

"use client"는 서버 전용 코드와 클라이언트 코드 사이에 위치합니다. 이는 파일의 상단, import 위에 배치되어, 서버 전용 부분에서 클라이언트 부분으로 경계를 넘어가는 절단점을 정의합니다. "use client"가 파일에서 정의되면, 그 파일에 import된 모든 다른 모듈들, 하위 컴포넌트를 포함하여, 클라이언트 번들의 일부로 간주됩니다.

서버 컴포넌트가 기본이므로, "use client" 지시문으로 시작하는 모듈에서 정의되거나 import되지 않는 한, 모든 컴포넌트는 서버 컴포넌트 모듈 그래프의 일부입니다.

알아두면 좋은 점:

  • 서버 컴포넌트 모듈 그래프의 컴포넌트는 서버에서만 렌더링될 것이 보장됩니다.
  • 클라이언트 컴포넌트 모듈 그래프의 컴포넌트는 주로 클라이언트에서 렌더링되지만, Next.js를 사용하면 서버에서 사전 렌더링되고 클라이언트에서 채워질 수 있습니다.
  • "use client" 지시문은 모든 import 이전에 파일의 상단에서 정의되어야 합니다.
  • "use client"는 모든 파일에서 정의될 필요는 없습니다. 클라이언트 모듈 경계는 "진입점"(entry point)에서 한 번만 정의되면, 그것으로부터 import된 모든 모듈이 클라이언트 컴포넌트로 간주됩니다.

⭐️우리는 언제 서버 or 클라이언트 컴포넌트를 써야할까?

서버 컴포넌트와 클라이언트 컴포넌트 사이의 결정을 간소화하기 위해, 클라이언트 컴포넌트에 대한 사용 사례가 있을 때까지 서버 컴포넌트를 사용하는 것을 추천합니다(App Directory에서 기본 설정).

이 표는 서버 컴포넌트와 클라이언트 컴포넌트의 다양한 사용 사례를 요약합니다:

무엇을 해야 하나?서버 컴포넌트클라이언트 컴포넌트
데이터 펫칭할 때
백엔드 리소스에 직접 접근할 때
민감한 정보를 서버에 보관할 때 (액세스 토큰, API 키 등).
큰 의존성을 서버에 보관하거나 / 클라이언트 측 JavaScript를 줄일 때
상호작용과 이벤트 리스너를 추가할 때 (onClick(), onChange() 등).
상태와 생명주기 효과를 사용할 때 (useState(), useReducer(), useEffect() 등).
브라우저 전용 API를 사용할 때
상태, 효과, 또는 브라우저 전용 API에 의존하는 사용자 정의 훅을 사용할 때
React 클래스 컴포넌트를 사용할 때

패턴

클라이언트 컴포넌트를 리프(leaf)로 이동하기

애플리케이션의 성능을 향상시키기 위해, 가능한 한 컴포넌트 트리의 리프(leaf)로 클라이언트 컴포넌트를 이동하는 것을 추천합니다.

예를 들어, 정적 요소(로고, 링크 등)와 상태를 사용하는 대화형 검색 바가 있는 레이아웃이 있을 수 있습니다.

전체 레이아웃을 클라이언트 컴포넌트로 만드는 대신, 대화형 로직을 클라이언트 컴포넌트(예: )로 이동하고 레이아웃을 서버 컴포넌트로 유지합니다. 이는 레이아웃의 모든 컴포넌트 자바스크립트를 클라이언트로 보낼 필요가 없음을 의미합니다.

// SearchBar: 클라이언트 컴포넌트
import SearchBar from './searchbar'
// Logo: 서버 컴포넌트
import Logo from './logo'
 
// Layout: 기본적으로 서버 컴포넌트
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

클라이언트 컴포넌트 내부에 서버 컴포넌트 중첩하기

위에서 개요를 설명한 렌더링 흐름에 따라, 클라이언트 컴포넌트로 서버 컴포넌트를 가져오는 것에는 제한이 있습니다. 이 접근 방식은 추가적인 서버 라운드 트립을 필요로 합니다.

지원되지 않는 패턴: 클라이언트 컴포넌트로 서버 컴포넌트 가져오기

다음 패턴은 지원되지 않습니다. 클라이언트 컴포넌트로 서버 컴포넌트를 가져올 수 없습니다:

'use client'

// 이 패턴은 **작동하지 않습니다**!
// 클라이언트 컴포넌트로 서버 컴포넌트를 가져올 수 없습니다.
import ExampleServerComponent from './example-server-component'

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExampleServerComponent />
    </>
  )
}

권장 패턴: 서버 컴포넌트를 클라이언트 컴포넌트의 props로 전달하기

대신, 클라이언트 컴포넌트를 설계할 때 React props를 사용하여 서버 컴포넌트의 "holes"을 표시할 수 있습니다.

서버 컴포넌트는 서버에서 렌더링되며, 클라이언트 컴포넌트가 클라이언트에서 렌더링될 때 "holes"은 서버 컴포넌트의 렌더링 결과로 채워집니다.

일반적인 패턴은 React의 {children} prop을 사용하여 "holes"을 만드는 것입니다. 우리는 <ExampleClientComponent>를 리팩토링하여 일반적인 {children} prop을 받아들이고 <ExampleClientComponent>의 import와 명시적 중첩을 부모 컴포넌트로 이동시킬 수 있습니다.

'use client'

import { useState } from 'react'

export default function ExampleClientComponent({
  // 여기!
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      // 여기!
      {children}
    </>
  )
}

이제, <ExampleClientComponent>{children}이 무엇인지 알지 못합니다. 실제로, 그것의 관점에서는 {children}이 결국 서버 컴포넌트의 결과로 채워질 것이라는 것조차 모릅니다.

<ExampleClientComponent>의 유일한 책임은 어떤 {children}이 최종적으로 어디에 배치될지 결정하는 것입니다.

부모 서버 컴포넌트에서는 <ExampleClientComponent><ExampleServerComponent>를 모두 import하고 <ExampleServerComponent><ExampleClientComponent>의 자식으로 전달할 수 있습니다:

// 이 패턴은 작동할 것입니다:
// 서버 컴포넌트를 클라이언트 컴포넌트의 child 또는 Props으로 전달할 수 있습니다.
import ExampleClientComponent from './ExampleClientComponent'
import ExampleServerComponent from './ExampleServerComponent.server'

export default function ParentServerComponent() {
  return (
    <ExampleClientComponent>
      // 여기!
      <ExampleServerComponent />
    </ExampleClientComponent>
  )
}

이 접근 방식을 사용하면 <ExampleClientComponent><ExampleServerComponent>의 렌더링이 분리되어 독립적으로 렌더링될 수 있으며, 서버 컴포넌트가 클라이언트 컴포넌트보다 먼저 서버에서 렌더링되는 Server Components와 일치합니다.

알아두면 좋은 점

  • 이 접근 방식을 사용하면 <ExampleClientComponent><ExampleServerComponent>의 렌더링이 분리되어 독립적으로 렌더링될 수 있으며, 서버 컴포넌트가 클라이언트 컴포넌트보다 먼저 서버에서 렌더링되는 Server Components와 일치합니다.
  • 이 패턴은 이미 레이아웃과 페이지에서 children 속성을 사용하여 적용되었으므로 추가적인 래퍼 컴포넌트를 생성할 필요가 없습니다.
  • React 컴포넌트(JSX)를 다른 컴포넌트로 전달하는 것은 새로운 개념이 아니며 항상 React 구성 모델의 일부였습니다.
  • 이 구성 전략은 서버 컴포넌트와 클라이언트 컴포넌트 모두에서 작동합니다. 전달된 prop을 받는 컴포넌트는 prop이 무엇인지에 대한 정보를 가지고 있지 않습니다. 이 컴포넌트는 전달된 것을 어디에 배치해야 하는지에 대해서만 책임이 있습니다.
  • 이로 인해 전달된 prop은 독립적으로 렌더링될 수 있으며, 이 경우 클라이언트 컴포넌트가 클라이언트에서 렌더링되기 전에 서버에서 이미 렌더링됩니다.
  • "콘텐츠를 올리는(lifting content up)" 동일한 전략은 부모 컴포넌트에서 가져온 중첩된 자식 컴포넌트의 상태 변경을 피하기 위해 사용되었습니다.
  • children 속성에만 국한되지 않습니다. JSX를 전달하기 위해 어떤 prop이든 사용할 수 있습니다.

서버에서 클라이언트 컴포넌트로 prop 전달하기 (직렬화)

  • 서버에서 클라이언트 컴포넌트로 전달되는 prop은 직렬화 가능해야 합니다. 이는 함수, 날짜 등과 같은 값은 직접적으로 클라이언트 컴포넌트로 전달할 수 없음을 의미합니다.

네트워크 경계는 어디에 있나요?

  • 앱 라우터에서 네트워크 경계는 서버 컴포넌트와 클라이언트 컴포넌트 사이에 있습니다. 이는 페이지와 다릅니다. 페이지에서는 네트워크 경계가 getStaticProps/getServerSideProps와 페이지 컴포넌트 사이에 있습니다. 서버 컴포넌트 내에서 가져온 데이터는 네트워크 경계를 넘어가지 않기 때문에 직렬화될 필요가 없습니다. 단, 클라이언트 컴포넌트로 전달될 경우에만 직렬화되어야 합니다. 서버 컴포넌트와 함께 데이터를 가져오는 방법에 대해 자세히 알아보세요. 서버 컴포넌트를 사용한 데이터 가져오기.

Client Components에서 서버 전용 코드를 제외하는 방법 (Poisoning)

JavaScript 모듈은 서버 컴포넌트와 클라이언트 컴포넌트 모두에서 공유될 수 있기 때문에, 서버에서만 실행되기로 의도된 코드가 클라이언트로 잠적(Sneak) 할 수 있습니다.

예를 들어, 다음과 같은 데이터 가져오기 함수를 살펴보겠습니다.

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

처음 보면 getData 함수가 서버와 클라이언트 양쪽에서 작동하는 것처럼 보입니다. 그러나 환경 변수 API_KEYNEXT_PUBLIC로 접두사가 없기 때문에, 서버에서만 액세스할 수 있는 개인 변수입니다. Next.js는 클라이언트 코드에서 안전한 정보 유출을 방지하기 위해 비공개 환경 변수를 빈 문자열로 대체합니다.

결과적으로, getData() 함수는 클라이언트에서 가져와 실행할 수 있지만, 예상대로 작동하지 않습니다. 변수를 공개하면 함수가 클라이언트에서 작동하지만, 민감한 정보가 유출됩니다.

따라서, 이 함수는 오직 서버에서만 실행되도록 의도되어 작성되었습니다.

server-only 패키지

서버 코드가 실수로 클라이언트 컴포넌트로 가져와지는 것을 방지하기 위해, server-only 패키지를 사용하여 다른 개발자가 이러한 모듈을 실수로 클라이언트 컴포넌트에 가져올 때 빌드 시간 오류를 발생시킬 수 있습니다.

server-only 패키지를 사용하려면 먼저 패키지를 설치합니다.

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

이제, getData()를 가져오는 어떤 클라이언트 컴포넌트도 빌드 시간 오류를 받게 되며, 해당 모듈이 서버에서만 사용될 수 있다는 설명이 표시됩니다.

client-only라는 대응하는 패키지는 클라이언트 전용 코드, 예를 들어 window 객체에 접근하는 코드와 같은 모듈을 표시하는 데 사용될 수 있습니다.

데이터 fetching

클라이언트 컴포넌트에서 데이터를 펫칭하는 것은 가능하지만, 데이터를 서버 컴포넌트에서 가져오는 것을 권장합니다. 데이터 펫칭을 서버로 이동시키면 성능과 사용자 경험이 향상됩니다.

써드파티 패키지

서버 컴포넌트는 새로운 개념이기 때문에, 생태계의 써드파티 패키지들은 useState, useEffect, createContext와 같은 클라이언트 전용 기능을 사용하는 컴포넌트에 use client 지시문을 추가하기 시작했습니다.

현재, 많은 npm 패키지의 컴포넌트들은 아직 이러한 지시문이 없습니다. 이러한 써드파티 컴포넌트들은 내부적으로 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>
 
      {/* 캐러셀이 클라이언트 컴포넌트 안에서는 작동 */}
      {isOpen && <Carousel />}
    </div>
  )
}

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

import { Carousel } from 'acme-carousel';

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/* 에러: `useState`는 서버 컴포넌트 내에서 사용할 수 없습니다 */}
      <Carousel />
    </div>
  );
}

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

이를 해결하기 위해서는 클라이언트 전용 기능에 의존하는 서드파티 컴포넌트를 자체 클라이언트 컴포넌트로 감싸면 됩니다.

'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

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

import Carousel from './carousel';

export default function Page() {
  return (
    <div>
      <p>사진 보기</p>

      {/* Carousel은 클라이언트 컴포넌트이므로 작동합니다 */}
      <Carousel />
    </div>
  );
}

대부분의 서드파티 컴포넌트를 감싸지 않아도 되는 것이 일반적이지만, Provider 컴포넌트는 예외입니다. 이는 React의 상태와 컨텍스트에 의존하며 일반적으로 애플리케이션의 루트에서 필요합니다. 아래에서 서드파티 컨텍스트 Provider에 대해 더 알아보세요.

라이브러리 제작자

  • 다른 개발자가 사용할 패키지를 만드는 라이브러리 제작자는 패키지의 클라이언트 진입점을 표시하기 위해 use client 지시문을 사용할 수 있습니다. 이를 통해 패키지 사용자는 래핑 경계를 생성하지 않고 패키지 구성 요소를 직접 서버 컴포넌트에 가져올 수 있습니다.
  • 가져온 모듈을 서버 컴포넌트 모듈 그래프의 일부로 만들기 위해 트리의 깊은 부분에 use client를 사용하여 패키지를 최적화할 수 있습니다.
  • 주의할 점은 일부 번들러가 use client 지시문을 제거할 수 있다는 점입니다. esbuild를 구성하여 use client 지시문을 포함하는 예제를 다음 레포지토리에서 찾을 수 있습니다: React Wrap BalancerVercel Analytics.

Context

대부분의 React 애플리케이션은 컴포넌트 간에 데이터를 공유하기 위해 context를 사용합니다. context는 직접 createContext를 사용하거나 서드파티 라이브러리에서 가져온 Provider 컴포넌트를 통해 간접적으로 사용됩니다.

Next.js 13에서는 컨텍스트가 클라이언트 컴포넌트 내에서 완전히 지원되지만 서버 컴포넌트 내에서 직접 생성하거나 사용할 수 없습니다. 이는 서버 컴포넌트가 상호작용이 없으므로 React 상태가 없고, 컨텍스트는 주로 일부 React 상태가 업데이트된 후 트리의 깊은 곳에 있는 상호작용 컴포넌트를 다시 렌더링하는 데 사용됩니다.

서버 컴포넌트 간에 데이터를 공유하는 대체 방법에 대해 설명하기 전에, 클라이언트 컴포넌트에서 컨텍스트를 사용하는 방법을 살펴보겠습니다.

클라이언트 컴포넌트에서 컨텍스트 사용하기

클라이언트 컴포넌트 내에서는 모든 컨텍스트 API를 완벽하게 지원합니다.

'use client'
 
import { createContext, useContext, useState } from 'react'
 
const SidebarContext = createContext()
 
export function Sidebar() {
  const [isOpen, setIsOpen] = useState()
 
  return (
    <SidebarContext.Provider value={{ isOpen }}>
      <SidebarNav />
    </SidebarContext.Provider>
  )
}
 
function SidebarNav() {
  let { isOpen } = useContext(SidebarContext)
 
  return (
    <div>
      <p>Home</p>
 
      {isOpen && <Subnav />}
    </div>
  )
}

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

import { createContext } from 'react'
 
// createContext는 서버 컴포넌트에서 지원되지 않습니다.
export const ThemeContext = createContext({})
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  )
}

이를 해결하기 위해 컨텍스트를 생성하고 해당 Provider를 클라이언트 컴포넌트 내에서 렌더링하세요.

'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({ children }) {
  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를 루트에서 렌더링한 후 앱의 다른 모든 클라이언트 컴포넌트에서 이 컨텍스트를 사용할 수 있습니다.

알아두면 좋은 점:
Provider는 트리의 가능한 한 깊은 곳에서 렌더링하는 것이 좋습니다. ThemeProvider가 전체 <html> 문서 대신에 children만 감싸도록 했음을 주목해주세요. 이렇게 하면 Next.js가 서버 컴포넌트의 정적인 부분을 최적화하기 쉬워집니다.

Server Components에서 써드파티 컨텍스트 Provider 렌더링하기

써드파티 npm 패키지에는 애플리케이션의 루트 근처에서 렌더링되어야 하는 Provider가 포함되는 경우가 많습니다. 이러한 Provider에 use client 지시어가 포함되어 있다면, 해당 Provider를 Server Components 내부에서 직접 렌더링할 수 있습니다. 그러나 Server Components는 아직 매우 새로운 기술이므로 많은 써드파티 Provider에서는 아직 이 지시어를 추가하지 않은 상태일 수 있습니다.

use client 지시어가 없는 써드파티 Provider를 렌더링하려고 하면 오류가 발생합니다:

import { ThemeProvider } from 'acme-theme';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* 오류: 'createContext'은 Server Components에서 사용할 수 없습니다 */}
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

이 문제를 해결하기 위해, 써드파티 Provider를 자체 클라이언트 컴포넌트로 감싸야 합니다:

'use client'
 
import { ThemeProvider } from 'acme-theme'
import { AuthProvider } from 'acme-auth'
 
export function Providers({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>{children}</AuthProvider>
    </ThemeProvider>
  )
}

이제 루트 레이아웃 내에서 직접 <Providers />를 가져와 렌더링할 수 있습니다:

// app/layout.js
import { Providers } from './providers'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Provider가 루트에 렌더링되면, 이러한 라이브러리의 모든 컴포넌트와 훅은 자체 클라이언트 컴포넌트 내에서 예상대로 작동합니다.

써드파티 라이브러리가 클라이언트 코드에 use client를 추가하면, 래퍼 클라이언트 컴포넌트를 제거할 수 있습니다.

Server Components 간 데이터 공유하기

Server Components는 대화형이 아니므로 React 상태를 읽지 않기 때문에 데이터를 공유하기 위해 React 컨텍스트가 필요하지 않습니다. 대신, 여러 Server Components가 액세스해야 하는 공통 데이터에 대해 네이티브 JavaScript 패턴을 사용할 수 있습니다. 예를 들어, 여러 컴포넌트에서 데이터베이스 연결을 공유하기 위해 모듈을 사용할 수 있습니다:

// utils/database.ts
export const db = new DatabaseConnection()
// app/users/layout.tsx
import { db } from '@utils/database'
 
export async function UsersLayout() {
  let users = await db.query()
  // ...
}
// app/users/[id]/page.tsx
import { db } from '@utils/database'
 
export async function DashboardPage() {
  let user = await db.query()
  // ...
}

위 예제에서 레이아웃과 페이지는 데이터베이스 쿼리를 수행해야 합니다. 이러한 컴포넌트들은 @utils/database 모듈을 가져와 데이터베이스에 대한 액세스를 공유합니다. 이러한 JavaScript 패턴을 전역 싱글톤이라고 합니다.

Server Components 간 fetch 요청 공유하기

데이터를 가져올 때, page 또는 layout와 그 하위 컴포넌트들 간에 fetch의 결과를 공유하고 싶을 수 있습니다. 이는 컴포넌트들 간의 불필요한 결합을 일으킬 수 있으며, 컴포넌트들 간에 props를 번갈아가며 전달해야 할 수도 있습니다.

대신, 데이터를 소비하는 컴포넌트와 함께 데이터 가져오기를 함께 배치하는 것을 권장합니다. Server Components에서는 fetch 요청이 자동으로 중복 제거되므로, 각 경로 세그먼트는 중복 요청을 걱정하지 않고 필요한 데이터를 정확히 요청할 수 있습니다. Next.js는 동일한 값을 fetch 캐시에서 읽습니다.

자세한 내용은 자동 fetch 요청 중복 제거를 참조하세요.


요약

  • React Essentials: Next.js로 애플리케이션을 구축하기 위해서는 서버 컴포넌트와 같은 React의 새로운 기능에 익숙해져야 함. 서버 컴포넌트와 클라이언트 컴포넌트의 차이점, 언제 사용해야 하는지, 그리고 권장되는 패턴에 대해 설명함.

  • 서버 컴포넌트: 서버 컴포넌트를 사용하면 서버 및 클라이언트를 모두 포함하는 애플리케이션을 구축할 수 있음. 클라이언트 측 앱의 다양한 상호작용성과 전통적인 서버 렌더링의 향상된 성능을 결합. 서버와 클라이언트를 모두 활용하는 하이브리드 애플리케이션을 구축하기 위한 새로운 개념을 도입.

  • 클라이언트 컴포넌트: 클라이언트 컴포넌트는 애플리케이션에 클라이언트 측 상호작용성을 추가. Next.js에서는 이들은 서버에서 사전 렌더링되고 클라이언트에서 채워짐(수화됨).

  • "use client" 지시문: 이 지시문은 서버 컴포넌트 모듈 그래프와 클라이언트 컴포넌트 모듈 그래프 사이의 경계를 선언. 이는 파일의 맨 위에 위치하며, import 위에 배치되어 서버 전용 부분과 클라이언트 부분 사이의 경계를 정의.

  • 서버 컴포넌트 또는 클라이언트 컴포넌트를 언제 사용해야 할까요?: 공식문서는 클라이언트 컴포넌트 사용 사례가 나올 때까지 서버 컴포넌트를 사용하는 것을 권장(App 디렉토리의 기본 설정). 서버 컴포넌트와 클라이언트 컴포넌트를 사용하는 다양한 사례 제공.

  • 패턴: 애플리케이션 성능을 향상시키기 위해 클라이언트 컴포넌트를 컴포넌트 트리의 말단으로 이동하는 것이 권장. 또한 서버 컴포넌트를 클라이언트 컴포넌트 내에 중첩하는 것은 지원되지 않음. 왜냐하면 이는 추가적인 서버 왕복이 필요하기 때문.

  • 컨텍스트: 대부분의 React 애플리케이션은 컴포넌트 간에 데이터를 공유하기 위해 context를 사용. Next.js 13에서는 컨텍스트가 클라이언트 컴포넌트 내에서 완전히 지원되지만 서버 컴포넌트 내에서 직접 생성하거나 사용할 수 없음. 서버 컴포넌트에서 컨텍스트를 지원하지 않기 때문에 서버 컴포넌트 내에서 컨텍스트를 생성하려고 하면 오류가 발생. 따라서 서버 컴포넌트에서 컨텍스트를 사용할 때는 클라이언트 컴포넌트 내에서 컨텍스트를 생성하고 해당 Provider를 렌더링하는 방식을 사용해야 함.

profile
코뿔소처럼 저돌적으로

0개의 댓글