Next.js - 스트리밍, 스켈레톤 UI

Stella·2026년 1월 5일

Next.js

목록 보기
5/8

!!한 입 크기로 잘라 먹는 Next.js 책 스터디 내용

스트리밍이란?

특정 UI요소를 스트리밍 방식으로 렌더링하는 기능을 제공, 렌더링 가능한 요소부터 먼저 표시하기 때문에 사용자를 필요 이상으로 오래 기다리지 않게 할 수 있다.

용량이 크거나 준비하는 데 오랜 시간이 걸리는 데이터를 보다 효과적으로 전송하기 위해 고안된 방식이다.
데이터를 여러 개로 나누어 조각 별로 실시간으로 전송한다.

웹 서비스의 스트리밍

오래 걸리는 부분과, 바로 준비되는 부분을 구분한다.
오래 걸리는 요소 -> 로딩 상태 ('로딩 중')으로 처리한다.

초기 로딩 시간을 줄인다. 필요한 정보를 빠르게 확인할 수 있다.

Next.js의 스트리밍 활용 사례

앱 라우터 버전은 다이나믹 페이지를 최적화하기 위해, 사용자 경험을 향상하기 위해 스트리밍을 사용한다.
브라우저의 접속 요청에 따라 실시간으로 서버에서 생성하므로 데이터를 불러오거나 UI요소를 렌더링하는 데 시간이 걸릴 수 있다.

다이나믹 페이지 + 스트리밍 기법
준비된 UI요소부터 사용자에게 표시, 나머지 요소는 준비되는 대로 페이지에 점진적으로 추가

페이지 컴포넌트를 제외하고 레이아웃 및 검색 폼 컴포넌트는 브라우저가 접속 요청 시 바로 렌더링 가능하다.
루트 layout, 검색 폼 layout, 검색 폼 component 이후에 페이지 컴포넌트가 렌더링 = 지연 발생

스트리밍 설정하기

src/app 폴더에 loading.tsx를 생성하면 이 파일의 경로 폴더를 포함해 아래 경로의 페이지는 모두 자동으로 스트리밍 할 수 있다.

src/util/delay.ts

export async function delay(ms: number){
    return new Promise((resolve) => setTimeout(resolve, ms));
} // 인위적으로 delay함수를 작성한다.

await delay(3000); 호출해서 컴포넌트의 실행을 3초간 지연한다.
스트리밍은 클라이언트의 접속 요청에 실시간으로 페이지를 생성해야 하는 다이나믹 페이지에 유용하다.

loading.tsx파일을 이용해 스트리밍을 설정할 때 주의할 점

- 페이지 컴포넌트만 점진적으로 렌더링할 수 있다.

loading.tsx를 이용해 스트리밍을 설정하면 페이지 컴포넌트만 점진적으로 렌더링
페이지 컴포넌트 이외의 컴포넌트(검색 폼 컴포넌트 등)에서 비동기 데이터 호출하더라도 이 컴포넌트를 점진적으로 렌더링할 수 없다.

= Suspense 컴포넌트를 사용하면 특정 컴포넌트 단위로 로딩 상태를 처리

- 쿼리 스트링 변경은 스트리밍을 다시 유발하지 않는다.

검색 페이지에서 새로운 검색어를 입력하면 스트리밍이 다시 동작 X
이미 검색 페이지에 접속한 상태라면 검색어를 변경하더라도 스트리밍이 동작하지 않아 페이지 컴포넌트의 로딩 UI가 다시 표시되지 않는다.

Next.js의 스트리밍은 초기 접속 페이지나 페이지를 이동할 때만 활성화되므로 쿼리 스트링 변경처럼 클라이언트의 상태 업데이트에는 반응하지 않는다. = 새 검색어를 입력해도 로딩 UI는 표시되지 않은 채 준비시간이 길어지면 사용자는 불만이 생길 수 있다.

= Suspense 컴포넌트를 사용하면 데이터로드가 다시 발생할 때 로딩 상태를 효과적으로 처리할 수 있다.

Suspense를 이용한 스트리밍 설정

특정 컴포넌트를 완전히 로드하기 전까지 페이지에 대체 UI를 표시하도록 한다.
이 컴포넌트를 사용하면 비동기 데이터나 외부 리소스를 불러오는 동안 '로딩 중'임을 나타내는 UI를 보여줄 수 있다.

1) 전체 페이지에 적용

<Suspense fallback=<div>로딩 중입니다.</div>
	<Child/> // child컴포넌트를 점진적으로 렌더링하도록 설정한다.
</Suspense>

Child컴포넌트가 완전히 로드되기 전까지 대체 UI로 화면에 표시된다.
앱에서 페이지뿐만 아니라 다른 컴포넌트도 점진적으로 렌더링할 수 있어 로딩 UI를 세밀하게 구현할 수 있다.

2) 인덱스 페이지 -> 다이나믹 페이지로 설정, 일부 컴포넌트에는 스트리밍 기능

  1. export const dynamic = "force-dynamic";

  2. 인덱스 페이지에서 AllBooks와 RecoBooks 컴포넌트를
    Suspense로 감싸 점진적으로 렌더링하도록 설정한다.

export default function Page() {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        <Suspense fallback={<div>로딩 중 입니다...</div>}>
          <RecoBooks />
        </Suspense>
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        <Suspense fallback={<div>로딩 중 입니다...</div>}>
          <AllBooks />
        </Suspense>
      </section>
    </div>
  );
}

컴포넌트 단위로 점진적인 렌더링 설정이 가능해 스트리밍을 정교하게 구현할 수 있다.

  1. 검색 페이지에서 Suspense 컴포넌트를 사용하도록 변경
async function SearchResult({q}: {q: String}){
    await delay(3000);
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_URL}/book/search?q=${q}`, // 검색어 q가 달라지면 새로운 캐시 키를 생성하므로 이 요청은 별도의 데이터로 처리된다.
      { cache: "force-cache"},
    );
    if (!response.ok) throw new Error(response.statusText);

    const books: BookData[] = await response.json();

    return (
      <div>
        {books.map((book) => (
          <BookItem key={book.id} {...book} />
        ))}
      </div>
    );
}

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{
    q?: string;
  }>;
}) {
  const { q } = await searchParams;

  return (
    <Suspense fallback={<div>검색 결과를 불러오는 중입니다...</div>}>
      <SearchResult q={q || ""} />
    </Suspense>
  )
}

SearchResult 컴포넌트를 만든다. 비동기 동작을 하는 컴포넌트를 감싸기 위해
로딩 UI를 불러오도록 텍스트를 렌더링한다.
쿼리 스트링을 변경하면 로딩 UI가 표시되지 않는다. = 기본적으로 페이지 이동과 초기 페이지의 접속에서만 동작하기 때문이다.

- Suspense 컴포넌트의 key Prop으로 key값 적용

key Prop은 리액트에서 특정 컴포넌트를 식별하기 위한 식별자
쿼리 스트링을 변경할 때마다 key값이 달라지므로 컴포넌트를 강제로 다시 렌더링

<Suspense key={q || ""} fallback={<div>검색 결과를 불러오는 중입니다...</div>}>

= 쿼리 스트링이 변경될 때마다 기존의 Suspense를 제거하고 새로 생성하는 과정에서 로딩 UI를 다시 표시

스트리밍과 검색 엔진 최적화(SEO)

검색 엔진 최적화에 영향을 미치지 않는다. 검색 엔진 크롤러의 동작에서 찾을 수 있다.
점진적으로 수신된 데이터는 처리하지 않는다. 크롤러는 서버에서 모든 HTML 데이터 수신할 때까지 대기 -> 한꺼번에 처리

스켈레톤 UI

콘텐츠를 점진적으로 렌더링하는 동안 사용하는 스켈레톤UI 렌더링할 콘텐츠의 최종 모습을 간략히 보여 주는 로딩 UI를 말한다. = 적극적 활용

  • 스켈레톤 UI 구현하기
    도서 아이템의 스켈레톤 컴포넌트를 생성,
import style from "./book-item-skeleton.module.css";
export default function BookItemSkeleton() {
  return (
    <div className={style.container}>
      <div className={style.cover_img}></div>
        <div className={style.info_container}>
          <div className={style.title}></div>
          <br />
            <div className={style.subtitle}></div>
            </div>
          </div>
  );
}
  • BookItemSkeleton 컴포넌트 불러오기
export default function Page() {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        {/* <Suspense fallback={<div>로딩 중 입니다...</div>}> */}
        <Suspense fallback={new Array(3).fill(0).map((_, idx) => ( // 도서 아이템 스켈레톤 컴포넌트 3개 불러오도록 설정
          <BookItemSkeleton key={`reco-book-skeleton-${idx}`} />
        ))}>
          <RecoBooks />
        </Suspense>
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        <Suspense fallback={new Array(5).fill(0).map((_, idx) => (
          <BookItemSkeleton key={`all-book-skeleton-${idx}`} />
        ))}> 
          <AllBooks />
        </Suspense>
        {/* 5개 렌더링하도록 설정한다. */}
      </section>
    </div>
  );
}

= 확인을 마쳤다면 delay함수를 모두 제거하기 (테스트 용도)

profile
공부 기록

0개의 댓글