Suspense
Suspense는 코드에서 로딩 상태를 나타내는 부분을 제거할 수 있게 하는 API입니다. 코드에서 로딩 상태에 대해 신경쓰지 않아도 유저가 로딩 상태 화면을 볼 수 있는 겁니다.
하지만 이는 getStaticProps와 같은 곳에서는 사용할 수 없습니다. 로딩상태가 없기 때문이죠. 그래서 NextJS를 사용하지 않거나 getStaticProps, getStaticPaths를 사용하지 않을 때는 suspense를 많이 사용합니다.
기존의 profile페이지는 SSR을 사용했습니다. 그리고 getServerSideProps에서 iron session에 접근하는 방법을 보여주었습니다. 이번에는 SSR을 사용하지 않고 로딩 상태를 확인할 수 있게 만든 다음 SUspense를 사용해서 얼마나 Suspense가 좋은지 알아보고 싶기 때문입니다.
https://swr.vercel.app/ko/docs/suspense
일단 먼저 코드를 보겠습니다.
/pages/profile/index.tsx
import type { NextPage, NextPageContext } from "next";
import Layout from "@components/layout";
import Link from "next/link";
import useUser from "@libs/client/useUser";
import { Review, User } from "@prisma/client";
import useSWR, { SWRConfig } from "swr";
import { cls } from "@libs/client/utils";
import { withSsrSession } from "@libs/server/withSession";
import client from "@libs/client/client";
import { Suspense } from "react";
interface ReviewWithUser extends Review {
createdBy: User;
}
interface ReviewsResponse {
ok: boolean;
reviews: ReviewWithUser[];
}
const Reviews = () => {
const { data } = useSWR<ReviewsResponse>("/api/reviews");
return (
<>
{data?.reviews.map((review) => (
<div key={review.id} className="mt-12">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 rounded-full bg-slate-400" />
<div>
<h4 className="text-sm font-bold text-gray-800">
{review?.createdBy?.name}
</h4>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<svg
key={star}
className={cls(
"h-5 w-5",
review.score >= star
? "text-yellow-400"
: "text-gray-400"
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p>{review?.review}</p>
</div>
</div>
))}
</>
);
};
const MiniProfile = () => {
const { user } = useUser();
return (
<>
<div className="flex items-center space-x-3">
{user?.avatar ? (
<img
src={`https://imagedelivery.net/_SMYXsMOOEvTYhYAAKoRCQ/${user.avatar}/avatar`}
className="h-16 w-16 rounded-full bg-slate-500"
/>
) : (
<div className="h-16 w-16 rounded-full bg-slate-500" />
)}
<div className="flex flex-col">
<span className="font-medium text-gray-800">
{user?.name}
</span>
<Link href="/profile/edit">
<a className="font-sm text-gray-700">
Edit profile →
</a>
</Link>
</div>
</div>
</>
);
};
const Profile: NextPage = () => {
return (
<Layout title="나의 계정" hasTabBar>
<div className="py-3 px-4">
<Suspense fallback="Loading profiles...">
<MiniProfile />
</Suspense>
<div className="mt-10 flex justify-around">
<Link href="/profile/sold">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
판매내역
</span>
</a>
</Link>
<Link href="/profile/bought">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
구매내역
</span>
</a>
</Link>
<Link href="/profile/loved">
<a className="flex flex-col items-center">
<div className="aligned-center flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
관심목록
</span>
</a>
</Link>
</div>
<Suspense fallback="Loading reviews...">
<Reviews />
</Suspense>
</div>
</Layout>
);
};
const Page: NextPage = () => {
return (
<SWRConfig
value={{
// fallback: {
// "api/users/me": { ok: true, profile },
// },
suspense: true,
}}
>
<Profile />
</SWRConfig>
);
};
/* export const getServerSideProps = withSsrSession(
async function ({ req }: NextPageContext) {
const profile = await client.user.findUnique({
where: {
id: req?.session.user?.id,
},
});
// console.log("profile serverside >> ", profile);
return {
props: {
profile: JSON.parse(JSON.stringify(profile)),
},
};
}
); */
export default Page;
다음과 같이 일단 SWRConfig으로 suspense: true를 주어 SWR에서 Suspense를 사용할 수 있게 했습니다.
프로필 페이지에서 SWR로 데이터를 불러와서 CSR을 활용할 곳은, Profile정보부분, Reviews목록을 렌더링하는 부분이 됩니다. 그래서 이둘을 컴포넌트화 했습니다. 그리고 각각을 Profile컴포넌트에서 렌더링했습니다. 그리고 이를 HOC로 Suspense컴포넌트로 감싸주었고, fallback으로 렌더링되기 이전에 보여줄 것을 써줍니다.
그리고 suspense: true로 쓰면 Profile컴포넌트가 suspended컴포넌트가 됩니다. suspended란 리액트가 SWR의 로딩이 끝나기 전까지 컴포넌트 렌더링을 하지 않겠다는 뜻입니다. 그래서 무조건 개발자는 데이터가 없는 로딩 상태일 때 어떤걸 보여줄지 코드로 작성해 주어야 합니다.
SWR을 사용했을 때 SWR요청이 끝날 때까지 기다린 다음 컴포넌트를 렌더링해 줄 것입니다.
그리고 저는 SWR로부터 데이터를 불러오는데, 꼭 필요한 부분만 Fallback으로 설정하고, 네비게이션바는 보이도록 하고싶었습니다. 그리고 Page에서 useSWR을 사용하는건 말이 되지 않았습니다. 그래서 각 최소기능의 컴포넌트에서 SWR을 사용하면 됩니다.
그리고 코드를 보면 저는 더이상 로딩상티를 신경쓰지 않았습니다. 무조건 데이터가 확실히 있을거라 가정하고 리턴했기 때문이죠. 이러면 데이터가 있는지 없는지 체크할 필요가 없기 때문에 코드가 훨씬 깔끔해졌습니다.
주의해야합니다. 만약 페이지에서 SWR을 사용하면 suspense전에 전체 페이지가 보이지 않을겁니다.
Suspense는 개발자가 활성화시킨다고 되는게 아니라 라이브러리에서 해당 기능을 지원해 주어야 하는 특징이 있습니다. 그래서 SWR을 사용한다면 위와같이 suspense:true를 주면 되지만, react query를 사용하면 다를것입니다.
다음과 같이 일부러 reviews목록을 불러오는 api를 5초로 설정해 놓고 Suspense가 잘 작동하는지 확인했더니 정말 잘 작동합니다!
원리
서버사이드의 경우 서버에서 먼저. 전체 어플리케이션을 렌더링하기 때문에, 어느 한 컴포넌트가 로딩하는데 오래걸리면. 백엔드에서 전체 렌더링 하는 동안. 유저는 빈 화면을 볼 확률이 높다는 것은 이미 다들 아실 겁니다.
예를 들면. 해당 어플리케이션을 렌더링 하려고 한다고 해 봅시다. 유저는 Header컴포넌트를 미리 볼 수가 없습니다. Post컴포넌트 렌더링이 끝날 때 까지 말이죠.
이는 다방면으로 문제가 있을 수 있습니다.
이를 해결하기 위해, Suspense로 Post컴포넌트를 감싸봅시다.
위 코드에 대한 HTML파싱 결과는, 위와 같게 됩니다. 유저는 Header컴포넌트를 바로 볼 수 있게 됩었습니다! Post컴포넌트를 기다릴 필요가 없이 말이죠. 최고의 장점은 새로운 APi덕분에 Posts컴포넌트의 로딩이 끝날 때 HTTP Stream을 사용하여, 리액트는 Loader컴포넌트의 HTML을 Post컴포넌트의 결과 HTML로 아래와같이 대체한다는 것입니다.
브라우저에서 리액트가 로딩 되기도 전에 말이죠.
'Web > NextJs' 카테고리의 다른 글
[ Next.js - DeepDive ] - REACT18 - Suspense 2 (0) | 2022.08.13 |
---|---|
[ Next.js - DeepDive ] - REACT18 - Sever Components (0) | 2022.08.13 |
[ Next.js - DeepDive ] - Data Fetching Reca (0) | 2022.08.12 |
[ Next.js - DeepDive ] - Fallback (0) | 2022.08.12 |
[ Next.js - DeepDive ] - Blocking SSG (0) | 2022.08.12 |