학습 Next.js - Day 19 / 스켈레톤 UI, 에러 핸들링

이유승·2024년 10월 11일

Next.js 학습

목록 보기
20/27



1. 스켈레톤 UI

  • 실제 컨텐츠가 로딩되기 전까지, 사용자에게 콘텐츠가 로딩 중임을 명시적으로 보여준다. 추가로 어떤 형태의 컨텐츠가 로딩 중인지도 함께 보여줄 수 있다.

  • 실제 컨텐츠가 이렇게 렌더링 된다면..

  • 컨텐츠가 로딩 중일 때는 이런 형태의 스켈레톤 UI을 렌더링 해주면 된다.



동일한 스켈레톤 UI을 여러 개 사용하려면

  • 복수의 컨텐츠가 로딩되는 상황에서는 당연히 스켈레톤 UI도 여러 개 출력되어야 한다. 별도의 함수를 생성하여 이를 처리할 수 있다.
import BookItemSkeleton from "./book-item-skeleton";

export default function BookListSkeleton({
  count,
}: {
  count: number;
}) {
  return new Array(count)
    .fill(0)
    .map((_, idx) => (
      <BookItemSkeleton key={`book-item-skeleton-${idx}`} />
    ));
}
  • 몇 개의 UI을 렌더링 해야하는지 기준값을 받는다. count 인자.

  • count만큼의 공간을 갖는 배열을 생성한다.

  • 생성된 배열 내부에는 초기화를 위해 0이라는 요소들을 집어넣도록 한다.

  • map 메소드를 이용하여 각 배열에 BookItemSkeleton UI 컴포넌트를 삽입해준다.



완성된 스켈레톤을 학습 프로젝트에서 적용하기

export default function Home() {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        <Suspense fallback={<BookListSkeleton count={3} />}>
          <RecoBooks />
        </Suspense>
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        <Suspense fallback={<BookListSkeleton count={10} />}>
          <AllBooks />
        </Suspense>
      </section>
    </div>
  );
}
  • Streaming와 동시에 사용한다고 하면, Suspense 컴포넌트의 fallback 옵션으로 스켈레톤 UI 컴포넌트를 넣어주면 된다.

  • 학습 프로젝트에서는 스켈레톤 UI가 필요한 갯수만큼 적용되도록, 앞서 생성해둔 BookListSkeleton 함수를 집어넣어주면 된다.



사실 라이브러리 사용이 더 쉽다..

  • UI 구현, 관련 로직 구현 등을 하나하나 수행하는 것은 역시 번거롭다. 이미 완성된 스켈레톤 라이브러리를 사용하는 것도 좋은 방법.



2. 에러 핸들링

  • 개발 중 혹은 서비스 중에 에러가 발생하는 일은 당연한 것이다.

  • 그런데 모든 에러마다 서버가 중단되고, 개발자가 에러를 확인하고 서버를 재가동해주는 것은 다소 비효율적인 일이다.
    -> 가령 순간적인 네트워크 에러로 데이터 전송에 문제가 생겼다고 하면, 그냥 새로고침만 해주면 되는데 굳이 서버가 멈춰야하는 이유가 없다.

  • 그 자리에서 해결할 수 있는 수준의 에러라면 그냥 사용자에게 문제가 생겼다는 사실만 보여주고, 뒤로가기나 새로고침 등으로 문제를 해결시키는게 더 낫다는 것.

  • 따라서 에러가 발생했을 때, 상황별 에러를 구분해서 적절하게 처리하는 에러 핸들링 작업의 중요성은 대단히 높다.

  • Next.js에서는 특정 경로에서 발생하는 에러들을 한 곳에서 모두 총괄할 수 있게 설계되어 있다.

  • Streaming 기능 적용을 위해 Loading 파일을 생성했듯이, error 파일을 생성해주면 Next.js의 에러 핸들링 기능이 자동으로 가동된다.

  • error 페이지는 반드시 클라이언트 컴포넌트로 설정해주어야 한다. 에러는 서버에서만 발생하는 게 아니기 때문.

  • 해당 경로 상에 위치한 페이지들에서 에러가 발생하면, 동일 경로 및 하위 경로에 위치하는 error 페이지로 제어권이 넘어가게 된다.

  • 상위 경로와 동일 경로에 모두 error 페이지가 존재하면, 동일 경로의 error 페이지가 우선권을 갖는다.

  • 위 스크린샷의 상황에서 설명하자면, 최상위 경로에 error가 있고 search 내부에 error가 있다.

  • search 페이지는 자신과 같은 경로의 error 페이지가 적용되고, 나머지 파일들은 상위 경로의 error 페이지가 적용된다는 말.



error 페이지의 기본 구조

"use client";

import { useRouter } from "next/navigation";
import { startTransition, useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  const router = useRouter();

  useEffect(() => {
    console.error(error.message);
  }, [error]);

  return (
    <div>
      <h3>오류가 발생했습니다</h3>
      <button
        onClick={() => {
          // 함수 하나를 인수로받아서, 해당 함수 내부의 코드를 동기적으로 실행
          startTransition(() => {
            router.refresh(); // 현재 페이지에 필요한 서버컴포넌트들을 다시 불러옴
            reset(); // 에러 상태를 초기화, 컴포넌트들을 다시 렌더링
          });
        }}
      >
        다시 시도
      </button>
    </div>
  );
}
  • 외부에서 에러 정보를 받아오는 Error 타입의 인자 'error'.
    -> Error 데이터 내부에는 message 등의 세부 정보들이 포함되어 있다.

  • 그리고 reset 함수. 에러가 발생한 페이지의 복구를 위해, 컴포넌트를 다시 렌더링 시키는 역할을 수행한다. 반환값은 void.

  • 다만 reset은 클라이언트측에서 리렌더링을 시도하는 역할만 수행한다. 데이터 페칭 함수 등을 재실행하지 않기 때문에 서버단에서 에러가 발생한 경우에는, reset 함수로 해결할 수가 없다.

  • 가장 쉬운 방법은, window.location.reload() 메소드를 이용해서 브라우저를 강제로 새로고침 시키는 것.

  • 그런데 강제 새로고침은, 브라우저에 저장되어 있던 데이터가 초기화되고 재실행 할 필요가 없는 다른 레이아웃이나 컴포넌트까지 모조리 재실행해버리는 문제가 있다.

  • router.refresh()을 이용하게 되면, Next 서버에 서버 컴포넌트만 재실행하게 만들 수 있다. reset()와 같이 사용하게 되면..
    -> 서버 컴포넌트가 재실행된다.
    -> 클라이언트에서 리렌더링이 발생한다.
    => 라는 수순으로 서버와 클라이언트 양측에서 재실행이 이루어지면서 문제가 해결된다.

  • 다만, 이 경우에는 반드시 router.refresh()와 reset()가 함께 실행되야 한다. refresh()만 단독으로 실행되면 클라이언트측의 에러 상태가 초기화 되지 않고, reset()만 단독으로 실행되면 서버에서 새로운 값이 전달되지 않아 에러를 해결할 수 없기 때문.

  • 또한, router.refresh()가 먼저 실행되어 결과값이 클라이언트로 전달된 뒤에 reset()가 실행되어야 한다. 두 작업이 비동기적으로 실행되어버리면 서버 컴포넌트의 재실행 결과가 클라이언트로 재때 전달되지 않기 때문.
    -> 이럴 때 흔히 사용되는 async-await는 사용할 수 없다. router.refresh()는 반환값이 비동기 함수가 아니라 void이기 때문.

  • React 18버전부터는 이런 상황에서 사용가능한 startTransition 함수를 제공하고 있다.

  • 콜백 함수를 인자로 받아, 내부의 UI 변경 작업들을 일괄적으로 처리해준다. router.refresh()와 reset()가 한몸처럼 처리되어 작업 순서 문제도 잘 해결된다.









00. 강의 소개.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글