[번역] Async rendering in React: Suspense, Hooks, and other methods_02

JJ·2023년 11월 17일

기술 블로그 번역

목록 보기
2/3
post-thumbnail

Using Suspense to reveal content all at once

Suspense가 가지는 또 하나의 이점으로, 기존 리액트의 waterfall 현상을 개선할 수 있다.

waterfall effect

UI가 점진적으로 로드되면서 발생하는 보다 자세하게는 여러개의 컴포넌트가 있을 때, 하나의 컴포넌트에서 데이터 로딩이 오래 걸리면, 이후의 컴포넌트도 이를 기다리게 되므로, 전체 렌더링 시간이 지연되는 현상을 말한다.

아래의 예제로 살펴보자.

src/components/fetchData.js

import axios from "axios";

export const fetchData = (apiURL, artificialDelay) => {
  let status = "pending";
  let result;
  let suspender = new Promise((resolve, reject) => {
    setTimeout(() => {
      axios(apiURL)
        .then((r) => {
          status = "success";
          result = r.data;
          resolve();
        })
        .catch((e) => {
          status = "error";
          result = e;
          reject();
        });
    }, artificialDelay);
  });
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    },
  };
};
  • fetchData 함수
    • parameter
      • apiURL
      • artificialDelay
    • variables
      • status : fetchData 함수 내부에서 API 호출의 응답에 따라 변경될 변수 (초기값: pending)
      • result : API 호출의 결과값을 저장할 변수
      • suspender : Promise 객체를 저장할 변수
        • API 호출이 일어남.
    • return
      • read() 메서드가 담긴 객체를 반환
        • status에 따라 각기 다른 변수를 반환하거나 던진다.

❗ fetchData의 동작 원리

1. fetchData 함수가 호출된다.
2. read 메서드가 담긴 객체를 반환하고, fetchData 함수는 종료된다.
3. read 메서드가 실행되는데, 이 때 status는 pending 이므로, suspender를 던진다.
3-1. 이때, fetchData 함수가 종료되어 실행컨텍스트에서 제거될 것 같지만, 객체 내부에서 suspender를 참조하고 있으므로, 클로져의 특징에 의해 suspender 내부의 로직이 실행된다.
4. api 호출이 일어난다.
5. 그 결과에 따라 status 와 result에 값이 할당된다.
6. read() 메서드가 다시 실행되고, status가 success 라면 API 호출의 결과값을 반환한다.

src/components/ShowDetails/index.js

import { fetchData } from "../fetchData";
import * as Styles from "./styles";

const resource = fetchData(`https://api.tvmaze.com/shows/27436`);

const removeTags = (str) => {
  if (str === null || str === "") return false;
  else str = str.toString();
  return str.replace(/(<([^>]+)>)/gi, "");
};

const ShowDetails = () => {
  const show = resource.read();

  return (
    <Styles.Root>
      <Styles.Container>
        <div>
          <img src={show.image.medium} alt="show poster" />
          <p>Show name: {show.name}</p>
          <p>Description: {removeTags(show.summary)}</p>
          <p>Language: {show.language}</p>
          <p>Genres: {show.genres.join(", ")}</p>
          <p>Score: {show.rating.average}/10</p>
          <p>Status: {show.status}</p>
        </div>
      </Styles.Container>
    </Styles.Root>
  );
};

export default ShowDetails;
  • ShowDetails 컴포넌트는 fetchData 함수의 결과값으로부터 read 메서드로 원하는 결과값을 받아 렌더링한다. 위의 fetchData 함수의 동작 원리에서 살펴본 것처럼 read 메서드를 사용하여 만약 pending 상태라면, react suspense에서 이를 캐치하여 렌더링을 지연시키고, success 가 되었을 때 렌더링이 된다.

src/components/ShowEpisodes/index.js

import { fetchData } from "../fetchData";
import * as Styles from "./styles";

const resource = fetchData(`https://api.tvmaze.com/shows/27436/episodes`, 5000);

const removeTags = (str) => {
  if (str === null || str === "") return false;
  else str = str.toString();
  return str.replace(/(<([^>]+)>)/gi, "");
};

const convertRuntimeToHoursAndMinutes = (runtime) => {
  const hours = Math.floor(runtime / 60);
  const minutes = runtime % 60;
  return `${hours}h ${minutes}m`;
};

const ShowEpisodes = () => {
  const episodes = resource.read();

  return (
    <Styles.Root>
      <Styles.Container>
        {episodes.map((episode, index) => (
          <Styles.ShowWrapper key={index}>
            <Styles.ImageWrapper>
              <img
                src={episode.image ? episode.image.original : ""}
                alt="Show Poster"
              />
            </Styles.ImageWrapper>

            <Styles.TextWrapper>
              <p>{episode.name}</p>
              <p>{removeTags(episode.summary)}</p>
              <p>Score: {episode.rating.average}/10</p>
              <p>Runtime: {convertRuntimeToHoursAndMinutes(episode.runtime)}</p>
            </Styles.TextWrapper>
          </Styles.ShowWrapper>
        ))}
      </Styles.Container>
    </Styles.Root>
  );
};

export default ShowEpisodes;
  • ShowEpisodes 컴포넌트 또한, fetchData 함수의 결과값으로부터 read 메서드로 원하는 결과값을 받아 렌더링한다. 위의 fetchData 함수의 동작 원리에서 살펴본 것처럼 read 메서드를 사용하여 만약 pending 상태라면, react suspense에서 이를 캐치하여 렌더링을 지연시키고, success 가 되었을 때 렌더링이 된다.

App.js

import React, { Suspense } from "react";
import "./App.css";

import ShowDetails from "./components/ShowDetails";
import ShowEpisodes from "./components/ShowEpisodes";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1 className="App-title">React Suspense Demo</h1>
      </header>
      <Suspense fallback={<p>loading...</p>}>
        <ShowDetails />
        <ShowEpisodes />
      </Suspense>
    </div>
  );
}

export default App;
  • 위의 코드를 살펴보면, ShowEpisodes의 경우 ShowDetails와 다르게 5초 후에 동작하도록 되어 있기 때문에 만약 suspense가 없더라면, 앞서 언급한 waterfall 현상처럼 ShowDetails가 먼저 렌더링되고, 이후에 ShowEpisodes가 렌더링될 것이다. 하지만, App.js 코드에서 Suspense의 범위에 두개의 컴포넌트가 모두 속해 있기 때문에, 렌더링은 두 컴포넌트의 API 호출이 완료되었을 때 이루어진다.
profile
한줄 한줄

0개의 댓글