Suspense
오늘의 목표
Suspense 기능 구현하기- 프론트엔드 측에서는 단순히 정적으로 데이터를 가져와서 화면에 그려주는 것 뿐만아니라, 외부 API를 호출하여 동적으로 데이터를 가져와서 화면에 그려주는 기능이 필요합니다.
- 하지만 외부 API를 호출하는 작업은 네트워크 상태에 따라 지연이 발생할 수 있기 때문에, 이러한 비동기 작업을 처리하는 방법이 필요합니다.
- 리액트에서는 이러한 비동기 작업을 처리하기 위해
Suspense라는 기능을 제공합니다. - 이번 섹션에서는
Suspense를 직접 구현해보며, 비동기 작업을 처리해보겠습니다.
Suspense
평소대로 API 호출후, 화면에 그려보기
- 평소대로 api 호출후 받은 응답값을 화면에 그려주는 코드를 먼저 작성하고, 이를 app.tsx 모듈에서 가져와 img 태그의 src속성에 할당해보겠습니다.
api.ts
1 |2 |export const getResource = () => {3 |const random = Math.random();4 |const photoURL = `https://picsum.photos/seed/${encodeURIComponent(5 |random6 |)}/200`;7 |8 |return new Promise((resolve, reject) => {9 |setTimeout(() => resolve(photoURL), 1500);10 |});11 |};12 |
app.tsx
1 |import React from "../core/react.js";2 |import { getResource } from "./api.js";3 |4 |export const App = () => {5 |...6 |const photo = getResource();7 |return (8 |...9 |<h4>10 |오늘 기분은 어떠신가요? 좋은 사진 보고가세요, 오늘도 화이팅입니다!!11 |</h4>12 |<img src={photo} alt="랜덤 이미지" style={{ width: "100%" }} />13 |);14 |};15 |

- 위의 이미지를 보시면,렌더링이 완료 되었지만 화면에 이미지가 반영되지는 않았습니다.
core/suspense.js
1 |const resourceCache = {};2 |3 |export const createResource = (asyncTask, key) => {4 |if (resourceCache[key]) return resourceCache[key];5 |// 먼저 키가 캐시되어 있는지 확인하고 캐시되어있을시 캐시된값을 반환6 |7 |// 캐싱 되어있지 않을때8 |const promise = asyncTask();9 |throw { promise, key };10 |};11 |
- 비동기함수에 대한 캐싱을 작성을 했습니다.
- 캐싱이 되어있지 않을때는 인자로 받은 비동기 함수를 실행시키고, 그 promise 객체를 전파하게끔 했습니다.
- 이제 인자로 비동기 함수를 받게 되었으니, 애플리케이션에서 사용해보겠습니다.
core/suspense.js
1 |export const App = () => {2 |3 |const photo = createResource(getResource, "test");4 |5 |return (6 |<div>7 |<h2>안녕하세요! 저는 유선향 입니다.</h2>8 |<h4>9 |오늘 기분은 어떠신가요? 좋은 사진 보고가세요, 오늘도 화이팅입니다!!10 |</h4>11 |<img src={photo} alt="랜덤 이미지" style={{ width: "100%" }} />12 |</div>13 |);14 |};15 |

가상돔 생성시 Promise 처리하기
- 이전에 작성한
createElement를 이용해서,tag의 타입이 함수인 경우에는 로딩중... 이라는 h2태그를 리턴하게 했습니다.
core/react.js
1 |const React = {2 |createElement: (tag, props, ...children) => {3 |if (typeof tag === "function") {4 |try {5 |return tag(props, ...children);6 |} catch ({ promise, key }) {7 |return { tag: "h2", props: null, children: ["로딩중..."] };8 |}9 |}10 |const el = {11 |tag,12 |props,13 |children,14 |};15 |return el;16 |},

- 예상대로 대체 UI인 "로딩중..." 이라는 문구가 화면에 표시되는것을 확인할 수 있습니다.
- 또한 더이상 콘솔에서 전파된 promise에 대한 에러메시지가 출력되지 않았습니다.
promise 완료 처리하기
- 그런데 지금은 promise가 완료 되었을때, 다시 렌더링이 실행되거나 하지는 않기 때문에, 영원히 "로딩중..." 상태에 머물러 있게 됩니다.
- then 메소드로 promise가 완료 되었을때, 캐시객체에 등록하고, 리렌더링이 실행되도록 코드를 수정해보겠습니다.
core/react.js
1 |const React = {2 |createElement: (tag, props, ...children) => {3 |if (typeof tag === "function") {4 |try {5 |return tag(props, ...children);6 |} catch ({ promise, key }) {7 |promise.then((value) => {8 |resourceCache[key] = value;9 |rerender();10 |});11 |return { tag: "h2", props: null, children: ["로딩중..."] };12 |}13 |...14 |},

- 목표한대로 잘 되고 있습니다!
동시성 렌더링
- 위에서 우리는 간단하게 Suspense 기능을 구현해보았습니다.
- 그런데, suspense 두가지를 동시에 사용하게 되면 어떻게 될까요?
- 바로 해보겠습니다.
core/react.js
1 |...2 |export const App = () => {3 |const photo1 = createResource(getResource, "photo-url");4 |const photo2 = createResource(getResource, "photo-url2");5 |6 |return (7 |<div>8 |...9 |<img src={photo1} alt="랜덤 이미지" style={{ width: "100%" }} />10 |<img src={photo2} alt="랜덤 이미지" style={{ width: "100%" }} />11 |</div>12 |);13 |};14 |render(<App />, document.getElementById("app"));15 |

- 두개의 Suspense가 동시에 작동하는 것이 아니라, 첫번째 Suspense가 완료된 이후에야 두번째 Suspense가 작동하는것을 확인할 수 있습니다.
- 이전에 한개의 Suspense만 사용했을때의 모습과 비교해 보시죠


동시성 문제 해결하기
app.tsx
1 |export const App = () => {2 |const photo1 = resourceCache["photo-url-1"];3 |const photo2 = resourceCache["photo-url-2"];4 |5 |return (6 |...7 |<Suspense8 |fallback={<div>photo-url 로딩중...</div>}9 |key="photo-url-1"10 |asyncFn={getResource}11 |>12 |<img src={photo1} alt="랜덤 이미지1" style={{ width: "100%" }} />13 |</Suspense>14 |<Suspense15 |fallback={<div>photo-url2 로딩중...</div>}16 |key="photo-url-2"17 |asyncFn={getResource}18 |>19 |<img src={photo2} alt="랜덤 이미지2" style={{ width: "100%" }} />20 |</Suspense>21 |);22 |};23 |
/core/suspense.js
1 |export const createResource = (asyncFn, key, fallback) => {2 |// 먼저 키가 캐시되어 있는지 확인하고 리턴3 |if (resourceCache[key]) {4 |return;5 |}6 |const promise = asyncFn();7 |throw { promise, key, fallback };//fallback도 전파8 |};9 |10 |export const Suspense = (props, children) => {11 |const { asyncFn, key, fallback } = props;12 |createResource(asyncFn, key, fallback);13 |return children;14 |};
/core/react.js
1 |const React = {2 |createElement: (tag, props, ...children) => {3 |if (typeof tag === "function") {4 |try {5 |return tag(props, ...children);6 |} catch ({ promise, key, fallback }) {7 |promise.then((value) => {8 |resourceCache[key] = value;9 |rerender();10 |});11 |return fallback;12 |}13 |}14 |const el = {15 |tag,16 |props,17 |children,18 |};19 |return el;20 |},
- 여러 코드를 수정했는데요, 우선 Suspense 컴포넌트가 추가되었습니다.
- Suspense 컴포넌트는 비동기 함수와 키, 그리고 대체 UI를 props로 받습니다.
- 그리고 createResource 함수를 호출하여, 비동기 함수를 실행시키고, promise 객체를 전파합니다.
- React의 createElement 함수에서는 전파된 fallback도 함께 받아서, 대체 UI를 렌더링하도록 수정했습니다.
목표 완수!
- 이렇게 해서, Suspense 훅을 아주 간단한 형태로 구현해보았습니다.
- Suspense 기능을 구현해보면서, 리액트에서 비동기 작업을 처리하는 방법에 대해 조금 더 이해할 수 있었습니다.
index.html 파일입니다!<iframe/>을 이용해서 삽입 되어있습니다,이미 모든 비동기 작업이 완료되어서 아래에는 두개의 이미지가 보이실텐데요, 브라우져 새로고침으로 다시 한번 확인해 보세요!