본문 바로가기

Study/NextJS 공식문서

[5주 차] Server and Client Composition Patterns

React 애플리케이션을 구축할 때, 어떤 부분을 서버에서 렌더링하고 어떤 부분을 클라이언트에서 렌더링할지 결정하는 것이 중요하다. 이를 위해 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 구성하는 패턴을 이해해야 한다.

데이터를 가져오거나 백엔드 리소스에 직접 접근해야 할 때, 엑세스 토큰이나 API 키 같은 민감한 정보를 서버에 유지하고자 할 때, 클라이언트 측 JavaScript 양을 줄이고자 할 때는 서버 컴포넌트를 사용한다.

onClick, onChange와 같은 이벤트 리스너를 추가하여 상호작용을 구현할 때, useState(), useEffect()와 같은 React 훅을 사용하여 상태 관리나 생명주기 메서드를 활용할 때, 브라우저 전용 API를 사용해야 할 때는 클라이언트 컴포넌트를 사용한다.

 

Server Component Patterns

서버에서 데이터를 가져올 때, 레이아웃과 페이지 등 여러 컴포넌트에서 동일한 데이터를 필요로 할 수 있다. 이 경우, React는 fetch를 확장하여 데이터 요청을 자동으로 메모이제이션한다. 또한, fetch를 사용할 수 없는 경우에는 React의 cache 함수를 사용하여 동일한 데이터를 여러 컴포넌트에서 중복 요청 없이 사용할 수 있습니다.
import { cache } from 'react';

async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

// fetchUserData 함수를 캐싱하여 중복 호출 방지
const getCachedUserData = cache(fetchUserData);

export default async function UserProfile({ userId }) {
  const userData = await getCachedUserData(userId);
  // userData를 사용하여 컴포넌트 렌더링
}

 

JavaScript 모듈은 서버와 클라이언트 컴포넌트 모두에서 공유될 수 있으므로, 서버에서만 실행되어야 하는 코드가 실수로 클라이언트 번들에 포함될 수 있다. 이를 방지하기 위해 
server-only 
패키지를 사용하여 이러한 모듈이 클라이언트 컴포넌트에서 임포트될 경우 빌드 타임 에러를 발생시킬 수 있습니다.
import 'server-only';

export async function getServerData() {
  const response = await fetch('https://api.example.com/data', {
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
    },
  });
  return response.json();
}

 

Client Component Patterns

클라이언트 컴포넌트를 컴포넌트 트리의 하위로 이동시키면 클라이언트 측 JavaScript 번들의 크기를 줄일 수 있다. 예를 들어, 네비게이션 컴포넌트가 정적인 요소(로고, 링크 등)와 상태를 사용하는 검색 바를 포함하고 있다면, 네비게이션을 클라이언트 컴포넌트로 만드는 대신, 상호작용이 필요한 로직을 클라이언트 컴포넌트로 분리하고, 나머지는 서버 컴포넌트로 유지하는 것이 좋다. 이렇게 하면 네비게이션의의 모든 JavaScript를 클라이언트로 전송할 필요가 없다.
// app/layout.js

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

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

 

서버 컴포넌트에서 데이터를 가져온 후, 해당 데이터를 클라이언트 컴포넌트에 Props로 전달해야 할 수 있다. 이때, 서버에서 클라이언트로 전달되는 Props는 JSON과 같이 React에 의해 직렬화될 수 있어야 한다.
// app/page.js
import ClientComponent from './ClientComponent';

export default async function Page() {
  const data = await fetchDataFromServer();

  return <ClientComponent data={data} />;
}

 

Server Component를 Client Conpoment 안에서 사용하는 방법

Server Component는 Client Component에서 직접 import할 수 없다. Server Components는 클라이언트에서 실행할 수 없으며, Client Component 내부에서 Server Component를 직접 import하면 새로운 서버 요청이 필요하므로, 이는 지원되지 않는다. 대신, Server Component를 props로 전달하는 방식이 권장된다.
// Pages in Next.js are Server Components by default
import ClientComponent from "./client-component";
import ServerComponent from "./server-component";

export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent /> {/* ✅ Server Component를 children으로 전달 */}
    </ClientComponent>
  );
}
// Server Component
export default function ServerComponent() {
  return <p>I'm a Server Component</p>;
}
// Client Component
"use client";

export default function ClientComponent({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>; // Server Component를 children으로 받아 사용
}