NextJS(Beta): Server and Client Components

hwisaac·2023년 4월 25일
8

Next.js

목록 보기
28/29

서버 및 클라이언트 구성 요소를 사용하면 서버 렌더링의 성능 개선과 클라이언트 측 앱의 리치 인터액티브 기능을 결합하여 응용 프로그램을 구축할 수 있습니다.

이 페이지에서는 서버 및 클라이언트 구성 요소의 차이점과 Next.js 애플리케이션에서 사용하는 방법에 대해 설명합니다.

서버 구성 요소

앱 디렉터리 내의 모든 구성 요소는 특수 파일 및 동위 구성 요소를 포함하여 기본적으로 React Server Components(RSC)입니다. 이를 통해 추가 작업 없이 Server Components를 자동으로 사용할 수 있으며, 높은 성능을 얻을 수 있습니다.

왜 서버 구성 요소인가요?

서버 구성 요소를 사용하면 서버 인프라를 더욱 효과적으로 활용할 수 있습니다. 예를 들어, 이전에 클라이언트에서 JavaScript 번들 크기에 영향을 주는 대형 종속성은 서버에서 완전히 유지될 수 있으므로 성능이 향상됩니다.

서버 구성 요소를 사용하면 React를 사용한 UI 템플릿링에 대한 PHP 또는 Ruby on Rails와 유사한 느낌을 제공하지만, 더욱 강력하고 유연한 기능을 제공합니다.

Next.js에서 라우트가 로드되면 초기 HTML은 서버에서 렌더링됩니다. 이 HTML은 다음으로 이어지는 Next.js 및 React 클라이언트 측 런타임을 비동기적으로 로드하여 클라이언트가 애플리케이션을 적용하고 대화식 기능을 추가할 수 있습니다.

서버 구성 요소를 사용하면 초기 페이지 로드가 더 빠르고, 클라이언트 측 JavaScript 번들 크기가 감소합니다. 기본 클라이언트 측 런타임은 캐시 가능하며 크기가 예측 가능하며, 애플리케이션이 성장함에 따라 증가하지 않습니다. 클라이언트 구성 요소를 통해 애플리케이션에서 클라이언트 측 대화식 기능을 사용할 때만 추가 JavaScript가 추가됩니다.

클라이언트 구성 요소

클라이언트 구성 요소를 사용하면 애플리케이션에 클라이언트 측 대화식 기능을 추가할 수 있습니다. Next.js에서는 서버에서 프리랜더링되고 클라이언트에서 하이드레이션됩니다. 클라이언트 구성 요소는 Next.js의 pages/ 디렉터리의 구성 요소와 유사한 역할을 합니다.

규칙

"use client" 지시어는 서버 및 클라이언트 구성 요소 모듈 그래프 사이의 경계를 선언하는 데 사용되는 규칙입니다.

'use client';
// app/Counter.js

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"가 파일에 정의되면 하위 구성 요소를 포함한 모든 모듈이 클라이언트 번들의 일부로 간주됩니다.

Next.js에서는 서버 구성 요소가 기본값입니다. 따라서 "use client" 지시어로 시작하는 모듈에서 정의되거나 가져오지 않은 경우 모든 React 구성 요소가 서버 구성 요소 모듈 그래프의 일부가 됩니다.

알아두면 좋은 사항

  • 서버 구성 요소 모듈 그래프에 있는 구성 요소는 서버에서만 렌더링됩니다.
  • 클라이언트 구성 요소 모듈 그래프에 있는 구성 요소는 주로 클라이언트에서 렌더링됩니다. 그러나 Next.js에서는 서버에서 프리랜더링되어 클라이언트에서 하이드레이션될 수도 있습니다.

"use client" 지시어 사용하기

use client 지시어를 파일 상단에 import 문 이전에 정의해야 합니다. use client는 모든 파일에서 정의될 필요는 없습니다. 클라이언트 모듈 경계는 "진입점(entry point)"에서 한 번 정의하면 해당 모듈에 import된 모든 모듈이 클라이언트 컴포넌트로 간주됩니다.

Server Component vs. Client Component를 언제 사용해야 할까요?

Server Component와 Client Component 사이에서 선택하는 것을 간소화하기 위해, Client Component가 필요해질 때까지 Server Components(앱 디렉토리의 기본값)를 사용하는 것이 좋습니다.

이 표는 Server Component와 Client Component의 다른 사용 사례를 요약합니다.

무엇을 해야 할까요?Server ComponentClient Component
데이터 가져오기⚠️
백엔드 리소스에 직접 액세스하기
서버에 민감한 정보 유지하기 (액세스 토큰, API 키 등)
큰 종속성을 서버에 유지/클라이언트 측 JavaScript 줄이기
상호 작용 및 이벤트 리스너 추가하기(onClick(), onChange() 등)
상태 및 라이프사이클 효과 사용하기(useState(), useReducer(), useEffect() 등)
브라우저 전용 API 사용하기
상태, 효과 또는 브라우저 전용 API에 의존하는 사용자 지정 훅 사용하기
React Class 컴포넌트 사용하기

(client component 에서 데이터 가져오는 것은 이 링크 참고 https://beta.nextjs.org/docs/rendering/server-and-client-components#data-fetching)

클라이언트 컴포넌트를 Leaf로 이동시키기

애플리케이션 성능을 개선하기 위해서, 가능한 경우 클라이언트 컴포넌트를 컴포넌트 트리의 leaf로 이동하는 것이 좋습니다.

예를 들어, 정적인 요소 (로고, 링크 등)와 상태를 사용하는 인터랙티브 검색바를 가진 레이아웃이 있다고 가정해봅시다.

레이아웃 전체를 클라이언트 컴포넌트로 만드는 대신, 인터랙티브한 로직을 클라이언트 컴포넌트()로 이동시키고 레이아웃은 서버 컴포넌트로 유지하는 것이 좋습니다. 이렇게 하면 레이아웃의 모든 컴포넌트 Javascript를 클라이언트로 보낼 필요가 없어집니다.

// app/layout.js
// SearchBar는 클라이언트 컴포넌트입니다
import SearchBar from './SearchBar';
// Logo는 서버 컴포넌트입니다
import Logo from './Logo';

// 레이아웃은 기본적으로 서버 컴포넌트입니다
export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  );
}

서버 컴포넌트를 클라이언트 컴포넌트에서 불러오기

서버 컴포넌트와 클라이언트 컴포넌트는 동일한 컴포넌트 트리에 교차해서 사용할 수 있습니다. React 내부에서는 두 환경의 작업을 병합합니다.

그러나 React에서는 서버 컴포넌트에서는 서버 전용 코드(예: 데이터베이스 또는 파일 시스템 유틸리티)가 포함될 수 있기 때문에, 클라이언트 컴포넌트에서 서버 컴포넌트를 가져올 때 제한이 있습니다.

예를 들어, 클라이언트 컴포넌트에서 서버 컴포넌트를 가져오는 것은 작동하지 않습니다.

app/ClientComponent.js

'use client';

// ❌ 이 패턴은 작동하지 않습니다. 서버 컴포넌트를 클라이언트 컴포넌트에서 가져올 수 없습니다.
import ServerComponent from './ServerComponent';

export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

반면에, 서버 컴포넌트를 클라이언트 컴포넌트의 자식 요소나 프롭으로 전달할 수 있습니다. 이를 위해서는 두 컴포넌트를 둘러싸는 다른 서버 컴포넌트를 만들면 됩니다. 예를 들어:

app/ClientComponent.js

'use client';

export default function ClientComponent({children}) {
  return (
    <>
      {children}
    </>
  );
}

app/page.js

// ✅ 이 패턴은 작동합니다. 서버 컴포넌트를 클라이언트 컴포넌트의 자식 요소나 프롭으로 전달할 수 있습니다.
import ClientComponent from "./ClientComponent";
import ServerComponent from "./ServerComponent";

// 페이지는 기본적으로 서버 컴포넌트입니다
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

이 패턴을 사용하면, React는 를 서버에서 렌더링한 후 결과를 클라이언트로 보내야 함을 알고, 서버 컴포넌트 안에는 서버 전용 코드가 없기 때문에 결과를 그대로 클라이언트에서 렌더링합니다. 클라이언트 컴포넌트의 관점에서는, 자식 요소가 이미 렌더링된 것처럼 보입니다.

이 패턴은 레이아웃과 페이지에서 이미 children 프롭을 사용하여 구현되어 있기 때문에 추가적인 래퍼 컴포넌트를 만들 필요가 없습니다.

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

서버 컴포넌트에서 클라이언트 컴포넌트로 전달되는 프롭은 직렬화될 수 있어야 합니다. 이는 함수, 날짜 등과 같은 값은 직접적으로 클라이언트 컴포넌트로 전달될 수 없다는 것을 의미합니다.

네트워크 경계는 어디인가요?

app 디렉토리에서 네트워크 경계는 서버 컴포넌트와 클라이언트 컴포넌트 사이에 있습니다. 이는 페이지 디렉토리에서 getStaticProps/getServerSideProps와 페이지 컴포넌트 사이에 있는 경계와 다릅니다. 서버 컴포넌트에서 가져온 데이터는 클라이언트 컴포넌트로 전달되지 않는 한 직렬화할 필요가 없습니다. 서버 컴포넌트로 데이터를 가져오는 방법에 대해 자세히 알아보세요.

클라이언트 컴포넌트에서 서버 전용 코드 배제하기 (Poisoning)

자바스크립트 모듈은 서버 컴포넌트와 클라이언트 컴포넌트 모두에서 공유될 수 있기 때문에, 서버에서만 실행될 것으로 예상되는 코드가 클라이언트에서 실행될 수 있는 가능성이 있습니다.

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

lib/data.js

export async function getData() {
  let res = await fetch("https://external-service.com/data", {
    headers: {
      authorization: process.env.API_KEY,
    },
  });

  return res.json();
}

처음 보면, getData가 서버와 클라이언트 모두에서 작동하는 것처럼 보입니다. 그러나 API_KEY 환경 변수가 NEXT_PUBLIC로 시작하지 않았기 때문에, 서버에서만 액세스할 수 있는 개인적인 변수입니다. Next.js는 클라이언트 코드에서 안전하지 않은 정보가 누출되는 것을 방지하기 위해 비공개 환경 변수를 빈 문자열로 대체합니다.

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

따라서, 이 함수는 서버에서만 실행될 것으로 의도되었습니다.

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

먼저 패키지를 설치합니다.

npm install server-only

그런 다음 서버 전용 코드를 포함하는 모듈에서 패키지를 가져옵니다.

lib/data.js

import "server-only";

export async function getData() {
  let resp = await fetch("https://external-service.com/data", {
    headers: {
      authorization: process.env.API_KEY,
    },
  });

  return resp.json();
}

이제 getData()를 가져오는 클라이언트 컴포넌트는 이 모듈이 서버에서만 사용될 수 있다는 빌드 시간 오류를 받게 됩니다.

client-only 패키지는 창 객체에 액세스하는 코드와 같은 클라이언트 전용 코드가 포함된 모듈을 표시하는 데 사용할 수 있습니다.

Third-party packages

"use client" 지시문은 Server Components의 일부로 소개된 새로운 React 기능입니다. Server Components가 아직 매우 새로운 기술이기 때문에, 클라이언트-only 기능인 useState, useEffect 및 createContext를 사용하는 컴포넌트에 이를 추가하는 제3자 패키지도 이제 막 등장하고 있습니다.

현재 npm 패키지에서 이 지시문이 아직 포함되지 않은 클라이언트-only 기능을 사용하는 많은 컴포넌트가 있습니다. 이러한 제3자 컴포넌트는 자체적으로 "use client" 지시문이 있기 때문에 자신의 Client Component 내에서 예상대로 작동하지만, Server Components 내에서는 작동하지 않습니다.

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

Client Component 내에서 을 사용하면 예상대로 작동합니다.

app/gallery.js

'use client';

import { useState } from 'react';
import { AcmeCarousel } from 'acme-carousel';

export default function Gallery() {
  let [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>

      {/* 🟢 AcmeCarousel이 Client Component 내에서 사용되기 때문에 작동합니다. */}
      {isOpen && <AcmeCarousel />}
    </div>
  );
}

그러나 Server Component 내에서 직접 사용하려고 하면 오류가 발생합니다.

app/page.js

import { AcmeCarousel } from 'acme-carousel';

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

      {/* 🔴 `useState`는 Server Component 내에서 사용할 수 없습니다. */}
      <AcmeCarousel />
    </div>
  );
}

이는 Next.js가 이 클라이언트-only 기능을 사용한다는 사실을 모르기 때문입니다.

이 문제를 해결하려면 클라이언트-only 기능을 사용하는 제3자 컴포넌트를 자체 Client Component로 감쌀 수 있습니다.

app/carousel.js

'use client';

import { AcmeCarousel } from 'acme-carousel';

export default AcmeCarousel;

이제 Server Component 내에서 을 직접 사용할 수 있습니다.

app/page.js

import Carousel from './carousel';

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

      {/* 🟢 Carousel이 Client Component이기 때문에 작동합니다. */}
      <Carousel />
    </div>
  );
}

우리는 대부분의 제3자 컴포넌트를 Client Components 내에서 사용할 것으로 예상하지만, provider 컴포넌트는 React state와 context에 의존하므로 일반적으로 애플리케이션의 루트에서 필요합니다. 제3자 컨텍스트 제공자에 대해 자세히 알아보세요.

데이터 가져 오기

클라이언트 컴포넌트에서 데이터를 가져올 수는 있지만, 특별한 이유가 없는 한 서버 컴포넌트에서 데이터를 가져오는 것이 좋습니다. 데이터 가져 오기를 서버로 이동하면 성능과 사용자 경험이 향상됩니다.

데이터 가져 오기에 대해 자세히 알아보세요.

컨텍스트

대부분의 React 애플리케이션은 createContext를 직접적으로 또는 제3자 라이브러리에서 가져온 provider 컴포넌트를 통해 컴포넌트 간 데이터를 공유하기 위해 컨텍스트를 사용합니다.

Next.js 13에서 컨텍스트는 Client Components 내에서 완전히 지원됩니다. 그러나 Server Components 내에서 직접적으로 생성하거나 사용할 수는 없습니다. 이는 Server Components가 상호작용하지 않기 때문에 React state가 없기 때문입니다. 그리고 컨텍스트는 주로 React state가 업데이트된 후 하위 인터랙티브 컴포넌트를 다시 렌더링하는 데 사용됩니다.

Server Components 간 데이터 공유를 위한 대체 방법에 대해 설명하기 전에 Client Components 내에서 컨텍스트를 사용하는 방법을 살펴보겠습니다.

Client Components에서 컨텍스트 사용하기

모든 컨텍스트 API가 Client Components 내에서 완전히 지원됩니다.

app/sidebar.js

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

그러나 컨텍스트 제공자는 현재 테마와 같은 전역적인 관심사를 공유하기 위해 애플리케이션의 루트 근처에 렌더링됩니다. 컨텍스트가 Server Components에서 지원되지 않기 때문에 애플리케이션의 루트에서 컨텍스트를 생성하려고 하면 오류를 일으킵니다.

app/layout.tsx

import { createContext } from 'react';

// 🔴 createContext는 Server Components에서 지원되지 않습니다.
export const ThemeContext = createContext({});

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">
          {children}
        </ThemeContext.Provider>
      </body>
    </html>
  );
}

이를 수정하려면 컨텍스트를 생성하고 해당 프로바이더를 클라이언트 컴포넌트 내부에 렌더링하면 됩니다:

app/theme-provider.tsx

'use client';

import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );
}

위 코드에서는 createContext를 사용하고 있습니다. 그러나 이 코드는 Client Components에서만 작동합니다.

이제 프로바이더가 루트에서 렌더링되었으므로 Server Component에서 해당 프로바이더를 직접 렌더링할 수 있습니다. 이를 위해서는 해당 프로바이더가 Client Component임을 나타내는 'use client' 주석을 추가해야 합니다.

app/layout.tsx

import ThemeProvider from './theme-provider';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

이제 프로바이더가 루트에서 렌더링되었으므로 앱 내의 모든 클라이언트 컴포넌트가 해당 컨텍스트를 사용할 수 있게 됩니다.

참고: 프로바이더는 가능한 한 깊이 있는 곳에서 렌더링해야 합니다. ThemeProvider가 전체 문서 대신 {children}만 감싸도록 했습니다. 이렇게 하면 Next.js가 Server Component의 정적 부분을 최적화하기 쉬워집니다.

Server Components에서 서드파티 컨텍스트 프로바이더 렌더링하기

일반적으로 npm 패키지에는 애플리케이션의 루트 근처에서 렌더링해야 하는 Provider가 포함되어 있습니다. 이러한 프로바이더가 'use client' 지시문을 포함하고 있다면 Server Components 내에서 직접 렌더링할 수 있습니다. 그러나 Server Components가 아직 너무 새로운 기능이므로, 많은 써드파티 프로바이더들은 아직 이 지시문을 추가하지 않은 경우가 많습니다.

"use client"를 포함하지 않은 써드파티 프로바이더를 렌더링하려고 하면 다음과 같은 오류가 발생합니다.

app/layout.js

import { ThemeProvider } from 'acme-theme';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* 🔴 Error: `createContext` can't be used in Server Components */}
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

이를 해결하기 위해서는 새로운 클라이언트 컴포넌트로 해당 프로바이더를 감싸주어야 합니다.

app/providers.js

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

프로바이더를 루트에서 렌더링하면 해당 라이브러리에서 제공하는 모든 컴포넌트 및 훅이 자체 Client Component 내에서 예상대로 작동합니다.

써드파티 라이브러리가 자체 클라이언트 코드에 "use client"를 추가하면 래퍼 Client Component를 제거할 수 있게 됩니다.

Server Components 간 데이터 공유하기

Server Components는 대화형이 아니기 때문에 React state를 읽어들일 필요가 없으므로, 데이터를 공유하기 위해서 컨텍스트의 전체 기능이 필요하지는 않습니다. 여러 Server Component에서 공통적으로 사용해야 하는 데이터가 있다면, 모듈 스코프 내에서 전역 싱글톤과 같은 네이티브 JavaScript 패턴을 사용할 수 있습니다.

예를 들어, 여러 컴포넌트에서 데이터베이스 연결을 공유하기 위해 다음과 같은 모듈을 사용할 수 있습니다.

utils/database.js

export const db = new DatabaseConnection(...);
app/users/layout.js
import { db } from "@utils/database";

export async function UsersLayout() {
  let users = await db.query(...);
  // ...
}
app/users/[id]/page.js
import { db } from "@utils/database";

export async function DashboardPage() {
  let user = await db.query(...);
  // ...
}

위 예제에서는 레이아웃과 페이지 모두 데이터베이스 쿼리를 수행해야 합니다. 이러한 각 컴포넌트는 @utils/database 모듈을 가져와 데이터베이스에 액세스할 수 있습니다.

Server ComponentsFetch 요청 공유하기
데이터를 가져올 때, 페이지나 레이아웃과 그 하위 컴포넌트들 사이에서 fetch 결과를 공유하고 싶을 수 있습니다. 이러한 컴포넌트들 간에는 불필요한 결합도가 생기며, 컴포넌트 간에 props를 주고받아야 하는 문제가 발생할 수 있습니다.

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

0개의 댓글