본문 바로가기
source-code/Next JS

[next js] server component 페이지 route 속도 개선하기 (with streaming)

by mattew4483 2024. 7. 23.
728x90
반응형

Backgrounds

현재 운영 중인 서비스의 관리자 페이지는 Next.js 14 버전으로 구성되어 있습니다.
 
Next.js는 13 버전부터 큰 변화를 겪었는데,
이는 바로 app 디렉터리 내 모든 컴포넌트가 기본적으로 서버 컴포넌트로 동작한다는 것입니다.
 
를 활용해, 생성된 가이드 데이터의 상세 페이지를 서버 컴포넌트로 구성했습니다.
서버 컴포넌트를 사용하면 서버에서 데이터를 가져와 HTML을 생성하고, 클라이언트에 완성된 HTML을 반환할 수 있으며
이를 통해 초기 로딩 시간을 단축하고, 사용자 경험을 향상하고자 했습니다.

// app/[id]/page.tsx
import fetchData from '../lib/fetchData';

export default async function Page({ params }) {
  const data = await fetchData(params.id);
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.description}</p>
    </div>
  );
}

추가적으로, 위 예시처럼 fetch API를 사용하여 데이터를 요청하고 결과를 캐싱함으로써
사용자가 한 번 생성된 페이지에 다시 접근했을 때는 완성된 HTML을 더욱 빠르게 불러올 수 있도록 했습니다.
 

Problems

그런데, 데이터 목록 중 하나를 클릭해 상세 페이지로 이동할 때 이상한 일이 발생했습니다.
한 번 접속한 페이지는 아주 빠르게 이동이 이루어졌지만, 최초로 접근하는 페이지는 이동 자체가 매우 느리게 이루어진 것이죠.

클릭 후, 한참을 기다려야 페이지 이동이 발생합니다!

즉 사용자가 클릭을 한 뒤, 페이지가 이동하고, 데이터를 불러올 때까지 로딩이 뜬 후, 다 불러오면 데이터가 보이는 게 아니라,
사용자가 클릭을 하면 한참 동안 아무런 반응이 없다가, 시간이 조금 흐른 뒤, 데이터가 다 불러와진 페이지로 이동이 발생했습니다.
 
이는 사용자 행동(클릭) 이후 애플리케이션의 상호작용이 즉각적으로 이루어지지 않는다는 점에서, 개선해야 할 UX라 생각했습니다.
 

Cause

왜 이런 일이 발생했을까요?

// app/[id]/page.tsx
import fetchData from '../lib/fetchData';

export default async function Page({ params }) {
  const data = await fetchData(params.id); // To Slow...
  // 해당 요청이 완료될 때까지, 페이지 생성 지연 => route 지연
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.description}</p>
    </div>
  );
}

원인은 상세 페이지에서 요청하는 api의 응답이 비교적 느렸기 때문이었습니다.
해당 api는 각 가이드 내 생성된 step 목록을 조회하는데, 이때 생성된 step 데이터의 크기가 크고 복잡했습니다.
 
서버 컴포넌트는 서버에서 데이터를 가져와 HTML을 생성하기 때문에
api 응답이 느린 경우 해당 api의 응답이 완료될 때까지 페이지 렌더링이 차단되고, 이로 인해 페이지 라우팅 역시 지연되고 만 것이죠!
 

Solution

하지만 특정 api의 응답이 느리다고 해서, 해당 페이지로의 라우팅까지 늦어질 이유는... 없습니다!
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#what-is-streaming

Routing: Loading UI and Streaming | Next.js

Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.

nextjs.org

이를 해결하기 위해 Next.js의 loading 파일을 활용했습니다.
 

loading UI

Next.js app 디렉터리에는 page, layout 등과 같이, loading이라는 사전 정의된 파일이 존재합니다.
 
해당 파일은  페이지가 로드되는 동안 로딩 UI를 표시하는 역할을 하며,
데이터를 불러오는 동안 사용자에게 스켈레톤 UI 또는 로딩 스피너와 같은 피드백을 제공해 줄 수 있는 것이죠!

// app/[id]/loading.tsx
export default function Loading() {
  return (
    <div className="skeleton">
      <p>Loading...</p>
    </div>
  );
}

해당 페이지 경로 내 loading.tsx 파일을 생성해 적용할 수 있습니다.
 

Streaming with Suspense

그렇다면, 이러한 동작이 어떻게 가능할까요?
 
Next.js는 React의 Suspense와 함께 스트리밍을 지원합니다.
이를 통해 서버에서 데이터를 가져오는 동안 클라이언트에서 UI의 일부를 먼저 렌더링 하고,
나머지 데이터가 준비되는 대로 점진적으로 UI를 업데이트할 수 있습니다.

React Suspense를 사용해 컴포넌트가 아직 준비되지 않은 상태에서 로딩 상태를 표시할 수 있습니다. (위 사진처럼요!)
즉 컴포넌트가 데이터를 가져오는 동안 보여줄 대체 UI를 제공할 수 있으며, 데이터가 준비되면 자동으로 UI가 업데이트됩니다.
 

Instant Loading States!

앞서 얘기한 loading 파일을 작성한 경우,
Next.js는 자동으로 page와 하위 컴포넌트들을 Suspense boundary로 감쌉니다.
→ 이를 통해 해당 서버 컴포넌트의 데이터 로딩이 완료되지 않은 상태에서, loading 파일에 정의된 UI를 띄워줄 수 있는 것이죠!
 

// app/[page]/loading.tsx
export default function Loading() {
  return (
    <div className="skeleton">
      <p>Loading...</p>
    </div>
  );
}

// app/[page]/page.tsx
import fetchData from '../lib/fetchData';

export default async function Page({ params }) {
  const data = await fetchData(params.id);
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.description}</p>
    </div>
  );
}

이를 통해 모든 데이터가 로드되어 UI가 렌더링 될 때까지 기다리지 않고도, 해당 페이지의 일부를 더 빨리 보여줄 수 있었습니다.

개선된 UX

즉 layout과 같이 데이터에 의존적이지 않은 컴포넌트들이 빠르게 반환되며
덕분에 React가 hydration을 일찍 시작할 수 있게 되며
렌더링 역시 데이터 로드가 완료될 때까지 차단되지 않는 것이죠!
 
이를 통해 사용자 클릭이 발생한 즉시 페이지 이동이 이루어지고,
정적인 구성 요소와 스켈레톤 UI를 빠르게 화면에 띄워 사용자 UX를 개선하는 결과를 만들어낼 수 있었습니다.

728x90
반응형