[번역] Next.js13 App Router - React Essentials

최영호·2023년 7월 23일
1

React Essentials

Next.js를 사용하여 애플리케이션을 만들려면 서버 컴포넌트 같은 리액트의 새로운 기능에 친숙해질 필요가 있습니다.

이번 시간엔 서버 컴포넌트와 클라이언트 컴포넌트 간의 차이점이나, 언제 어떤걸 사용해야 하고, 추천하는 패턴에는 어떤게 있는지 알아봅시다.

만약 리액트가 처음이라면 공식 리액트 문서를 읽고 오는걸 추천합니다.
리액트를 배우기에 좋은 문서 링크 몇가지를 공유해 드리겠습니다.

Server Components

서버 컴포넌트와 클라이언트 컴포넌트는 클라이언트 사이드 앱의 풍부한 인터랙션과 전통적인 서버 렌더링 방식의 강력한 퍼포먼스를 혼합시켜 개발자들에게 서버와 클라이언트에서 생성 되는 어플리케이션을 만들 수 있도록 도와줍니다.

Thinking in Server Components

리액트가 UI를 만들어 내는 패러다임을 바꾸었던 것처럼 리액트 서버 컴포넌트는 서버와 클라이언트의 성능을 끌어올릴 수 있는 하이브리드 어플리케이션을 만들어 내는 새로운 멘탈 모델을 우리에게 제시하고 있습니다.

리액트가 전체 어플리케이션을 클라이언트 사이드에서 렌더링 하는게 아닌, 리액트는 이제 목적에 따라 어디서 컴포넌트를 렌더링 할 지 정할 수 있는 유연성을 제공하고 있습니다.

예를 들어 아래와 같은 어플리케이션의 한 페이지를 생각해 봅시다.

페이지를 작은 컴포넌트들로 나누어 본다면, 대부분의 컴포넌트들은 인터랙티브하지 않고 서버 컴포넌트의 형식으로 서버에서 렌더링 되어도 괜찮겠다는 걸 파악할 수 있습니다.
인터랙션이 필요한 UI의 경우 클라이언트 컴포넌트로 집어 넣을 수 있습니다.
이게 바로 Next.js의 서버 우선 접근 방식(server-first approach)입니다.

Why Server Components?

왜 서버 컴포넌트라고 물어볼 수 있습니다.
클라이언트 컴포넌트 대신 서버 컴포넌트를 사용해서 얻을 수 있는 이점은 도대체 무엇일까요?

서버 컴포넌트는 개발자들로 하여금 서버의 인프라 환경을 효과적으로 사용할 수 있도록 합니다.
예를 들어 데이터 패치 로직을 데이터베이스와 가장 가까운 서버로 옮길 수 있고, 클라이언트 자바스크립트 번들 사이즈에 큰 영향을 끼치는 거대한 의존성 코드를 서버에 격리시켜 퍼포먼스를 향상 시킬 수 있습니다.
서버 컴포넌트는 리액트 어플리케이션을 만드는게 마치 PHP 혹은 Ruby on Rails를 사용하고 있는것처럼 느끼게 하지만 리액트가 가진 유연성과 UI를 템플릿화 시키는 컴포넌트 모델을 사용할 수 있다는 점이 다릅니다.

서버 컴포넌트를 사용하면 초기 페이지 로딩은 더 빠르고 클라이언트 사이드 자바스크립트 번들 사이즈는 줄어듭니다.
기본 클라이언트 사이드 런타임은 캐싱 가능하고 특정 사이즈로 예측할 수 있게 되고 어플리케이션의 확장에 따라 사이즈가 커지지 않습니다.
추가적인 자바스크립트 코드는 어플리케이션에 존재하는 클라이언트 컴포넌트를 통해 클라이언트 사이드 인터랙션을 처리할때만 추가 됩니다.

Next.js를 통해 특정 루트가 로딩 되면, 초기 HTML은 서버에서 렌더링 됩니다.
이 HTML은 점진적으로 클라이언트 쪽에서 비동기적으로 Next.js와 리액트 클라이언트 사이드 런타임을 로딩하면서 클라이언트가 어플리케이션을 전담하고 인터랙션을 추가하도록 허용하면서 브라우저에서 업그레이드 됩니다.

서버 컴포넌트로의 이전을 쉽게 하기 위해 App Router 내의 특수 파일나란히 나열된 컴포넌트(colocated components)를 포함한 모든 컴포넌트는 기본으로 서버 컴포넌트로 세팅 됩니다.
이 방식은 추가적인 수고 없이 자동으로 서버 컴포넌트를 받아들일 수 있도록 해주고 바로 효과적인 퍼포먼스를 얻을 수 있습니다.
또한 선택적으로 "use client" 디렉티브(directive)를 사용하여 클라이언트 컴포넌트를 사용할 수 있습니다.

Client Components

클라이언트 컴포넌트는 어플리케이션에 클라이언트 사이드 인터랙션을 추가해줍니다.
Next.js에선 서버에서 미리 렌더링 되고 클라이언트에서 하이드레이션 처리 됩니다.
클라이언트 컴포넌트는 이제껏 Pages 라우터에서 컴포넌트들을 렌더링 하던 방식과 동일하다고 생각하면 됩니다.

The "use client" directive

"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" 디렉티브는 모든 파일에 정의 될 필요는 없습니다. 클라이언트 모듈 경계는 "시작 지점"에 정의 되어 있으면 됩니다. "시작 지점" 내에 import 되는 모듈들은 전부 클라이언트 컴포넌트로 처리 됩니다.

When to use Server and Client Components?

서버 컴포넌트와 클라이언트 컴포넌트 간 무엇을 선택할지 고민을 줄이기 위해 클라이언트 컴포넌트를 선택해야 할 이유가 없다면, 서버 컴포넌트를 사용하길 추천합니다.(app 디렉토리의 기본 설정이기도 합니다.)

아래 테이블은 서버 컴포넌트와 클라이언트 컴포넌트의 서로 다른 사용 예시를 요약한 테이블 입니다.

할 일서버 컴포넌트클라이언트 컴포넌트
데이터 패치OX
백엔드 리소스 직접 접근OX
엑세스 토큰, API 키 같은 민감한 정보는 서버에서만 보관OX
사이즈가 큰 디펜던시는 서버에서 처리하여 클라이언트 사이드 자바스크립트 번들 파일 사이즈 감소OX
인터랙션 추가 및 onclick(), onChange() 같은 이벤트 리스터 등록XO
스테이트와 useState(), useReducer(), useEffect() 같은 라이프사이클 이펙트들 사용XO
브라우저에서만 사용 가능한 API 호출XO
스테이트, 이펙트, 브라우저에서만 호출 가능한 API에 의존하는 커스텀 훅 사용XO
리액트 클래스 컴포넌트 사용XO

Patterns

Moving Client Components to the Leaves

어플리케이션의 퍼포먼스를 향상 시키고 싶다면 가능한 클라이언트 컴포넌트를 컴포넌트 트리의 리프 노드로 옮기는 걸 추천합니다.

예를 들어 로고, 링크 같은 정적인 엘리먼트들을 가지고 있고 스테이트를 사용하는 인터랙티브한 검색바를 가진 레이아웃 컴포넌트가 있다고 해봅시다.

레이아웃 컴포넌트 전체를 클라이언트 컴포넌트로 만들지 않고 인터랙션 로직은 클라이언트 컴포넌트로 옮겨서(<SearchBar/> 같이) 레이아웃 컴포넌트를 서버 컴포넌트로 유지합니다.
이 방식이 의미하는 건 레이아웃 컴포넌트를 구성하는 모든 컴포넌트를 클라이언트로 보낼 필요가 없다는 것입니다.

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

Composing Client and Server Components

서버 컴포넌트와 클라이언트 컴포넌트는 동일한 컴포넌트 트리에서 결합 될 수 있습니다.

뒷단에선 리액트는 다음의 방식으로 렌더링을 처리합니다.

  • 서버에선 리액트는 클라이언트로 결과를 보내기 전에 모든 서버 컴포넌트들을 렌더링 합니다.
    • 여기엔 클라이언트 컴포넌트 내부에 존재하는 서버 컴포넌트도 포함입니다.
    • 이 과정에서 마주치는 클라이언트 컴포넌트들은 무시 됩니다.
  • 클라이언트에선 리액트는 클라이언트 컴포넌트를 렌더링하고 서버 컴포넌트의 렌더링 된 결과를 슬롯 처리하고 서버에서 만들어진 결과물과 클라이언트 컴포넌트를 병합시킵니다.
    • 클라이언트 컴포넌트 내부에 존재하는 서버 컴포넌트는 정확하게 클라이언트 컴포넌트 내부에 위치하게 됩니다.

알아두면 좋은 점
Next.js에선 초기 페이지 로딩시 위 과정을 밟은 서버 컴포넌트 결과물과 클라이언트 컴포넌트 모두 초기 페이지 로딩을 빠르게 하기 위해 미리 서버에서 HTML의 형태로 렌더링 됩니다.

Nesting Server Components inside Client Components

위의 렌더링 플로우에는 클라이언트 컴포넌트 내부에 서버 컴포넌트를 import 하는데 있어 이 방식이 추가적인 서버 처리를 요구하기 때문에 제한 사항이 존재합니다.

지원 불가 패턴: 클라이언트 컴포넌트 내부로 서버 컴포넌트 import

다음의 패턴은 지원 불가능합니다. 클라이언트 컴포넌트 내부로 서버 컴포넌트를 import 할 수 없습니다.

'use client'
 
// 이 패턴은 동작하지 않습니다!!
// 클라이언트 컴포넌트 내부로 서버 컴포넌트를 import 할 수 없습니다.
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 />
    </>
  )
}

추천 패턴: 서버 컴포넌트를 클라이언트 컴포넌트의 프롭으로 전달

대신에 클라이언트 컴포넌트를 설계할 때, 리액트 프롭으로 서버 컴포넌트의 "슬롯"을 마킹할 수 있습니다.

프롭으로 전달 된 서버 컴포넌트는 서버에서 렌더링 되고 클라이언트 컴포넌트가 클라이언트에서 렌더링 될 때
서버 컴포넌트의 렌더링 결과물로 "슬롯"을 채우게 됩니다.

흔히 사용 되는 패턴은 리액트의 children 프롭으로 "슬롯"을 마련하는 것입니다.
<ExampleClientComponent> 를 리팩토링 하여 children 프롭을 받을 수 있게 하고 부모 컴포넌트에 import 로직과 <ExampleClientComponent>children 프롭으로 전달하도록 렌더링 로직을 변경합니다.

'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이 무엇인지 알고 있습니다.
서버 컴포넌트에서 어떤 결과물이 넘어와 채워질지는 지금 당장 알 필요는 없습니다.

ExampleClientComponent 컴포넌트가 가지는 유일한 책임은 어떤 children이든 간에 어디에 위치할지를 정하는 것입니다.

부모 서버 컴포넌트에선 <ExampleClientComponent> 컴포넌트와 <ExampleServerComponent> 컴포넌트를 import 하여 <ExampleServerComponent><ExampleClientComponent> 의 children으로 내려주면 됩니다.

// This pattern works:
// 이 패턴은 동작합니다.
// 서버 컴포넌트를 클라이언트 컴포넌트의 child 프롭으로 전달할 수 있습니다.
import ExampleClientComponent from './example-client-component'
import ExampleServerComponent from './example-server-component'
 
// Next.js 에서 페이지들은 기본적으로 서버 컴포넌트 입니다.
export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  )
}

이 방식을 사용하여 <ExampleClientComponent> 컴포넌트와 <ExampleServerComponent> 컴포넌트는 서로 분리 되고 <ExampleServerComponent> 컴포넌트는 클라이언트 컴포넌트 렌더링 전에 서버에서 렌더링 되는 서버 컴포넌트로 인식될 수 있어서 독립적으로 렌더링 될 수 있습니다.

알아두면 좋은 점

  • 이 패턴은 이미 children 프롭으로 layouts와 pages에 적용 되어 있습니다. 그렇기 때문에 추가적인 래퍼(wrapper) 컴포넌트를 만들 필요는 없습니다.
  • 리액트 컴포넌트(JSX)를 다른 컴포넌트로 전달하는 건 새로운 컨셉이 아니며 언제나 리액트 합성 모델의 일부분이었습니다.
  • 이 합성 전략은 서버 컴포넌트와 클라이언트 컴포넌트 모두 작동하는데, 이유는 프롭을 받은 컴포넌트는 해당 프롭이 어떤 프롭인지 모르기 때문입니다. 오직 전달 받은 요소가 어디에 위치해야 할지에 대해서만 책임을 가지게 됩니다.
    • 이 방식은 넘겨진 프롭이 독립적으로 렌더링 되도록 허용합니다. 이번 방식에선 클라이언트 컴포넌트가 클라이언트에서 렌더링 되기 전에 서버에서 렌더링 프로세스가 진행 됩니다.
    • 매우 비슷한 전략인 "컨텐츠 끌어올리기"는 부모 컴포넌트의 스테이트 변화가 자식 컴포넌트의 리렌더링을 피할 수 있도록 하기 위해 사용되곤 합니다.
  • children 프롭에만 국한된 얘기는 아닙니다. 어떤 프롭이든 JSX로 전달할 수 있습니다.

Passing props from Server to Client Components (Serialization)

서버에서 클라이언트 컴포넌트로 전달 되는 프롭은 직렬화 처리 될 수 있어야 합니다.
이말은 곧 functions, Dates 같은 요소들은 클라이언트 컴포넌트로 곧바로 전달 될 수 없다는 의미입니다.

네트워크 경계는 어디에 있나요?
App Router에서 네트워크 경계는 서버 컴포넌트와 클라이언트 컴포넌트 간에 있습니다.
이는 경계가 getStaticProps/getServerSideProps 와 Page 컴포넌트 사이에 존재했던 Pages Router와는 다릅니다.
서버 컴포넌트 내에서 행해지는 데이터 패치는 클라이언트 컴포넌트에 전달 되지 않는 이상 네트워크 경계를 넘어가지 않기 때문에 직렬화 될 필요가 없습니다.
서버 컴포넌트에서의 데이터 패치에 대해 좀 더 알아보세요.

Keeping Server-Only Code out of Client Components (Poisoning)

자바스크립트 모듈은 서버 컴포넌트와 클라이언트 컴포넌트 간에 공유 될 수 있기 때문에 서버에서만 실행 되어야 하는 코드가 클라이언트에서 실행 될 수 있습니다.

예를 들어 아래의 데이터 패치 함수를 살펴봅시다.

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() 함수가 클라이언트에 import 되고 실행 될 순 있어도 예상대로 동작하지는 않을것입니다.
그리고 변수를 public 하게 바꿔서 함수가 클라이언트에서도 동작할 수 있도록 할 수 있지만, 보안 정보가 새어나갈 수 있습니다.

그러므로 이 함수는 서버에서만 실행 되도록 만들어져 있다고 볼 수 있습니다.

The "server only" package

클라이언트에서 원치 않게 서버 코드를 실행 시키는걸 막기 위해 server-only 패키지를 사용하여 다른 개발자들이 서버에서만 사용 되어야 할 모듈을 클라이언트 컴포넌트에 import 할 때 빌드 에러를 발생 시킬 수 있습니다.

server-only 를 사용하기 위해선 패키지를 설치해야 합니다.

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

이제 getData() 함수를 import 하는 어떤 클라이언트 컴포넌트도 이 모듈은 서버에서만 사용할 수 있다는 빌드 에러를 받게 될 것입니다.

상응하는 패키지인 client-only 패키지는 window 객체에 접근하는 코드 같은 클라이언트에서만 실행 되는 코드를 포함하는 모듈을 마킹하는데 사용할 수 있습니다.

Data Fetching

클라이언트 컴포넌트에서도 데이터 패치를 할 수 있지만 클라이언트에서 데이터 패치를 해야할 특별한 이유가 없다면 서버 컴포넌트에서 데이터 패치를 진행하는걸 추천합니다.
데이터 패치 로직을 서버로 옮기면 더 뛰어난 퍼포먼스와 UX를 가져갈 수 있습니다.

데이터 패치에 대해 더 알아보기

Third-party packages

서버 컴포넌트는 새로운 개념이기 때문에 웹 생태계에 존재하는 써드파티 패키지들은 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>
 
      {/* Carousel 컴포넌트가 클라이언트 컴포넌트 내에서 사용 되었기 때문에 정상 동작합니다 */}
      {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>View pictures</p>
 
      {/* Carousel 컴포넌트는 클라이언트 컴포넌트이기 때문에 정상 동작합니다. */}
      <Carousel />
    </div>
  )
}

써드파티 컴포넌트를 클라이언트 컴포넌트 내부에서 사용할 확률이 높기 때문에 대부분의 써드파티 컴포넌트를 래핑하여 사용할 필요는 없으리라 생각합니다.
그러나 한가지 예외 사항은 프로바이더 컴포넌트입니다. 왜냐면 이들은 리액트 스테이트와 컨텍스트에 의존하고 일반적으로 어플리케이션의 최상단에서 사용되기 때문입니다.
써드파티 컨텍스트 프로바이더에 대해 더 자세히 알아보세요.

Library Authors

  • 비슷한 방식으로 다른 개발자들이 사용하는 패키지를 만들어 내는 라이브러리 소유자는 "use client" 디렉티브를 사용하여 패키지에 클라이언트 엔트리가 어디서 부터인지 마킹할 수 있습니다. 이 방식은 패키지를 사용하는 유저들이 래핑 컴포넌트를 따로 만들 필요 없이 바로 서버 컴포넌트 내부에서 사용할 수 있도록 도와줍니다.
  • 패키지 트리의 깊은 곳에 "use client" 디렉티브를 사용하여 import 하는 모듈이 서버 컴포넌트 모듈 그래프의 일부분임을 허용하게 함으로써 패키지 최적화를 처리할 수 있습니다.
  • 몇몇 번들러들이 "use client" 디렉티브를 벗겨내는건 의미가 없습니다. 여기 어떻게 esbuild 가 React Wrap Balancer, Vercel Analytics repositories에서 "use client" 디렉티브를 포함시킬 수 있게 설정하는 방법에 대한 예시가 있습니다.

Context

대부분의 리액트 어플리케이션은 컴포넌트 사이 데이터를 공유하기 위해 컨텍스트에 의존하는데 createContext를 직접 사용하거나 써드파티 라이브러리에서 import 한 프로바이더 컴포넌트로 간접적으로 사용하게 됩니다.

Next.js 13버전에선 컨텍스트는 클라이언트 컴포넌트에서 완벽하게 지원됩니다만 서버 컴포넌트에선 직접적으로 컨텍스트를 만들거나 사용할 수 없습니다.
이는 서버 컴포넌트는 인터랙션을 담당하지 않기 때문에 리액트 스테이트가 없고, 컨텍스트는 주로 리액트 스테이트가 업데이트 된 이후에 트리 깊숙히 위치한 인터랙션을 담당하는 컴포넌트를 리렌더링 하기 위해 사용하기 때문입니다.

서버 컴포넌트 간에 데이터를 공유하는 대체 방안에 대해 얘기하겠지만, 당장은 클라이언트 컴포넌트에서 어떻게 컨텍스트를 사용하는지 알아봅시다.

Using context in Client Components

모든 컨텍스트 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>
  )
}

그러나 컨텍스트 프로바이더는 일반적으로 현재 theme 같은 글로벌 요소들을 공유하기 위해 어플리케이션의 최상단에 배치됩니다.
컨텍스트는 서버 컴포넌트에서 지원하지 않기 때문에 어플리케이션의 최상단에 컨텍스트를 생성하는건 에러를 발생 시킵니다.

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

이를 해결하기 위해선 클라이언트 컴포넌트 내에서 컨텍스트를 생성하고 프로바이더를 렌더링 해야 합니다.

'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({ children }) {
  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>
  )
}

어플리케이션 최상단에서 프로바이더를 렌더링 하면서 앱 전체에 존재하는 클라이언트 컴포넌트는 해당 컨텍스트를 가져다 사용할 수 있습니다.

알아두면 좋은점:
프로바이더는 가능한 UI 트리의 가장 깊숙한 곳에 위치시켜야 합니다. ThemeProvider 컴포넌트가 <html> 문서 전체 대신 {children} 만 래핑하고 있는걸 체크해 보세요. 이는 Next.js가 서버 컴포넌트의 정적인 부분을 최적화 하는데 더 쉽게 만들어 줍니다.

Rendering third-party context providers in Server Components

써드파티 npm 패키지는 종종 어플리케이션의 최상단에 배치 되어야 하는 프로바이더를 포함하는 경우가 있습니다.
만약 이들 프로바이더가 "use client" 디렉티브를 사용하고 있다면 서버 컴포넌트에 바로 사용할 수 있습니다.
그러나 서버 컴포넌트는 매우 새로운 개념이기 때문에 많은 써드파티 프로바이더들은 이 디렉티브를 사용하고 있지 않습니다.

"use client" 디렉티브를 사용하지 않는 써드파티 프로바이더를 사용하고 싶다면 에러가 발생하게 됩니다.

import { ThemeProvider } from 'acme-theme'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/*  에러: `createContext` 는 서버 컴포넌트에서 사용할 수 없습니다. */}
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

이를 해결하기 위해선 써드파티 프로바이더를 래핑하는 클라이언트 컴포넌트를 사용하세요.

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

이제 최상단 레이아웃에서 <Providers /> 를 사용할 수 있습니다.

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

최상단에서 프로바이더를 렌더링 하면서 이 라이브러리에서 사용하는 모든 컴포넌트와 훅은 클라이언트 컴포넌트 내에서 정상 동작하게 됩니다.

써드파티 라이브러리에서 "use client" 디렉티브를 사용한다면 래퍼 클라이언트 컴포넌트를 삭제하고 사용하세요.

Sharing data between Server Components

서버 컴포넌트는 인터랙티브 하지 않고 리액트 스테이트로 부터 읽을게 없기 때문에 데이터를 공유하기 위한 컨텍스트가 필요 없습니다.
대신 일반 자바스크립트 패턴을 활용하여 다수의 서버 컴포넌트가 요구하는 데이터를 공유할 수 있습니다.
예를 들어 어떤 모듈은 다수의 컴포넌트에서 발생하는 데이터베이스 커넥션을 공유할 수 있습니다.

export const db = new DatabaseConnection()
import { db } from '@utils/database'
 
export async function UsersLayout() {
  let users = await db.query()
  // ...
}
import { db } from '@utils/database'
 
export async function DashboardPage() {
  let user = await db.query()
  // ...
}

위의 예제에서 레이아웃과 페이지 컴포넌트는 데이터베이스 쿼리를 사용하고 있습니다.
이들 컴포넌트들은 데이터베이스 접근 기능을 @utils/database 모듈을 import 하여 사용하고 있습니다.
이런 자바스크립트 패턴을 글로벌 싱글턴 패턴이라 부릅니다.

Sharing fetch requests between Server Components

데이터를 패칭할 때 패치한 결과물페이지 혹은 레이아웃 또는 자식 컴포넌트에 공유하고 싶을 수 있습니다.
이는 컴포넌트 간 불필요한 연결을 만들어 내고 컴포넌트 간 프롭을 앞뒤로 전달하게 됩니다.

대신 데이터를 사용하는 컴포넌트와 동일한 위치에 데이터 패치 로직을 위치시키세요.
패치 리퀘스트는 서버 컴포넌트에서 자동으로 중복 제거 처리 되기에 리퀘스트 중복에 대한 걱정 없이 각각의 라우트 세그먼트는 필요한 데이터를 바로 요청할 수 있습니다.
Next.js는 패치 캐시에서 동일한 값을 읽을 수 있습니다.

profile
무친 프론트엔드 개발자를 꿈꾸며...

0개의 댓글