Sua Blog

사용자 경험 최적화 - React Suspense (feat. React 18)

React 18에 Suspense 추가

React v18이 릴리즈 된 후 Suspense가 정식 기능이 되었다.

React.Suspense를 사용하면 어떤 작업(데이터 페칭 등의 비동기 작업)이 끝날 때까지 컴포넌트의 렌더링을 잠시 중단 시키고 다른 컴포넌트를 먼저 렌더링 할 수 있다. 로딩 화면을 만들 때 일반적으로 hook을 사용하거나 데이터 로딩을 전문으로 하는 라이브러리 등에 의존하는 경우가 많은데 기존 라이브러리와 Suspense를 함께 사용하면 효과가 더 좋다.

Suspense를 지원하는 라이브러리들: Relay, SWR, React Query, Recoil

기존에 사용하고 있던 방식 Apollo Client

const DataComponent = () => {
  const { loading, data } = useQuery(FETCH_DATA);

  if (loading) return <Spinner />;
  return <DataList data={data} />;
};

Suspense

<Suspense fallback={<Spinner />}>
  <DataComponent />
</Suspense>

위와 같이 Suspense로 감싸주면 컴포넌트의 렌더링을 특정 작업 이후로 미루고, 그 작업이 끝날 때 까지는 fallback속성으로 넘긴 컴포넌트를 대신 보여줄 수 있다.

컴포넌트 렌더링 전에 어떤 작업이 일어나야하는지는 DataComponent안에 명시 되어있을 것이다.

useEffect

리액트에서 비동기 데이터를 읽어올 때 생명주기 함수인 componentDidMount()를 사용하거나 함수형 컴포넌트에서 useEffect() hook을 사용하였다. API를 호출하여 네트워크를 통해 데이터를 가져오는 것은 컴포넌트에서 발생할 수 있는 대표적인 side effect이기 때문이다.

동작 구조가 컴포넌트 렌더링 -> data 요청 -> data 응답 -> 컴포넌트 렌더링의 방식이다. 이러한 구조는 워터폴(waterfall) 현상을 발생시키는데 한 페이지상에 여러 컴포넌트에서 동시에 비동기 데이터를 읽어오는 경우 상위 컴포넌트의 데이터 로딩이 끝나야지만 하위 컴포넌트의 데이터 로딩이 시작 될 수 있다.

data fetching library

data fetching library는 워터폴 현상을 해결한다. 컴포넌트 트리 구조에 필요한 모든 data fetching 요청을 렌더링 이전에 실행하도록 중앙화 하여 해결한다. 즉, data 요청 -> data 응답 -> 컴포넌트 렌더링의 구조로 바뀌게 되는 것이다. 이렇게 구조가 바뀌면 data fetching 요청이 컴포넌트 렌더링에 의존하지 않고 모두 한 번에 실행되므로 워터폴 현상을 막을 수 있다.

하지만 data fetching library로 모든걸 해결할 수는 없다. 컴포넌트에 필요한 모든 data 응답을 받을 때까지 해당 컴포넌트는 물론이고 하위 컴포넌트의 로딩도 의미가 없어져 버린다. 하위 컴포넌트가 렌더링 된 시점에서는 상위 컴포넌트에 의해 이미 모든 데이터를 응답받은 상태이기 때문에 하위 컴포넌트의 로딩은 절대 렌더링 되지 않을 것이다.

개발 측면에서도 if 조건문을 사용하여 어떤 컴포넌트를 보여줄지 제어하는 것은 명령형(imperative) 코드에 가깝기 때문에 선언적(declarative) 코드를 지향하는 React의 기본 방향성과 맞지 않기도 하다.

기본적으로 데이터 로딩과 UI 렌더링이라는 두 가지의 전혀 다른 목표가 한 컴포넌트에 있어 코드를 읽기 어려워지고 테스트를 작성하기도 어려워진다.

Suspense

Suspense를 사용하면 구조가 data 요청 -> suspense 하위 컴포넌트에 리소스 반영 -> suspense에 의한 loading 렌더링 -> data 응답 -> 컴포넌트에 반영으로 바뀌게 된다.

이런 구조로 바뀌면 fetching 라이브러리만 사용했을 때 모든 data 응답을 기다려야 컴포넌트 트리를 렌더링할 수 있었던 문제를 해결할 수 있다. fetching 요청 직후 응답 도착 여부와는 상관없이 렌더링을 수행하기 때문이다.

React.lazy와 함께 사용

SPA의 단점은 한번에 사용하지 않는 모든 컴포넌트까지 불러오기 때문에 첫 화면이 렌더링 될 때 까지의 시간이 오래걸리는 것이다. React lazy는 컴포넌트를 동적으로 import할 수 있기 때문에 이를 사용하면 초기 렌더링 지연 시간을 어느정도 줄일 수 있다.

만약 Router로 분기가 나누어진 컴포넌트를 React.lazy를 통해 import하면 해당 path로 이동할 때 컴포넌트를 불러오게 되는데 이 과정에서 로딩시간이 생기게 된다. 이 로딩되는 시간동안 로딩화면을 보여지도록 해주는 역할을 하는 것이 Suspense이다.

const SignIn = React.lazy(() =>
  import('../screen/SignInScreen').then(({ SignInScreen }) => ({
    default: SignInScreen,
  }))
);

<Suspense fallback={<Spinner />}>
  <Switch>
    ...
    <Route exact path="/signIn" render={() => <SignIn />} />
    ...
  </Switch>
</Suspense>;

정리

사용자가 기다리는 대부분의 시간은 data fetching과 관련이 있다. Suspense를 통해 이 과정을 더 효울적으로 처리할 수 있다면 사용자 경험 개선에 도움이 될 것 같다.

참고

react docs https://www.daleseo.com/react-suspense/