React <Suspense>

lucky·2023년 5월 1일
0

React

목록 보기
1/1

이 글은 리액트 공식문서의 < Suspense > 파트를 요약 및 해석한 것입니다.

프론트엔드 코드를 작성하면서 매번 마주치는 비동기처리 함수.
그동안 react-query의 isLoading과 isError를 줄기차게 사용하던 중 리액트의 새로운 기능인 < Suspense > 컴포넌트가 여기저기 보이기 시작한다.
얼핏 보니 꽤 괜찮아 보여서 바로 공식문서 ㄱㄱ

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

Suspense 컴포넌트를 매우 잘 요약하는 3줄의 코드이다.
Suspense 컴포넌트안에 비동기 요청을 처리하는 컴포넌트를 자식으로 넣어주면 자식 컴포넌트가 로딩중일때 fallback의 props로 넘겨준 컴포넌트가 알아서 보여진다.

마치 like

{isLoading ? <Loading /> : <SomeComponent />}

뭐야 이게 더 간단한데?
하지만 요청해야하는 비동기 함수가 여러개라면? 먼저 완료된 컴포넌트만 보여주고 싶다면?
Suspense가 가독성도 좋고 편리하다고 할 수 있겠다.

Let's go deeper

	<>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>

이렇게 < Albums >만 Suspense로 감싸주면 로딩중일때 사용자는 기다리는 동안 Suspense 밖에 있는 이름(artists.name)을 볼 수 있게 되면서 인내심이 +1 상승하게 된다.

별거 아닌거 같지만 만약 한 페이지 내에서 로딩이 필요한 컴포넌트와 아닌 컴포넌트가 여러개 섞여있다면(예를 들어서 대시보드 페이지같은) Suspense가 사용자 경험을 개선하는데 큰 역할을 할 것이다.

	<>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>

이렇게 하면 Biography와 Albums 모두 로딩이 끝나야 Suspense에서 벗어날 수 있다.

<>
      <h1>{artist.name}</h1>
      <Suspense fallback={<BigSpinner />}>
        <Biography artistId={artist.id} />
        <Suspense fallback={<AlbumsGlimmer />}>
          <Panel>
            <Albums artistId={artist.id} />
          </Panel>
        </Suspense>
      </Suspense>
    </>

이렇게 하면 Biography와 Albums의 Suspense는 구분된다.
컴포넌트는 가장 가까운 부모의 Suspense와 엮이게 된다.
즉 Albums가 로딩중일 때는 AlbumsGlimmer가 보이고 Biography가 로딩중일 때는 BigSpinner가 보인다.
사실 이 코드에서는 Suspense가 nested되어 있기 때문에 로딩의 순서가 존재한다.
즉 Albums가 로딩이 끝나도 Bioraphy가 로딩중이라면 BigSpinner가 보여지면서 Albums는 안보일것이다.아마도

	<>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>

위와 같이 사용자의 입력에 따라 검색결과를 보여주는 코드가 있다고 하자.
한글자 입력할때마다 loading...이 보여질것이다.

근데 만약 이전 입력을 로딩중일때도 보여주고 싶다면?

useDeferredValue 훅을 사용하면 된다.

	const [query, setQuery] = useState('');
  	const deferredQuery = useDeferredValue(query);
  	const isStale = query !== deferredQuery;
    
	<>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{ opacity: isStale ? 0.5 : 1 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>

이렇게 하면 로딩중인 동안에 이전 쿼리의 결과를 계속 보여준다.
개인적으로 괜찮았다고 생각했던 부분.

만약 Suspense가 기존 UI를 가리는 것을 막고싶다면?
다시 말해 loading 컴포넌트를 사용하고 싶지 않고 이전의 UI를 보여주면서 기다리고 싶다면
stateTransition을 쓰면 된다.

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

이렇게 하면 IndexPage에서 ArtistPage로 넘어갈때 BigSpinner가 보이지 않고 로딩하는 동안 IndexPage가 보여지게 된다.

근데 이러면 사용자가 클릭이 먹혔는지 안먹혔는지 모른다. 그래서 useTransition 훅으로 isPending을 받아서 UIf로 표시해준다.

const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }
  
  
   return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
  
  

이런식으로 isPending에 따라 UI에 변화를 주면 된다...!

Suspense를 사용함으로써 조금 더 clean하고 논리적인 코드를 작성할 수 있지 않을까 기대한다.

profile
Good luck on our develope

0개의 댓글