



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 함수를 집어넣어주면 된다.

개발 중 혹은 서비스 중에 에러가 발생하는 일은 당연한 것이다.
그런데 모든 에러마다 서버가 중단되고, 개발자가 에러를 확인하고 서버를 재가동해주는 것은 다소 비효율적인 일이다.
-> 가령 순간적인 네트워크 에러로 데이터 전송에 문제가 생겼다고 하면, 그냥 새로고침만 해주면 되는데 굳이 서버가 멈춰야하는 이유가 없다.
그 자리에서 해결할 수 있는 수준의 에러라면 그냥 사용자에게 문제가 생겼다는 사실만 보여주고, 뒤로가기나 새로고침 등으로 문제를 해결시키는게 더 낫다는 것.
따라서 에러가 발생했을 때, 상황별 에러를 구분해서 적절하게 처리하는 에러 핸들링 작업의 중요성은 대단히 높다.
Next.js에서는 특정 경로에서 발생하는 에러들을 한 곳에서 모두 총괄할 수 있게 설계되어 있다.


Streaming 기능 적용을 위해 Loading 파일을 생성했듯이, error 파일을 생성해주면 Next.js의 에러 핸들링 기능이 자동으로 가동된다.
error 페이지는 반드시 클라이언트 컴포넌트로 설정해주어야 한다. 에러는 서버에서만 발생하는 게 아니기 때문.

해당 경로 상에 위치한 페이지들에서 에러가 발생하면, 동일 경로 및 하위 경로에 위치하는 error 페이지로 제어권이 넘어가게 된다.
상위 경로와 동일 경로에 모두 error 페이지가 존재하면, 동일 경로의 error 페이지가 우선권을 갖는다.
위 스크린샷의 상황에서 설명하자면, 최상위 경로에 error가 있고 search 내부에 error가 있다.
search 페이지는 자신과 같은 경로의 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()가 한몸처럼 처리되어 작업 순서 문제도 잘 해결된다.
