Suspense를 사용해 데이터 로딩 관리하기

드뮴·2025년 3월 1일
4

🪴 개발일지

목록 보기
3/6
post-thumbnail

로딩 화면을 if 문으로 처리해야할까?

Tanstack Query를 이용해서 데이터를 요청하는 코드를 사용하는 컴포넌트 코드이다.

로딩 중인지, 에러가 발생했는지 처리를 할 수 있다는 점은 좋지만 중요한 건 해당 상태에 따라 조건문을 통해 처리하게 되는데 중요한 건 이렇게 처리해주는 컴포넌트가 많다는 점이다.

데이터를 요청하는 모든 곳에서 이런 패턴으로 사용되고 있고, 그때마다 적절한 로딩 중이나 에러 발생에 대한 컴포넌트를 보여주고 싶었지만 임시 방편으로 <div>로딩 중</div> 처리를 해두거나 혹은 다른 컴포넌트에서는 따로 로딩 컴포넌트를 만들었다. 그러나 이렇게 사용하게 되어서 불편한 점이 많았다.

  • 컴포넌트가 많기 때문에 전부 다 if 문으로 처리해주지만 if 문으로 처리하게 되면 코드 가독성이 좋지 않다는 생각이 들었다. 로딩 상태를 내부가 아닌 외부에서 처리할 수 있게 수정하고 싶었다.
  • 만약 로딩 화면을 보여주기 위해 여러 조건을 추가해서 처리하게 된다면 조건문 자체가 복잡해지기 때문에 코드는 더 복잡해진다. 따라서 로딩 상태를 내부에서 관리하는 건 복잡성을 증가시킨다고 생각했다.

위 이유들로 조금 더 깔끔하고 편리하게 관리할 수 없을까? 고민하던 중 리액트의 Suspense에 대해 알게되었다.


Suspense란?

Suspense는 리액트의 내장 컴포넌트로, 비동기 작업이 완료될 때까지 컴포넌트 트리의 렌더링을 일시 중단하는 매커니즘을 제공한다.

데이터 로딩, 코드 스플리팅, 이미지 로딩 등 비동기적으로 무언가를 기다려야하는 상황을 선언적으로 처리할 수 있게 해준다.

Suspense의 핵심 개념

  1. 선언적 대기: if/else로 로딩 상태를 관리하는게 아닌 컴포넌트가 준비될 때까지 전달한 UI를 보여달라고 선언적으로 표현한다.
  2. 경계 설정: Suspense는 애플리케이션에서 로딩 상태를 보여줄 경계를 설정한다. 이 경계 내부의 컴포넌트가 데이터를 기다리는 동안 경계에 지정된 fallback UI가 표시된다.
  3. 컴포넌트 트리 일시 중단: Suspense 내부의 컴포넌트가 아직 렌더링할 준비가 되지 않았다고 알릴 수 있고, 이때 리액트는 해당 컴포넌트 렌더링을 일시 중단하고 fallback을 표시한다.

Suspense의 작동 원리

Suspense가 fallback에 있는 컴포넌트를 보여주는 원리는 무엇일까?

  1. Suspense는 자식 컴포넌트가 렌더링 중에 Promise를 던지는지(throw) 감지한다. 리액트는 컴포넌트가 Promise를 던지면 이를 특별한 시그널로 해석한다.
  2. 컴포넌트가 Promise를 던지면, 리액트는 해당 컴포넌트의 렌더링을 중단하고 가장 가까운 Suspense 경계를 찾아 올라간다.
  3. 리액트는 찾은 Suspense 경계의 fallback prop에 지정된 컴포넌트를 대신 렌더링한다.
  4. Promise가 해결되면 리액트는 이를 감지하고 중단했던 원래 컴포넌트의 렌더링을 다시 시도한다.
  5. 데이터가 준비되었으므로 컴포넌트는 Promise를 던지지 않고 성공적으로 렌더링 된다. 리액트는 fallback을 실제 컴포넌트로 교체한다.

리액트는 컴포넌트의 렌더링을 시도하고, Promise를 감지하면 렌더링을 중단하고 fallback을 표시해주고 Promise가 해결되면 다시 렌더링을 시도하게 된다.


Suspense 사용하기

1. Suspense 모드 활성화하기

Tanstack Query(v5)에서 Suspense 모드를 사용하기 위해서 useSuspenseQuery를 제공한다.

따라서 해당 쿼리를 사용해 별도의 옵션 없이 Suspense 모드를 이용할 수 있다.

const { data: post } = useSuspenseQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})

2. Suspense로 감싸주기

if (isLoading)을 통해 로딩 중일 때 로딩 컴포넌트를 보여주던 코드를 삭제하면 된다. 대신 해당 컴포넌트 상위에 Suspense 태그로 감싸주고 fallback에는 로딩 중 보여줄 컴포넌트를 넣어주면 된다.

3. 컴포넌트 별로 로딩 컴포넌트 다르게 설정하기

Suspense를 이용해 로딩 컴포넌트를 동일하게 사용할 수도 있지만, 개인적으로 이후 스켈레톤 UI를 도입해 로딩 화면을 페이지마다 다르게 보여줄 생각이었다. 그렇다면 스켈레톤 UI를 도입하는 경우처럼 컴포넌트 별로 로딩 화면을 다르게 설정하려면 어떻게 할 수 있을까?

라우트 설정 파일이나 혹은 다른 컴포넌트 파일의 상위에서 Suspense로 감싸주면 된다.

예시 라우트 파일을 살펴보면 다음과 같다.

<Routes>
  <Route 
    path="/questions" 
    element={
      <Suspense fallback={<QuestionPageSkeleton />}>
        <QuestionPage />
      </Suspense>
    }
  />
  <Route 
    path="/profile" 
    element={
      <Suspense fallback={<ProfilePageSkeleton />}>
        <ProfilePage />
      </Suspense>
    }
  />
</Routes>
  • 질문 리스트 페이지에서는 질문 리스트 카드를 보여주는 스켈레톤 UI를 보여주고, 프로필 페이지에서는 프로필 카드를 보여주는 스켈레톤 UI를 적용할 것이다.
  • 그런데 두 페이지를 보면 질문 리스트는 질문 리스트를 요청하는 로직이 존재하기 때문에 해당 element를 Suspense로 감싸주고, 프로필도 프로필 관련 정보를 요청하는 로직이 존재하므로 해당 element를 Suspense로 감싸준다.

위와 같이 처리하게 되면, 로딩 컴포넌트를 라우트 파일에서만 관리하면 되기 때문에 각각 컴포넌트 파일을 전부 찾아 다니며 수정할 번거로움도 사라지게 된다. 그러나 페이지 컴포넌트 자체에 Suspense를 감싸게 되면 로딩이 필요없는 다른 요소도 표시되지 않기 때문에 정확히 데이터를 로딩하는 부분에 감싸는게 좋다.


Suspense를 도입한 결과

PR 링크
[Refactor] 일관된 로딩 처리를 위한 Suspense 적용

변경 전

변경 후

  • 컴포넌트 내부에서 로딩 상태에 대한 처리를 별도로 해줄 필요가 없어졌다. 각 컴포넌트는 복잡하게 로딩 중일 때 처리와 실제 데이터가 있을 때 보여주는 UI 처리 등 조건문을 통해 복잡하게 나타낼 필요가 없어졌다. 즉, 로딩 중과 실제 컴포넌트를 분리하여 실제 컴포넌트 로직에 집중할 수 있게 되었다.

이 글에서는 간단하게 라우트 별로 Suspense를 설정해주도록 작성했지만, 예를 들어 /items 페이지가 있고 해당 페이지에는 페이지 타이틀, 아이템 목록, 검색 기록 등 다양한 구성 요소가 있는데 데이터를 받아와 표시해주는 부분이 아이템 목록이고 이 부분에만 로딩 표시를 해줘야한다면 해당 컴포넌트를 Suspense로 감싸주면 된다.

profile
안녕하세오

0개의 댓글

관련 채용 정보