Tanstack Query를 이용해서 데이터를 요청하는 코드를 사용하는 컴포넌트 코드이다.
로딩 중인지, 에러가 발생했는지 처리를 할 수 있다는 점은 좋지만 중요한 건 해당 상태에 따라 조건문을 통해 처리하게 되는데 중요한 건 이렇게 처리해주는 컴포넌트가 많다는 점이다.
데이터를 요청하는 모든 곳에서 이런 패턴으로 사용되고 있고, 그때마다 적절한 로딩 중이나 에러 발생에 대한 컴포넌트를 보여주고 싶었지만 임시 방편으로 <div>로딩 중</div>
처리를 해두거나 혹은 다른 컴포넌트에서는 따로 로딩 컴포넌트를 만들었다. 그러나 이렇게 사용하게 되어서 불편한 점이 많았다.
위 이유들로 조금 더 깔끔하고 편리하게 관리할 수 없을까? 고민하던 중 리액트의 Suspense
에 대해 알게되었다.
Suspense
는 리액트의 내장 컴포넌트로, 비동기 작업이 완료될 때까지 컴포넌트 트리의 렌더링을 일시 중단하는 매커니즘을 제공한다.
데이터 로딩, 코드 스플리팅, 이미지 로딩 등 비동기적으로 무언가를 기다려야하는 상황을 선언적으로 처리할 수 있게 해준다.
Suspense가 fallback에 있는 컴포넌트를 보여주는 원리는 무엇일까?
리액트는 컴포넌트의 렌더링을 시도하고, Promise를 감지하면 렌더링을 중단하고 fallback을 표시해주고 Promise가 해결되면 다시 렌더링을 시도하게 된다.
Tanstack Query(v5)에서 Suspense 모드를 사용하기 위해서 useSuspenseQuery를 제공한다.
따라서 해당 쿼리를 사용해 별도의 옵션 없이 Suspense 모드를 이용할 수 있다.
const { data: post } = useSuspenseQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
})
if (isLoading)을 통해 로딩 중일 때 로딩 컴포넌트를 보여주던 코드를 삭제하면 된다. 대신 해당 컴포넌트 상위에 Suspense 태그로 감싸주고 fallback에는 로딩 중 보여줄 컴포넌트를 넣어주면 된다.
Suspense를 이용해 로딩 컴포넌트를 동일하게 사용할 수도 있지만, 개인적으로 이후 스켈레톤 UI를 도입해 로딩 화면을 페이지마다 다르게 보여줄 생각이었다. 그렇다면 스켈레톤 UI를 도입하는 경우처럼 컴포넌트 별로 로딩 화면을 다르게 설정하려면 어떻게 할 수 있을까?
라우트 설정 파일이나 혹은 다른 컴포넌트 파일의 상위에서 Suspense로 감싸주면 된다.
예시 라우트 파일을 살펴보면 다음과 같다.
<Routes>
<Route
path="/questions"
element={
<Suspense fallback={<QuestionPageSkeleton />}>
<QuestionPage />
</Suspense>
}
/>
<Route
path="/profile"
element={
<Suspense fallback={<ProfilePageSkeleton />}>
<ProfilePage />
</Suspense>
}
/>
</Routes>
위와 같이 처리하게 되면, 로딩 컴포넌트를 라우트 파일에서만 관리하면 되기 때문에 각각 컴포넌트 파일을 전부 찾아 다니며 수정할 번거로움도 사라지게 된다. 그러나 페이지 컴포넌트 자체에 Suspense를 감싸게 되면 로딩이 필요없는 다른 요소도 표시되지 않기 때문에 정확히 데이터를 로딩하는 부분에 감싸는게 좋다.
이 글에서는 간단하게 라우트 별로 Suspense를 설정해주도록 작성했지만, 예를 들어 /items 페이지가 있고 해당 페이지에는 페이지 타이틀, 아이템 목록, 검색 기록 등 다양한 구성 요소가 있는데 데이터를 받아와 표시해주는 부분이 아이템 목록이고 이 부분에만 로딩 표시를 해줘야한다면 해당 컴포넌트를 Suspense로 감싸주면 된다.