본문으로 건너뛰기

23.08.03

오늘 한 일

  • 카공실록 장소 상세페이지 서버 컴포넌트 분리
  • gloddy 모임 상세페이지 api 연결

카공실록 장소 상세페이지 서버 컴포넌트 분리

이전에는 page.tsx 파일에 UI와 비즈니스 로직이 섞여있었다. 그래서 역할에 따라 컴포넌트를 분리했다.

Suspensive 라이브러리 사용

gloddy와 카공실록 두 프로젝트에서 Suspensive 라이브러리를 도입하자고 제안했다. 이 라이브러리를 사용하면 코드를 더욱 선언적으로 작성할 수 있어서 좋다.

다들 도입에 대해 흔쾌히 동의해서 바로 적용해보았다.

적용해보기

커스텀 쿼리 훅을 만들 때 useQuery 대신 useSuspenseQuery를 사용한다.

// apis/place/queries.ts
export function useGetPlace(id: number) {
return useSuspenseQuery(Keys.place(id), () => getPlace(id));
}

나는 서버컴포넌트(페이지)에서 데이터를 prefetch하고 있다. suspense로 감싸고 그 위에 errorboundary를 감싸서 에러를 잡으려고 했다.
react-query에서 QueryErrorResetBoundary를 제공하는데, 이 컴포넌트를 사용하면 에러가 발생했을 때 캐시를 리셋할 수 있는 UI를 띄울 수 있다.

하지만 사용할 떄 꽤나 복잡해 보일 수 있다. QueryErrorResetBoundary + ErrorBoundary가 기본이고, 추가로 Suspense까지 사용한다면 정신 없다..


이런 문제를 Suspensive 라이브러리에서 해결해주고 있다.

QueryErrorBoundaryQueryErrorResetBoundary + ErrorBoundary와 동일하고,
QueryAsyncBoundaryQueryErrorResetBoundary + ErrorBoundary + Suspense와 동일하다.

AsyncBoundary라는 컴포넌트도 제공하고 있는데, ErrorBoundary + Suspense와 동일하기 때문에
결국 QueryAsyncBoundaryQueryErrorBoundary + AsyncBoundary이다.

import PlaceDetail from './components/PlaceDetail';
import { Keys, getPlace } from '@/apis/place';
import { HydrationProvider } from '@/providers/HydrationProvider';
import { QueryAsyncBoundary } from '@suspensive/react-query';

export default async function PlacePage({ params }: { params: { id: string } }) {
const placeId = Number(params.id);
return (
<QueryAsyncBoundary
pendingFallback={<div>로딩중</div>}
rejectedFallback={({ error, reset }) => (
<div>
<div>에러가 발생했습니다.</div>
<button onClick={reset}>재시도</button>
<pre>{error.message}</pre>
</div>
)}
>
<HydrationProvider queryKey={Keys.place(placeId)} queryFn={() => getPlace(placeId)}>
<PlaceDetail placeId={placeId} />
</HydrationProvider>
</QueryAsyncBoundary>
);
}

이제 데이터를 가져올 때 별다른 분기처리 없이 data를 가져오면 된다.

image
// components/PlaceDetail.tsx
export default function PlaceDetail({ placeId }: PlaceDetailProps) {
const { data: placeData } = useGetPlace(placeId); // placeData: Place

return (
// ...
);
}
주의할 점

위와 같이 코드를 작성하면 에러가 발생하게 된다.

Uncaught Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".

image

함수를 "use server"로 표시해서 명시적으로 작성한 경우 말고는 직접 클라이언트 컴포넌트에 전달할 수 없다고 한다.

해결 방법

1. rejectedFallback에 들어갈 fallback 컴포넌트 만들기

fallback 클라이언트 컴포넌트를 만들어서 rejectedFallback에 전달해주면 된다.

2. "use server" 추가

"use serve"를 추가하기 위해서는 next config 파일에 다음과 같이 설정해야한다.

// next.config.js
module.exports = {
experimental: {
serverActions: true,
},
};

<QueryAsyncBoundary
pendingFallback={<div>로딩중</div>}
rejectedFallback={async ({ error, reset }) => {
'use server';
return (
<div>
<div>에러가 발생했습니다.</div>
<button onClick={reset}>재시도</button>
<pre>{error.message}</pre>
</div>
);
}}
>

3. QueryAsyncBoundary를 커스텀 컴포넌트로 만들기

커스텀 클라이언트 컴포넌트로 만들어서 사용하면 된다.


내일 할 일

  • gloddy 모임 멤버 상세정보 페이지 퍼블리싱