Skip to content

Commit

Permalink
#21.0 Suspense
Browse files Browse the repository at this point in the history
## 목표

- 이번 색션에서는 React 18버전에서 새롭게 지원되는 기능들에 대해서 알아본다.
    - NextJS 에서도 이러한 React의 기능들을 지원한다.
    - 프레임워크만 다룰 줄 알고 라이브러리는 다룰 줄 모르면 안되기 때문에 둘 다 알아본다.
- 이번 강의에서는 이미 안정되어 사용가능한 기능인 **Suspense** 에 대하여 알아본다.
    - 주의 :
        - 이 페이지는 **다소 오래되었으며** 역사적인 목적으로만 존재합니다.
        - React 18은 동시성을 지원하면서 출시되었습니다. 그러나 **더 이상 "모드"가 없으며** 새로운 동작은 완전히 선택되어 있으며 [새 기능을 사용할 때만](https://reactjs.org/blog/2022/03/29/react-v18.html#gradually-adopting-concurrent-features) 활성화됩니다 .
    - **Suspense** 기능을 실습해 보기 위해서 캐럿마켓 프로젝트의 `pages/profile/index.tsx` 파일에서 기존에 `getServerSideProps` 로 구현 되었던 부분을 주석처리 하고 유저 정보를 로딩 할 수 있는 환경을 만들어서 실습 하여 본다.

## **Suspense 란?**

- 코드에서 로딩 상태를 나타내는 부분을 제거 할 수 있게 해주는 API
    - 코드에서 로딩 상태에 대해 신경쓰지 않아도 유저가 로딩 화면을 볼 수 있다.
- **Suspense** 는 다음 것들과 함께 사용 할 수 없다.
    - `getServerSideProps`
    - `getStaticProps`
    - `getStaticPaths`

    → 위 기능들을 사용하면, 클라이언트 단에서 따로 로딩을 하지 않기 때문..

## 정리

- **Suspense** 의 장점 중 하나는
    - **Suspense** 로 감싼 컴포넌트에서 받아오는 API 데이터는 이미 로딩이 성공했다는 가정 하에 사용 할 수 있다.

        → 렌더링 코드 부분에서 API 데이터 유효성 확인 및 제외처리를 할 필요가 없다.

- 페이지에서 SWR을 사용하면, **Suspense** 는 SWR 로딩이 끝날때까지 페이지 컴포넌트 전체를 표시하지 않는다.
    - 따라서 로딩 데이터 부분만 로딩 UI을 표시하고 싶다면, 해당 부분만을 컴포넌트로 추출하여 **Suspense** 로 감싸준다.
- **Suspense** 를 활성화 시키는 방법은 라이브러리마다 다르다.
    - **Suspense** 는 개발자가 활성화 시킨다고 되는 것이 아니라, 라이브러리에서 해당 기능을 지원해줘야 하기 때문
    - 예 : SWR와 React Query 의 **Suspense** 를 활성화 시키는 방법은 각각 다르다.

## 참고

### Suspense

[Suspense for Data Fetching (Experimental) – React](https://17.reactjs.org/docs/concurrent-mode-suspense.html)

- Suspense를 사용하면 컴포넌트가 렌더링되기 전까지 기다릴 수 있습니다.
- React 16.6 버전에서는 코드를 불러오는 동안 “기다릴 수 있고”, 기다리는 동안 로딩 상태를 지정할 수 있도록 `< Suspense >` 컴포넌트가 추가되었습니다.
- Suspense는 단순히 데이터 로딩뿐만 아니라 이미지, 스크립트, 비동기 작업을 기다리는 데에도 사용될 수 있습니다.

```tsx
// ProfilePage를 불러오는 동안 Loading를 표시합니다
<Suspense fallback={< Loading / >}>
	<ProfilePage />
</Suspense>
```

### 완전 새로운 리액트가 온다? 핵심정리 10분컷.

https://www.youtube.com/watch?v=7mkQi0TlJQo

### Error: Fallback data is required when using suspense in SSR

> swr의 공식문서(https://swr.vercel.app/ko/docs/suspense)에 따르면, swr의 suspense모드는 next js에서 페이지를 서버사이드 렌더링(이 경우에는 pre-render)할 때 문제가 발생하는 것 같습니다.
>
- 해결법은 크게 두가지
    - 하나는 swrconfig에 fallback 데이터를 미리 제공하는 것

        → 하지만, 이 방법을 사용하면 suspense를 사용하는 의미가 없어집니다. 왜냐하면, suspense의 목적은 클라이언트단에서 실제 데이터를 받은 이후에만 렌더링하는 건데, fallback은 서버사이드에서 데이터를 패치하여 넣어줘야 하기 때문입니다.

        - (서버사이드에서 데이터를 받으면, 클라이언트사이드에서 데이터는 이미 존재하니 suspense의 필요성이 없어짐.)
    - 다른 방법은 next/dynamic을 사용해서 아래처럼 서버사이드 렌더링을 꺼주는 겁니다.
        - `export default dynamic(async () => Page, { ssr: false });`
- 참고 문서 : vercel/swr#1906
  • Loading branch information
clean-teach committed Feb 2, 2024
1 parent 08ffedb commit fc67afd
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 59 deletions.
3 changes: 3 additions & 0 deletions pages/api/reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ async function handler(
const {
session: { user },
} = req;

await new Promise((resolve) => setTimeout(resolve, 5000));

const reviews = await client.review.findMany({
where: {
createdForId: user?.id,
Expand Down
269 changes: 210 additions & 59 deletions pages/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Review, User } from '@prisma/client';
import { cls } from '@libs/client/utils';
import { withSsrSession } from '@libs/server/withSession';
import client from '@libs/server/client';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';

interface ReviewWithUser extends Review {
createdBy: User;
Expand All @@ -17,26 +19,69 @@ interface ReviewsResponse {
reviews: ReviewWithUser[];
}

const Profile: NextPage = () => {
const { user } = useUser();
const Reviews = () => {
const { data } = useSWR<ReviewsResponse>('/api/reviews');

return <>
{data?.reviews.map((review) => (
<div key={review.id} className="mt-12">
<div className="flex space-x-4 items-center">
<div className="w-12 h-12 rounded-full bg-slate-500" />
<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-gray-600 text-sm">
<p>
{review.review}
</p>
</div>
</div>
)
)}
</>
}

const MainProfile = () => {
const { user } = useUser();

return <div className="flex items-center mt-4 space-x-3">
{user?.avatar ? (
<img src={'https://imagedelivery.net/aSbksvJjax-AUC7qVnaC4A/${user?.avatar}/avatar'} className="w-16 h-16 bg-slate-500 rounded-full" />
) : (
<div className="w-16 h-16 bg-slate-500 rounded-full" />
)}
<div className="flex flex-col">
<span className="font-medium text-gray-900">{user?.name}</span>
<Link href="/profile/edit">
<a className="text-sm text-gray-700">Edit profile &rarr;</a>
</Link>
</div>
</div>
};

const Profile: NextPage = () => {
return (
<Layout hasTabBar title="나의 캐럿">
<div className="px-4">
<div className="flex items-center mt-4 space-x-3">
{user?.avatar ? (
<img src={'https://imagedelivery.net/aSbksvJjax-AUC7qVnaC4A/${user?.avatar}/avatar'} className="w-16 h-16 bg-slate-500 rounded-full" />
) : (
<div className="w-16 h-16 bg-slate-500 rounded-full" />
)}
<div className="flex flex-col">
<span className="font-medium text-gray-900">{user?.name}</span>
<Link href="/profile/edit">
<a className="text-sm text-gray-700">Edit profile &rarr;</a>
</Link>
</div>
</div>
<Suspense fallback="Loading Main Profile ...">
< MainProfile />
</Suspense>
<div className="mt-10 flex justify-around">
<Link href="/profile/sold">
<a className="flex flex-col items-center">
Expand Down Expand Up @@ -108,59 +153,165 @@ const Profile: NextPage = () => {
</a>
</Link>
</div>
{data?.reviews.map((review) => (
<div key={review.id} className="mt-12">
<div className="flex space-x-4 items-center">
<div className="w-12 h-12 rounded-full bg-slate-500" />
<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-gray-600 text-sm">
<p>
{review.review}
</p>
</div>
</div>
)
)}
<Suspense fallback="Loading Review...">
<Reviews />
</Suspense>
</div>
</Layout>
);
};

const Page: NextPage<{ profile: User }> = ({ profile }) => {
const Page: NextPage = () => {
return (
<SWRConfig value={{ fallback: { '/api/users/me': { of: true, profile } } }}>
<SWRConfig value={{ suspense: true }}>
<Profile />
</SWRConfig>
)
};
// const Profile: NextPage = () => {
// const { user } = useUser();
// const { data } = useSWR<ReviewsResponse>('/api/reviews');

// return (
// <Layout hasTabBar title="나의 캐럿">
// <div className="px-4">
// <div className="flex items-center mt-4 space-x-3">
// {user?.avatar ? (
// <img src={'https://imagedelivery.net/aSbksvJjax-AUC7qVnaC4A/${user?.avatar}/avatar'} className="w-16 h-16 bg-slate-500 rounded-full" />
// ) : (
// <div className="w-16 h-16 bg-slate-500 rounded-full" />
// )}
// <div className="flex flex-col">
// <span className="font-medium text-gray-900">{user?.name}</span>
// <Link href="/profile/edit">
// <a className="text-sm text-gray-700">Edit profile &rarr;</a>
// </Link>
// </div>
// </div>
// <div className="mt-10 flex justify-around">
// <Link href="/profile/sold">
// <a className="flex flex-col items-center">
// <div className="w-14 h-14 text-white bg-orange-400 rounded-full flex items-center justify-center">
// <svg
// className="w-6 h-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="text-sm mt-2 font-medium text-gray-700">
// 판매내역
// </span>
// </a>
// </Link>
// <Link href="/profile/bought">
// <a className="flex flex-col items-center">
// <div className="w-14 h-14 text-white bg-orange-400 rounded-full flex items-center justify-center">
// <svg
// className="w-6 h-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="text-sm mt-2 font-medium text-gray-700">
// 구매내역
// </span>
// </a>
// </Link>
// <Link href="/profile/loved">
// <a className="flex flex-col items-center">
// <div className="w-14 h-14 text-white bg-orange-400 rounded-full flex items-center justify-center">
// <svg
// className="w-6 h-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="text-sm mt-2 font-medium text-gray-700">
// 관심목록
// </span>
// </a>
// </Link>
// </div>
// {data?.reviews.map((review) => (
// <div key={review.id} className="mt-12">
// <div className="flex space-x-4 items-center">
// <div className="w-12 h-12 rounded-full bg-slate-500" />
// <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-gray-600 text-sm">
// <p>
// {review.review}
// </p>
// </div>
// </div>
// )
// )}
// </div>
// </Layout>
// );
// };

// const Page: NextPage<{ profile: User }> = ({ profile }) => {
// return (
// <SWRConfig value={{ fallback: { '/api/users/me': { of: true, profile } } }}>
// <Profile />
// </SWRConfig>
// )
// };

export const getServerSideProps = withSsrSession(async function ({ req }: NextPageContext) {
const profile = await client.user.findUnique({
where: { id: req?.session.user?.id },
});
return {
props: {
profile: JSON.parse(JSON.stringify(profile)),
}
}
});
// export const getServerSideProps = withSsrSession(async function ({ req }: NextPageContext) {
// const profile = await client.user.findUnique({
// where: { id: req?.session.user?.id },
// });
// return {
// props: {
// profile: JSON.parse(JSON.stringify(profile)),
// }
// }
// });

export default Page;
export default dynamic(async () => Page, { ssr: false });

0 comments on commit fc67afd

Please sign in to comment.