뒤로
시간을 달리는 인사이트
시간을 달리는 인사이트 ·

Next.js의 app 디렉터리 아키텍처 이해하기 (의역)

Next.js 13 버전이 공개된 이후로, 그 안에 포함된 신규 기능들의 안정성에 대한 논의가 진행되고 있습니다. "Next.js 13의 새로운 기능은 무엇인가요?"라는 질문에 대해, 우리는 이번 버전에서 추가된 내용들을 살펴보고, 여러 실험을 통해 Next.js 13의 안정성을 확인했습니다. 또한 이후에는 새로 도입된 <Link> 및 <Image> 컴포넌트와 (아직 베타 버전인) @next/font에 대해 깊이 있게 살펴보았고, 이들은 사용자에게 즉시적인 이점을 제공합니다. 터보팩은 아직 알파 버전이며, 개발 빌드를 위한 것으로 발표되었습니다. 아직 개발이 진행 중이므로, 통합 및 최적화가 완료되지 않았습니다. 따라서 여러분의 프로젝트에서 사용 가능한지는 여러분의 기술 스택에 따라 다를 것입니다. 이 글은 주로 이번 발표에서 주요 주제였던 새로운 앱 디렉터리 구조(AppDir)에 초점을 맞추고 있습니다.

원문: Understanding App Directory Architecture in Next.js

앱 디렉터리는 React 서버 컴포넌트와 엣지 런타임, 이 두 가지 핵심적인 React 생태계의 변화와 결합되어 있기 때문에 계속해서 주목을 받고 있습니다. 이는 무엇보다도 Next.js의 미래 지향적인 모습을 가늠하게 합니다. 하지만 아직은 실험적인 단계이며, 완성될 수 있는 로드맵이 며칠 내에 나올 것 같지는 않습니다. 그렇다면, 지금 바로 프로덕션에 도입해야 할까요? 만약 그렇게 한다면 어떤 이점을 얻을 수 있고, 어떤 위험에 노출될 수 있을까요? 소프트웨어 개발의 답변처럼 '상황에 따라 다르다'는 말이 가장 적절할 것입니다.

그럼, 앱 디렉터리가 무엇인지부터 알아봅시다. 앱 디렉터리는 Next.js에서 라우팅을 처리하고 뷰를 렌더링하는 새로운 전략입니다. 이는 서로 다른 몇 가지 기능을 합쳐 React의 Concurrent 기능을 최대한 활용하도록 설계되었습니다. (네, 바로 React Suspense에 대해 말하는 겁니다.) 이는 Next.js 앱에서 컴포넌트와 페이지를 어떻게 생각해야 하는지에 대한 패러다임 변화를 가져왔습니다. 이 새로운 앱 빌드 방식은 아키텍처에 많은 개선점을 제공합니다. 간단하게 요약하면 다음과 같습니다.

  • 부분 라우팅

  • 라우트 그룹

  • 병렬 라우트

  • 인터셉팅 라우트

  • 서버 컴포넌트 vs 클라이언트 컴포넌트

  • Suspense 바운더리 더 자세한 내용은 이 문서에서 확인할 수 있습니다.

비교를 위해 현재의 라우팅 및 렌더링 아키텍처(Page 디렉터리)를 보면, 개발자는 각 라우트별로 데이터를 가져와야 했습니다.

  • getServerSideProps: 서버 사이드 렌더링

  • getStaticProps: 서버 사이드 프리 렌더링 또는 증분 정적 재생성

  • getStaticPaths + getStaticProps: 서버 사이드 프리 렌더링 또는 정적 사이트 생성 지금까지는 페이지 단위로 렌더링 전략을 선택하는 것이 불가능했습니다. 대부분의 앱은 전반적으로 서버 사이드 렌더링이나 정적 사이트 생성을 사용했습니다. 하지만 Next.js는 이제 아키텍처 내에서 개별 라우트를 생각하는 것을 표준화하는 데 충분한 추상화를 제공하였습니다.

앱이 브라우저에 도달하면 하이드레이션이 시작되고, _app 컴포넌트를 React 컨텍스트 Provider로 감싸 데이터를 공유하는 라우트를 생성하게 됩니다. 이를 통해 데이터를 렌더링 트리의 맨 위로 올리고 앱의 각각의 컴포넌트로 계단식으로 내려갈 수 있게 되었습니다.

import { type AppProps } from "next/app";

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <SomeProvider>
      <Component {...pageProps} />
    </SomeProvider>
  );
}

전체 데이터를 앱에서 쉽게 사용할 수 있게 도와주는 이 전략은 매우 유용했습니다. 그러나, 모든 것이 컨텍스트 Provider로 감싸지게 되면서 앱의 루트와 데이터 하이드레이션(hydration)이 묶이게 되었죠. 결국, 서버에서 해당 트리(즉, 특정 컨텍스트의 Provider 내 모든 라우트)의 한 부분을 렌더링하는 것이 불가능해졌습니다.

그래서 레이아웃 패턴이 등장했습니다. 페이지 주변에 래퍼(wrapper)를 만들면, 전체 앱에 대한 렌더링 전략을 결정하는 대신, 각 라우트별로 렌더링 전략을 선택하거나 변경할 수 있게 됩니다. 페이지 디렉터리에서 어떻게 상태를 관리하는지에 대한 자세한 내용은 “Next.js의 상태관리” 글과 Next.js 공식 문서를 참조하세요.

레이아웃 패턴은 대단한 해결책이었습니다. 렌더링 전략을 세세하게 정의할 수 있게 되었죠. 그래서, App 디렉터리는 레이아웃 패턴을 중심에 두었습니다. 그 결과, Next.js 아키텍처는 성능, 보안, 데이터 처리 측면에서 엄청난 발전을 이루게 되었습니다.

리액트의 Concurrent 기능을 활용하면 이제 각 컴포넌트를 브라우저로 스트리밍하면서, 각 컴포넌트가 자신의 데이터를 직접 처리할 수 있게 되었습니다. 이로 인해, 이제 렌더링 전략이 페이지 전체가 아닌 컴포넌트 기반으로 훨씬 더 세분화되었습니다. 레이아웃이 기본적으로 중첩되므로, 개발자는 파일 시스템 아키텍처를 따라 각 페이지가 어떻게 변화하는지 더 정확하게 파악할 수 있게 되었습니다. 그리고 컨텍스트를 사용하려면 "use client" 지시문을 통해 컴포넌트를 클라이언트 사이드로 명시적으로 전환해야 합니다.

App 디렉터리의 구성은 어떻게 되나요?

이 아키텍처는 페이지별 레이아웃 아키텍처를 기반으로 합니다. 이제는 app 컴포넌트나 document 컴포넌트가 없습니다. 이 두 컴포넌트는 모두 루트 layout.jsx 컴포넌트로 대체되었습니다. 예상하셨겠지만, 이 컴포넌트는 전체 애플리케이션을 감싸는 특별한 레이아웃입니다.

export function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

루트 레이아웃은 서버가 전체 앱에 반환하는 HTML을 한 번에 조작하는 방법입니다. 루트 레이아웃은 서버 컴포넌트이며 페이지 이동시 다시 렌더링되지 않습니다. 즉, 레이아웃의 모든 데이터나 상태는 앱의 수명주기 내내 유지됩니다.

루트 레이아웃은 전체 앱을 위한 특별한 컴포넌트지만, 다른 구성요소를 위한 루트 컴포넌트도 가질 수 있습니다.

  • loading.jsx: 전체 라우트의 Suspense 바운더리를 정의할 수 있습니다.

  • error.jsx: 전체 라우트의 에러 바운더리를 정의할 수 있습니다.

  • template.jsx: 레이아웃과 유사하지만, 모든 페이지 이동시 다시 렌더링합니다. 인/아웃 트랜지션과 같은 경로 간 상태를 처리하는데 특히 유용합니다.

이러한 컴포넌트와 규칙은 기본적으로 중첩됩니다. 즉, /about은 자동으로 /의 래퍼 안에 중첩됩니다.

마지막으로, 모든 라우트에 대한 page.jsx가 있어야 하는데, 이는 해당 URL 세그먼트(당신이 컴포넌트를 넣는 위치로 알고 있는)에 대해 렌더링할 주요 컴포넌트를 정의하기 때문입니다. 이 컴포넌트는 기본적으로 중첩되지 않으며, 해당 URL 세그먼트와 정확하게 일치하는 경우에만 DOM에 표시됩니다.

아키텍처에는 훨씬 더 많은 기능이 추가될 예정이지만, 이정도면 프로덕션 환경에서 Page 디렉터리를 App 디렉터리로 마이그레이션을 고려하기 전 멘탈 모델을 세우기에 충분할 것입니다. 꼭 공식 업그레이드 가이드도 확인하세요.

서버 컴포넌트 요약

리액트 서버 컴포넌트를 사용하면 앱은 인프라를 활용해 성능과 전반적인 사용자 경험을 개선할 수 있습니다. 예를 들어 RSC는 최종 번들에 종속되지 않기 때문에 번들 크기가 즉각적으로 개선됩니다. 그리고 서버에 렌더링 되기 때문에 모든 종류의 파싱, 포맷팅 또는 컴포넌트 라이브러리가 서버에 남게 됩니다. 또한, 비동기적인 특성 덕분에 서버 컴포넌트는 클라이언트로 스트리밍됩니다. 이를 통해 렌더링 된 HTML은 브라우저에서 점진적으로 개선될 수 있습니다.

따라서 서버 컴포넌트는 앱 크기와 번들 크기 사이의 선형적 상관관계를 깨고 최종 번들의 크기를 보다 예측할 수 있고 캐싱할 수 있으며 일정하게 유지할 수 있게 합니다. 따라서 RSC는 기존 리액트 컴포넌트(현재는 혼동을 피하기 위해 클라이언트 컴포넌트로 부름)에 비해 모범 사례로 즉시 자리 잡았습니다.

서버 컴포넌트에서 데이터 페칭은 훨씬 유연하며, 제 생각에는 바닐라 자바스크립트에 더 가깝게 느껴져 러닝 커브가 낮다고 느껴집니다. 예를 들어 자바스크립트 런타임을 이해하면 데이터 페칭을 병렬 또는 순차적으로 정의할 수 있으므로 리소스 로딩 워터폴을 더욱 세밀하게 제어할 수 있게 됩니다.

  • 병렬 데이터 페칭, 모든 것을 기다립니다.

import TodoList from "./todo-list";

async function getUser(userId) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json();
}
async function getTodos(userId) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json();
}
export default async function Page({ params: { userId } }) {
  // 두 요청을 동시에 시작합니다.
  const userResponse = getUser(userId);
  const todosResponse = getTodos(username);
  // 모든 프라미스가 이행될 때까지 기다립니다.
  const [user, todos] = await Promise.all([userResponse, todosResponse]);
  return (
    <>
      <h1>{user.name}</h1>
      <TodoList list={todos}></TodoList>
    </>
  );
}
  • 병렬로 진행. 한 요청을 기다리면서 다른 요청을 스트리밍합니다.

async function getUser(userId) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json();
}

async function getTodos(userId) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json();
}
export default async function Page({ params: { userId } }) {
  // 두 요청을 동시에 시작합니다.
  const userResponse = getUser(userId);
  const todosResponse = getTodos(userId);
  // user 정보를 기다립니다.
  const user = await userResponse;
  return (
    <>
      <h1>{user.name}</h1>
      <Suspense fallback={<div>Fetching todos...</div>}>
        <TodoList listPromise={todosResponse}></TodoList>
      </Suspense>
    </>
  );
}
async function TodoList({ listPromise }) {
  // 앨범 프라미스가 이행되도록 기다립니다.
  const todos = await listPromise;
  return (
    <ul>
      {todos.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
}

이 경우 <TodoList>는 수행중인 프라미스를 수신하고 렌더링하기 전에 이를 기다려야 합니다. 앱은 모든 작업이 완료될 때까지 suspense fallback 컴포넌트를 렌더링 합니다.

  • 순차적 데이터 페칭은 한 번에 하나의 요청을 실행하고 각 요청을 기다립니다.

async function getUser(username) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json();
}

async function getTodos(username) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json();
}

export default async function Page({ params: { userId } }) {
  const user = await getUser(userId);
  return (
    <>
      <h1>{user.name}</h1>
      <Suspense fallback={<div>Fetching todos...</div>}>
        <TodoList userId={userId} />
      </Suspense>
    </>
  );
}
async function TodoList({ userId }) {
  const todos = await getTodos(userId);
  return (
    <ul>
      {todos.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
}

이제 Page는 getUser 데이터를 가져온 뒤에 렌더링을 시작합니다. <TodoList>를 만나면, getTodos 데이터를 가져오는 것을 기다린 후 렌더링을 시작하게 됩니다. 이는 페이지 디렉터리에서 사용하던 것에 비해 더 세밀하게 처리되는 것입니다.

그리고 아래의 중요한 사항들을 주의하시기 바랍니다.

  • 같은 컴포넌트 범위 내에서 실행되는 요청들은 병렬로 처리됩니다.(더 자세한 내용은 아래에 있는 확장된 Fetch API 부분에서 확인하실 수 있습니다.)

  • 같은 서버 런타임에서 실행되는 동일한 요청들은 중복이 제거됩니다.(실제로는 가장 짧은 캐시 만료 시간을 가진 하나의 요청만 실행됩니다.)

  • fetch를 사용하지 않는 요청들(예: SDK, ORM 또는 데이터베이스 클라이언트와 같은 제3자 라이브러리)의 경우, 수동으로 세그먼트 캐시 설정을 하지 않는 한, 라우트 캐싱에는 영향을 끼치지 않습니다.

export const revalidate = 600; // 매 10분마다 revalidate

export default function Contributors({ params }: { params: { projectId: string } }) {
  const { projectId } = params;
  const { contributors } = await myORM.db.workspace.project({ id: projectId });
  return <ul>{/* ... */}</ul>;
}

이는 개발자들에게 제어력을 더욱 강화시켜줍시다. Page 디렉터리에서는 모든 데이터를 사용할 수 있게 될 때까지 렌더링이 지연됩니다. getServerSideProps를 활용하면 사용자에게 로딩 스피너가 계속 표시되며, 모든 라우트의 데이터를 사용 가능해질 때까지 대기하게 됩니다. App 디렉터리에서 이런 행동을 복제하려면, 해당 라우트의 layout.tsx에서 패치 요청이 필요하며, 이는 가능하면 피해야 합니다. "전부 아니면 아예 없다"는 접근법은 거의 필요하지 않으며, 세분화된 전략에 비해 사용자 경험을 저하시킵니다.

확장된 Fetch API에 대해 이야기해보자면, 문법은 fetch(route, options)과 동일합니다. 그러나 웹 Fetch 사양에 따르면, options.cache는 이 API가 브라우저 캐시와 어떻게 상호 작용할지를 결정합니다. 그러나 Next.js에서는 프레임워크의 서버 측 HTTP 캐시와 상호 작용합니다.

Next.js의 확장된 Fetch API 및 캐시 정책을 이해하는데 중요한 두 가지 값이 있습니다.

  1. force-cache: 기본 값입니다. 새로운 매칭을 찾아 반환합니다.

  2. no-store 또는 no-cache: 모든 요청을 원격 서버에서 패치합니다.

  3. next.revalidate: ISR과 동일한 구문이며, 리소스를 새로운 리소스로 간주하는 엄격한 한계값을 지정합니다. fetch(https://route, { cache: "force-cache", next: { revalidate: 60 } });

요청을 분류할 수 있는 캐싱 전략이 있습니다.

  1. 정적 데이터: 장기간 유지됩니다. (예: 블로그 글)

  2. 동적 데이터: 자주 변경되거나 사용자 상호작용에 따른 결과물. (예: 댓글 섹션, 장바구니)

기본적으로 모든 데이터는 정적 데이터로 간주됩니다. 이는 force-cache가 기본 캐싱 전략이기 때문입니다. 완전히 동적인 데이터에 대해서는 no-store 또는 no-cache를 설정하면 됩니다.

쿠키 또는 헤더 설정과 같은 동적 기능을 사용하는 경우 기본값이 force-cache에서 no-store로 바뀌는 부분에 유의해야 합니다!

마지막으로, 증분적인 정적 재생성과 비슷한 기능을 구현하려면 next.revalidate를 사용해야 합니다. 전체 라우트에 대해 정의하는 대신 해당 경로의 일부인 컴포넌트만 정의한다는 장점이 있습니다.

Page 디렉터리에서 App 디렉터리로 이동하는 것은 많은 작업처럼 보일 수 있지만, Next.js는 두 아키텍처가 공존할 수 있도록 준비되어 있어 단계적으로 마이그레이션을 수행할 수 있습니다. 또한, 문서에는 아주 훌륭한 마이그레이션 가이드가 있으니, 리팩터링을 시작하기 전에 충분히 읽어보는 것이 좋습니다.

마이그레이션 경로를 안내하는 것은 이 글의 범위를 벗어나며, 위에 제공된 문서와 중복될 수 있습니다. 대신에, 공식 문서가 제공하는 정보를 보완하고, 제 경험에서 예상되는 문제점에 대한 통찰력을 제공하려고 노력하겠습니다.

만제어력을 더 많이 부여하는 방법에 대해 살펴봅시다. Page 디렉터리에서는 모든 데이터가 사용 가능해질 때까지 렌더링이 중지됩니다. getServerSideProps를 사용하면, 사용자에게는 로딩 스피너가 계속 보이게 됩니다. 이것이 전체 라우트의 데이터가 준비될 때까지입니다. App 디렉터리에서 이러한 동작을 흉내 내려면, 해당 라우트의 layout.tsx에서 데이터를 가져오는 요청이 발생해야 합니다. 이는 항상 피해야 합니다. "모든 것이 아니면 아무 것도 없다"는 접근 방식은 거의 필요하지 않으며, 세부적인 전략과는 반대로 사용자 경험을 해치게 됩니다.

Next.js에서 확장된 Fetch API에 대해 논의해봅시다. 구문은 fetch(route, options)와 같습니다. 하지만 웹 Fetch 사양에 따르면 options.cache는 이 API가 브라우저 캐시와 어떻게 상호 작용하는지를 결정합니다. 그러나 Next.js에서는 프레임워크의 서버 측 HTTP 캐시와 상호 작용합니다.

Next.js의 확장된 Fetch API와 캐시 정책에 관련해서 이해해야 할 두 가지 값이 있습니다:

  • force-cache: 이것이 기본값이며, 새로운 match를 찾아 반환합니다.

  • no-store 또는 no-cache: 모든 요청은 원격 서버에서 데이터를 가져옵니다.

  • next.revalidate: 이것은 ISR(Incremental Static Regeneration)와 같은 구문이며, 리소스를 새 리소스로 간주하는 엄격한 임계값을 설정합니다.

  • fetch(https://route, { cache: "force-cache", next: { revalidate: 60 } });

요청을 캐싱 전략에 따라 분류할 수 있습니다:

  • 정적 데이터: 오랫동안 유지됩니다. 예를 들면 블로그 글 등이 있습니다.

  • 동적 데이터: 자주 변경되거나 사용자 상호 작용의 결과물입니다. 예를 들어, 댓글 섹션이나 장바구니와 같은 것들입니다.

기본적으로 모든 데이터는 정적 데이터로 간주됩니다. 이는 force-cache가 기본 캐시 전략이기 때문입니다. 완전히 동적인 데이터에 대해 force-cache를 사용하고 싶지 않다면 no-store 또는 no-cache를 지정하면 됩니다.

쿠키 또는 헤더 설정과 같은 동적 기능을 사용하는 경우, 기본값이 force-cache에서 no-store로 변경되는 것을 주의하세요!

마지막으로, 증분적으로 정적 재생성과 유사한 기능을 구현하려면 next.revalidate를 사용해야 합니다. 이를 통해 전체 라우트에 대해 정의하는 것이 아니라 해당 경로의 일부인 컴포넌트만 정의할 수 있습니다.

Page 디렉터리에서 App 디렉터리로 이동하기 Page 디렉터리에서 App 디렉터리로 로직을 옮기는 것은 많은 작업처럼 보일 수 있지만, Next.js는 두 아키텍처가 공존할 수 있도록 해주어 점진적으로 이동할 수 있습니다. 또한, 리팩토링을 시작하기 전에 문서에 있는 훌륭한 마이그레이션 가이드를 충분히 읽어보시기를 권장드립니다.

마이그레이션 경로를 안내하는 것은 이 글의 범위를 벗어나고, 기존 문서와 중복될 수 있습니다. 대신, 공식 문서에 제공된 것 이외에 제 경험에서 예상되는 문제점에 대한 통찰력을 제공하려고 노력할 것입니다.

리액트 컨텍스트를 다루는 경우 이 글에서 언급한 모든 이점을 제공하기 위해 RSC(React Server Component)는 상호작용할 수 없고, 이는 hook가 없다는 것을 의미합니다. 그래서 클라이언트 측 로직을 렌더링 트리의 하위 컴포넌트로 최대한 늦게 밀어넣게 되었고, 상호작용 기능을 추가하면 해당 컴포넌트의 자식이 클라이언트 측이 됩니다.

일부 컴포넌트를 밀어넣는 것이 불가능한 경우도 있습니다. 예를 들어, 일부 핵심 기능이 리액트 컨텍스트에 의존하고 있는 경우입니다. 대부분의 라이브러리는 속성 전달을 방지하기 위해 준비되어 있으므로, 많은 라이브러리는 루트에서 멀리 떨어진 자식 컴포넌트로 건너뛰는 컨텍스트 공급자를 생성합니다. 따라서 리액트 컨데이터를 얼마나 개발자가 통제할 수 있을까요? Page 디렉터리에서는 모든 데이터가 사용 가능해질 때까지 렌더링이 중지됩니다. getServerSideProps를 이용하면, 해당 라우트의 모든 데이터가 준비될 때까지 사용자는 계속해서 로딩 스피너를 볼 수밖에 없습니다. 이 행동을 App 디렉터리에서 흉내내려면, 해당 라우트의 layout.tsx에서 데이터 요청을 발생시켜야 하며, 이는 우리가 항상 피하려는 상황입니다. 이러한 '전부 아니면 아무것도 없다' 방식은 거의 필요하지 않으며, 더 미세한 전략과는 대조적으로 체감 성능을 저하시킵니다.

확장된 Fetch API에 대해 살펴봅시다. 문법은 fetch(route, options)와 동일합니다. 하지만 웹 Fetch 사양에 따르면 options.cache는 이 API가 브라우저 캐시와 상호작용하는 방식을 결정합니다. 그러나 Next.js에서는 프레임워크 서버 사이드 HTTP 캐시와 상호작용합니다.

Next.js의 확장된 Fetch API 및 캐시 정책과 관련하여 이해해야 할 몇 가지 값이 있습니다:

  • force-cache: 기본값. 새로운 match를 찾아 반환합니다.

  • no-store 또는 no-cache: 모든 요청에 대해 원격 서버에서 가져옵니다.

  • next.revalidate: ISR과 동일한 구문. 리소스에 엄격한 만료 시점을 지정합니다.

캐싱 전략을 이용하면 요청을 분류할 수 있습니다. 정적 데이터는 더 오래 유지되며 (예: 블로그 글), 동적 데이터는 자주 변경되거나 사용자 상호 작용의 결과물이 됩니다 (예: 댓글 섹션, 장바구니). 기본적으로 모든 데이터는 정적 데이터로 간주됩니다. 완전히 동적인 데이터에 대해서는 force-cache 대신 no-store 또는 no-cache를 사용해야 합니다. 쿠키나 헤더 설정과 같은 동적 기능을 사용하는 경우에는 기본 캐싱 전략이 force-cache에서 no-store로 바뀔 수 있음에 주의하세요.

추가로, 증분 정적 재생성과 유사한 기능을 구현하려면 next.revalidate를 사용해야 합니다. 이를 사용하면 전체 라우트를 정의하는 대신 특정 컴포넌트만 정의할 수 있습니다.

Page 디렉터리에서 App 디렉터리로 이동하는 것은 번거로울 수 있지만, Next.js는 이 두 구조가 공존할 수 있도록 준비되어 있어, 점진적으로 이동할 수 있습니다. 또한 문서에는 리팩토링에 앞서 충분히 읽어봐야 할 훌륭한 이동 가이드가 있습니다.

이 글에서는 이동 경로를 자세히 안내하지는 않습니다. 이는 이미 문서에 잘 설명되어 있기 때문입니다. 대신, 제 경험을 바탕으로 예상할 수 있는 문제점에 대한 통찰을 제공하려 합니다.

리액트 컨텍스트를 사용하는 경우를 생각해봅시다. RSC는 상호작용을 제공하지 않고, 훅을 제공하지 않기 때문에, 클라이언트 사이드 로직은 렌더링 트리의 하위 컴포넌트로 가능한 한 늦게 이동하는 것이 좋습니다. 상호작용 기능이 추가되면, 해당 컴포넌트의 자식은 클라이언트 사이드가 됩니다.

일부 컴포넌트를 이동하는 것이 불가능한 경우도 있습니다. 예를 들어, 핵심 기능이 리액트 컨텍스트에 의존하는 경우입니다. 대부분의 라이브러리는 프로퍼티 드릴링으로부터 사용자를 보호하기 위해 준비가 되어있으며, 컨텍스트 Provider를 생성하여 루트에서 먼 자식 컴포넌트를 뛰어넘습니다. 따라서 리액트 컨텍스트를 완전히 제거하면 일부 외부 라이브러리가 제대로 작동하지 않을 수 있습니다.

이 문제를 해결하기 위한 임시 방법으로 클라이언트 사이드 래퍼를 Provider에 사용할 수 있습니다. 따라서 레이아웃 컴포넌트는 클라이언트 컴포넌트가 렌더링될 때 건너뛸 수 있습니다. 하지만 이 방법을 사용할 때는 <Providers> 컴포넌트 내의 모든 컴포넌트가 클라이언트 사이드에서 렌더링되지 않으므로, 이 방법은 최선한 대안이 아니며 최소화해야 합니다. 가능하면 각 라이브러리를 개별적으로 검토하고 필요한 경우 수정하거나 대체하는 것이 좋습니다.

기존의 훅을 사용하는 경우 리액트 서버 컴포넌트는 훅을 지원하지 않습니다. 그러므로 기존의 훅이 있는 경우에는 리팩토링이 필요합니다. 예를 들어, 데이터 페칭 훅을 사용하는 경우, 이를 확장된 Fetch API를 사용하는 서버 컴포넌트로 변경해야 합니다. 그리고 상태 관리 라이브러리 (예: Redux, MobX 등)를 사용하는 경우에는, 이를 서버 컴포넌트에서 분리하고 클라이언트 컴포넌트에 남겨둬야 합니다.

사용자 상호작용에 따른 데이터 변경 리액트 서버 컴포넌트는 상호작용이 없기 때문에, 서버 컴포넌트가 사용자 상호작용에 의해 변경되는 데이터를 처리하는 방법에 대해 고민해야 합니다. 예를 들어, 사용자가 버튼을 클릭하여 데이터를 변경하는 경우, 이 변경은 클라이언트 컴포넌트에서 처리되어야 합니다. 그런 다음 이 변경은 클라이언트 컴포넌트와 서버 컴포넌트 간의 통신을 통해 서버 컴포넌트로 전달되어야 합니다.

마지막으로, 모든 컴포넌트가 서버 컴포넌트로 전환되지 않아도 괜찮습니다. 서버 컴포넌트는 렌더링 성능 향상을 위한 도구일 뿐이며, 모든 상황에 적합한 것은 아닙니다. 따라서 컴포넌트의 종류와 사용 사례에 따라 최적의 접근 방식을 결정해야 합니다.

이 모든 변경은 상당한 작업을 필요로 하지만, 이는 웹 개발의 미래를 향한 큰 발걸음입니다. 서버 컴포넌트는 렌더링 성능을 크게 향상시키고, 더 복잡한 웹 애플리케이션을 구축하는 데 필요한 도구를 제공합니다. 그러므로 이러한 변화에 대한 준비와 투자는 분명히 가치가 있을 것입니다.

2

댓글

로그인 후 댓글을 남길 수 있습니다.

하영진
하영진

좋은글 감사합니다