
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;
}
},
};
};
read() 메서드가 담긴 객체를 반환❗ 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;
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;
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;
App.js 코드에서 Suspense의 범위에 두개의 컴포넌트가 모두 속해 있기 때문에, 렌더링은 두 컴포넌트의 API 호출이 완료되었을 때 이루어진다.