NextJS - 스트리밍과 에러핸들링

김명원·2025년 3월 20일

learnNextjs

목록 보기
21/24

스트리밍과 에러핸들링

스트리밍이란?

서버에서 클라이언트로 어떠한 데이터를 넘겨줘야 될 때 보내줘야 되는 데이터의 크기나 너무나도 크거나 또는 서버 측에서 데이터를 준비하는데 걸리는 시간이 오래 걸리게 되면
데이터를 여러 개의 조각으로 쪼개서 쪼개진 작은 용량의 데이터들을 하나하나씩 클라이언트에게 전송하는 기술을 스트리밍이라고 합니다.

사용자는 모든 데이터가 불러와 지지 않은 상태에서도 지금까지 전달받은 데이터에 접근할 수가 있게 되기 때문에 사용자에게 긴 로딩 없이 좋은 경험 서비스를 제공하게 됩니다.

Next는 동영상 뿐만아니라 웹 서비스에서도 적용할 수 있게 끔 HTML 페이지를 스트리밍하는 기능을 자체적으로 제공합니다.
Next.js는 단순한 컴포넌트들부터 화면에 보여주고

그리고 데이터 패칭 등의 이유로 렌더링이 오래 걸릴 것으로 예상되는 컴포넌트들은 로딩바 같은 대체 UI를 화면에 보여주고 있다가

서버 측에서 컴포넌트의 렌더링이 완료가 되면 그때 컴포넌트들을 전달해서 콘텐츠를 보여주는 방식으로 페이지를 제공합니다.

이렇게 되면 사용자들에게 뭐라도 빨리 보여주게 되면서 사용자의 경험을 좋게 만듭니다.

이러한 Next.js의 스티리밍은 Dynamic Page에 자주 사용됩니다. 다이나믹 페이지들을 빌드 타임에 생성이 되지 않기 때문에 요청이 들어오면 모든 컴포넌트들을 다 실행해서 페이지를 매번 새롭게 렌더링을 실행시켜줘야 하기 때문에 만약 이러한 상황이 오래 걸리면 사용자의 경험이 낮아지기 때문에 Dynamic Page에 자주 활용되는 것입니다.

페이지 스트리밍 적용시키기

Dynamic Page에 스트리밍을 적용시키기 위해서 Dynamic.page와 동일한 위치에 새로운 파일로 예를들어 loading.tsx와 같은 새로운 파일을 만들어줍니다.
그러면 자동으로 스트리밍으로 대체가 되고 로딩 중일때 loading.tsx가 로딩화면을 대체하게 됩니다.

주의점이 존재합니다.
loading.tsx 파일은 사실 동일한 경로에 있는 페이지 컴포넌트만 스트리밍 되도록 설정하는 게 아니라 레이아웃 파일처럼 해당하는 경로 아래에 있는 모든 페이지 컴포넌트들이 다 스트리밍이 되도록 설정이 됩니다.

또한 loading.tsx 파일이 스트리밍하도록 설정하는 페이지 컴포넌트는 모든 페이지 컴포넌트가 아니고 async라는 키워드가 붙어서 비동기로 작동하도록 설정된 페이지 컴포넌트에만 스트리밍이 작동하게 됩니다.

세번째로 loading.tsx라는 파일은 무조건 page 컴포넌트에만 스트리밍을 적용할 수 있다는 점입니다. 컴포넌트들이나 레이아웃과 같은 일반적인 컴포넌트들에는 loading.tsx 파일로는 스트리밍을 설정할 수 없습니다.
만약 별도의 컴포넌트들에도 스트리밍을 적용하고 싶다면, React의 Suspense라는 컴포넌트를 별도로 활용을 해야합니다.

마지막으로 loading.tsx 파일로 설정된 스트리밍은 브라우저에서 쿼리 스트링이 변경될 때에는 트리거링이 되지 않습니다.
만약 이런 경우에도 스트리밍이 똑같이 적용되기를 원하면, React의 Suspense 컴포넌트를 이용해야 합니다.

컴포넌트 스트리밍 적용시키기

위에 방식으로는 일반적인 컴포넌트들에 스트리밍을 따로 설정할 수 가 없습니다.
React Suspense 컴포넌트를 이용해서 컴포넌트 별로 세밀하게 스트리밍을 적용하는 방법을 살펴보고자 합니다.

먼저 상단에 Suspense를 react로 부터 불러와줍니다.

import { Suspense } from "react";

다음 프로젝트를 감싸주면 됩니다. 예를들어

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{
    q?: string;
  }>;
}) {
  return (
    <Suspense>
      <SearchResult q={(await searchParams).q || ""} />
    </Suspense>
  );
}

이때 Loading 상태를 표시하는 대체 UI는 Suspense 컴포넌트에 fallback이라는 props로 전달해주면 됩니다.

<Suspense fallback={<div>Loading...</div>}>
  <SearchResult q={(await searchParams).q || ""} />
</Suspense>

컴포넌트로 전달을 해주어도 똑같이 동작을 하게 됩니다.

만약 쿼리스트링이 바뀔때마다 Loading 상태를 나타내고 싶다면 Suspense에 key 값이 변화할 때마다 다시 로딩 상태로 돌아가도록 설정을 해주면 됩니다.

<Suspense
  key={(await searchParams).q || ""}
  fallback={<div>Loading...</div>}
>	
  <SearchResult q={(await searchParams).q || ""} />
</Suspense>
);

대부분 loading.tsx 방식을 이용하는 것보다 Suspense 컴포넌트를 이용하는 방식이 더 선호가 더 많은 곳에 범용적으로 활용할 수 있습니다.

스켈레톤 UI 적용하기

스켈레톤 UI는 뜻 그대로 뼈대를 나타내는 UI입니다.
일부 컨텐츠가 로딩이 되는 동안 실제 컨텐츠 대신에 비슷하게 생긴 박스 형태의 실루엣 정도만 미리 보여주는 유튜브 예가 있습니다.

사용자에게는 컨텐츠가 끝났을 때 나의 눈 앞에 나타나게 될 것인지 예상할 수 있도록 만들어주기 때문에 더 좋은 사용자 경험을 줄 수 있습니다.

또한 스켈레톤 UI를 만들어서 스트리밍 UI로 만들어주면 더욱더 완성도 높은 프로젝트를 구현할 수 있습니다.

또한 React Loading Skeleton 이라는 자동으로 스켈레톤 UI를 만들어주는 라이브러리도 존재합니다.

에러 핸들링

보통 에러를 처리하기 위해 try문을 이용해 catch로 에러를 잡고 그 에러를 뛰어주는 방법으로 오류텍스트가 나타나게 했습니다.

Next는 특정 결로에서 발생한 모든 오류들을 한꺼번에 처리할 수 있는 편리한 에러 핸들링 기능을 하나 제공합니다.
그 방법은 에러를 처리할 파일을 하나 만들어주면 됩니다.
error.tsx 파일로
예시로

"use client";

export default function Error() {
  return (
    <div>
      <h3>오류가 발생했습니다.</h3>
    </div>
  );
}

다음 최상단에 'use clinet'를 입력해 클라이언트 컴포넌트로 설정까지 해주면 됩니다.
기본적으로 오류 라는 것은 서버든지 클라이언트든지 어떤 환경에서든 다 발생할 수 있기 때문에 모두 다 함께 대응할 수 있도록 클라이언트 컴포넌트로 설정을 하는 것입니다.

그렇게 되면 error.tsx 파일에 같은 경로에 있거나 또는 하위 경로에 있는 page에서 오류가 발생하게 되면 에러 컴포넌트가 page 컴포넌트 대신에 출력이 됩니다.
마치 대체 UI를 설정하는 것과 비슷한 개념입니다.

이때 에러 메세지를 출력하고 싶다면 props로 error 메세지를 정해주면 됩니다.

"use client";

import { useEffect } from "react";

export default function Error({ error }: { error: Error }) {
  useEffect(() => {
    console.error(error);
  }, [error]);
  return (
    <div>
      <h3>오류가 발생했습니다.</h3>
    </div>
  );
}

만약에 에러 페이지에서 다시 시도 같은 버튼을 만들어서 reset을 시키고 싶다면 그 또한 가능합니다 error에 2번째 인자로 reset을 받고 button을 만들어서 onClick 이벤트로 전달해주면 됩니다.

"use client";

import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error.message);
  }, [error]);
  return (
    <div>
      <h3>오류가 발생했습니다.</h3>
      <button onClick={() => reset()}>다시 시도</button>
    </div>
  );
}

하지만 이 reset 버튼가지고 데이터 패칭을 다시 수행하지 않기 때문에 모든 오류를 복구할 수는 없습니다.
대신에 클라이언트 컴포넌트 내부에서 발생한 오류만 복구할 수 있습니다.

이럴때에는 브라우저를 강제로 새로고침 시켜버리는 방법이 있습니다.

<div>
  <h3>오류가 발생했습니다.</h3>
  <button onClick={() => window.location.reload()}>다시 시도</button>	
</div>

하지만 이 방법은 데이터들이 다 날라가고 에러가 발생하지 않은 레이아웃이나 또 다른 별도의 컴포넌트들까지 완전히 새롭게 다시 렌더링 해야 하기 때문에 추천드리는 방법은 아닙니다.

다음 방법은 useRoute 객체를 불러와서 onClick 이벤트가 실행 되었을 때, 라우터 객체에 refresh 메서드를 호출해서 Next.js 서버에게 서버 컴포넌트만 새롭게 렌더링 해달라고 하고 props로 받은 reset 함수를 그 뒤에 호출해서 위에 새롭게 받은 서버 데이터를 화면에 새롭게 다시 렌더링 하는 것입니다.

<div>
  <h3>오류가 발생했습니다.</h3>
  <button
    onClick={() => {
      router.refresh();
      reset();
    }}
    >
    다시 시도
  </button>
</div>

refresh 메서드를 통해 현재 페이지에 필요한 서버 컴포넌트들을 다시 볼러와도 client 컴포넌트에 에러 컴포넌트가 초기화 되지 않기 때문에
그 이후 에러 상태를 초기화 시켜주는 reset 메서드를 실행시켜줘야 서버 컴포넌트의 결과 값도 다시 계산하고 에러 상태도 다시 초기화하는 동작이 일어나 페이지를 복구 시킬 수 있습니다.

하지만 refresh 메서드는 비동기 메서드이기 때문에 오류가 다시 발생합니다. ㅋㅋ 이 문제를 해결하기위해 Next 18 버전에서는 startTransition 메서드를 제공합니다. 비어있는 콜백함수로 그대로 안에 있는 내용을 전하면

<div>
  <h3>오류가 발생했습니다.</h3>
  <button
    onClick={() => {
      startTransition(() => {
        router.refresh();
        reset();
      });
    }}
    >
    다시 시도
  </button>
</div>

이제 오류를 수정할 수 있는 버튼이 완성이 됩니다.

profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글