글 작성자: 망고좋아
반응형

🎯 리액트 React.lazy와 Suspense란?

📝 React.lazy

const SomeComponent = React.lazy(() => import('./SomeComponent'));
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </Suspense>
  );
}
  • React.lazy()를 사용하면 동적으로 불러오는 컴포넌트를 정의할 수 있다.
  • 번들의 크기를 줄이고, 초기 렌더링에서 사용되지 않는 컴포넌트를 불러오는 작업을 지연시킬 수 있다.
  • React.lazy로 import 해준 컴포넌트는 반드시 Suspense의 자식으로 들어가야 한다.

 

📝 Suspense

특정 컴포넌트에서 사용되고 있는 데이터의 준비가 아직 끝나지 않았음을 react에 알릴 수 있으며 data fetching 라이브러리와 함께 사용할 수 있는 구조

Suspense를 사용하면, 렌더링을 시작하기 전에 응답이 오기를 기다리지 않아도 된다.

  • Suspense를 사용하면 해당 데이터를 사용하는 컴포넌트를 렌더링 하지 않고 다른 로딩 화면을 보여줄 수 있다.
  • fetching 라이브러리에서의 워터폴 현상은 이전 fetch 요청에 대한 응답이 도착해야 다음 fetch 요청을 보낼 수 있는 구조이다.
    • 컴포넌트 렌더링 -> data fetching 요청 -> data 응답 구조로 동작
  • 이러한 문제를 Suspense를 이용해 data fetching 요청 -> data 응답 -> 컴포넌트 렌더링의 구조로 바뀌는 것이다.
  • 이렇게 구조가 바뀌면 data fetching 요청이 컴포넌트 렌더링에 의존되지 않고 모두 한 번에 실행되므로 워터폴 현상을 막을 수 있다.

 

// fetching 라이브러리만을 사용했을 때 구조
data fetching 요청 -> 로딩중 UI 렌더링 -> data 응답 -> 컴포넌트에 응답 반영

// suspense 사용
data fetching 요청 -> suspense 하위의 컴포넌트에 요청 리소스를 반영 -> suspense에 의해 로딩 UI 렌더 -> 요청 리소스로 data 응답이 들어옴 -> 컴포넌트에 응답 반영
  • 이러한 구조에서 suspense는 요청 직후 요청 리소스를 바로 컴포넌트로 주입하는 방식으로 바꿔준다.
  • 여기서 말하는 요청 리소스는 Promise의 형태가 아니다. data fetching 라이브러리 내부적으로 구현되어있는 일반 객체이다.

 

const ProfilePage = React.lazy(() => import('./ProfilePage')); // 지연 로딩

// 프로필을 불러오는 동안 스피너를 표시
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
  • Suspense를 사용하면 선언적으로 데이터 등을 기다려준다.
  • 이미지, 스크립트, 그 밖의 비동기 작업을 기다리는 데에도 사용할 수 있다.

 

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // 비록 아직 불러오기가 완료되지 않았겠지만, 사용자 정보 읽기를 시도합니다
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // 비록 아직 불러오기가 완료되지 않았겠지만, 게시글 읽기를 시도합니다
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
  • Suspense를 사용하면 컴포넌트가 렌더링 되기 전까지 기다릴 수 있다.
  • Suspense는 데이터 불러오기 라이브러리가 아니다.
  • Suspense는 컴포넌트가 읽어 들이고 있는 데이터가 아직 준비되지 않았다고 React에 알려줄 수 있는, 데이터 불러오기 라이브러리에서 사용할 수 있는 메커니즘이다. 이후 리액트는 데이터가 준비되기를 기다렸다가 UI를 갱신할 수 있다.

 

📝 Suspense가 아닌 것

  • Suspense는 데이터 불러오기에 대한 구현이 아니다.
    • GraphQL, REST 또는 특정한 데이터 형식, 라이브러리, 전송 또는 프로토콜을 사용한다고 가정하지 않는다.
  • Suspense는 바로 사용할 수 있는 클라이언트가 아니다. fetch 또는 Relay를 Suspense로 “대체”할 수 없다.
  • Suspense는 데이터 불러오기 작업과 뷰 레이어를 결합해주지 않는다. UI 상에 로딩 상태를 표시할 수 있도록 조정하는 것을 돕지만, 네트워크 로직을 React 컴포넌트에 종속시키는 것은 아니다.

 

📝 Suspense가 가능한 것

  • 데이터 불러오기 라이브러리들이 React와 깊게 결합할 수 있도록 해 준다.
  • 의도적으로 설계된 로딩 상태를 조정할 수 있도록 해 준다.
    • Suspense는 데이터가 어떻게 불러져야 하는지를 정하지 않고, 앱의 시각적인 로딩 단계를 밀접하게 통제할 수 있도록 해준다,
  • 경쟁 상태(Race Condition)를 피할 수 있도록 돕는다.
    • await를 사용하더라도 비동기 코드는 종종 오류가 발생하기 쉽다. Suspense를 사용하면 데이터를 동기적으로 읽어오는 것처럼 느껴지게 해 준다. 마치 이미 불러오기가 완료된 것처럼!

 

📝 동작 순서

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // 아직 로딩이 완료되지 않았더라도, 사용자 정보 읽기를 시도합니다
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // 아직 로딩이 완료되지 않았더라도, 게시글 읽기를 시도합니다
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
  1. 이미 fetchProfileData()에게 요청을 보냈다.
  2. <ProfilePage> 렌더링을 시도
    1. 자식 컴포넌트로 <ProfileDetails><ProfileTimeline>을 반환
  3. <ProfileDetails> 렌더링을 시도
    1. resource.user.read() 호출
    2. 아직 불러온 데이터가 아무것도 없으므로, 이 컴포넌트는 “정지”
    3. 이 컴포넌트를 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도
  4. <ProfileTimeline> 렌더링 시도
    1. resource.posts.read() 호출
    2. 또 한 번, 아직 데이터가 없으므로, 이 컴포넌트 또한 “정지”
    3. React는 이 컴포넌트도 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도
  5. 렌더링을 시도할 컴포넌트가 없다.
    1. <ProfileDetails>가 정지된 상태이므로, React는 트리 상에서 <ProfielDetails> 위에 존재하는 것 중 가장 가까운 <Suspense> Fallback을 찾는다.
    2. <h1>Loading profile...</h1>
  • 데이터가 계속 흘러들어옴에 따라, React는 렌더링을 다시 시도하며, 그때마다 React가 “더 깊은 곳까지” 처리할 수 있게 된다.
  • resource.user를 불러오고 나면, <ProfileDetails> 컴포넌트는 성공적으로 렌더링 ⇒ Fallback 사라짐

 

🏷 요약

  • suspense란 비동기 작업이 자식 컴포넌트에서 처리되기 전에 suspense의 fallback props가 렌더링 된다. 그리고 자식 컴포넌트에서 비동기 작업이 끝나면 리 렌더링이 일어나면서 fallback이 종료되고 컴포넌트가 렌더링 된다.
  • suspense는 렌더링 시점에서만 감지해준다.
  • 따라서 기존에 있는 isLoding을 완전히 대체할 수 없다. 첫 페이지 로딩은 suspense를 사용하고, 페이지 내에서 비동기 처리를 하는 부분은 isLoading을 사용해서 로딩 창을 보여줘야 한다.
    • 추후 지원된다고 한다.
  • 따라서 첫 화면 렌더링 된 이후에 데이터를 요청해서 받아오는 일이 없다면 suspense만으로 로딩 창을 손쉽게 구현할 수 있다.
  • 하지만 렌더링 후 페이지 내에서 데이터를 요청해서 받고 보여줘야 한다면 useState로 isLoding를 설정해서 로딩 창을 보여줘야 한다.

 

📌 참고

반응형