[react] 리소스 로딩을 기다리는 커스텀훅 만들기

hoonsbory·2023년 3월 6일
2
post-thumbnail

리소스 로딩을 기다려야 하는 이유?

폰트나 이미지 등의 로딩이 되기전에 사용자는 미완성된 레이아웃을 마주하므로 좋지 않은 경험을 하게 됩니다.

폰트 로딩에 있어서 이런 현상을 FOIT, FOUT라고 합니다.

FOIT - Flash of Invisible Text

브라우저가 웹 글꼴을 다운로드하기 전에 텍스트가 보이지 않는 현상

FOUT - Flash of Unstyled Text

스타일이 지정되지 않은 텍스트의 플래시 - 대체 폰트(따로 정하지 않는다면 기본 폰트)가 출력되는 현상

이미지 또한 뒤늦게 로딩이 되면 layout shift가 발생할수도 있으며, 사용자는 이미지가 있는지 조차 모를 수도 있습니다.

위 현상들을 해결하기 위한 방안은 뭘까요?

대체로 로딩 전까지 글꼴대체, 숨기기, 스켈레톤 UI 적용 등의 방법이 있습니다.

오늘은 리소스 로딩이 될때까지 컴포넌트를 숨겼다가 나타내는 방법을 소개합니다.

결국 중요한 것은 리소스를 기다리는 것인데, 어떻게 효율적으로 훅으로 사용할 것인지 알아보겠습니다.




저는 폰트와 이미지를 기다리는 훅을 작성했습니다.

useLoading.tsx 전체 코드

import { useEffect, useState } from 'react';
let fontFace: Promise<void> | undefined;
const loadResources = (srcArr: string[]) => {
  if (!fontFace)
    fontFace = new Promise<void>((resolve, reject) => {
      const font = new FontFace(
        '폰트명',
        'url("폰트경로")',
      );
      font.load().then(() => {
        document.fonts.add(font);
        resolve();
      });
    });
  const promiseArr: Promise<void>[] = srcArr.map(src => {
    return new Promise<void>((resolve, reject) => {
      const image = new Image();
      image.src = `이미지경로`;
      image.onload = () => resolve();
      image.onerror = () => reject();
    });
  });

  return Promise.all([fontFace, ...promiseArr]);
};

const useLoading = (srcArr: string[] = []) => {
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    loadResources(srcArr).then(() => setLoaded(true));
  }, []);
  return loaded;
};

export default useLoading;

각 코드를 세세히 살펴보겠습니다.

각 리소스에 대한 로딩을 병렬로 처리하여 Promise를 반환하는 loadResources함수를 정의합니다.

fontFace = new Promise<void>((resolve, reject) => {
      const font = new FontFace(
        '폰트명',
        'url("폰트 경로")',
      );
      font.load().then(() => {
        document.fonts.add(font);
        resolve();
      });
    });

저는 모든 페이지에 공통으로 필요한 폰트 Promise를 먼저 정의했습니다.

FontFace 인스턴스를 생성하고 load 함수를 사용하여 폰트의 로딩이 끝나면 document에 폰트를 추가하고 Promise를 resolve 이행해줍니다.

그럼 이제 폰트 로딩을 기다리는 Promise가 생성됐습니다.

여기서 짚고 넘어가야할 것이, fontFace 변수를 두어 조건에 따른 대입연산을 하도록 작성한 것입니다.

캐싱에 대해 아는 사람이라면,

이미 캐싱된 리소스기 때문에 어차피 font.load를 해도 캐싱된 값을 쓰니까 딱히 조건문을 걸어줄 필요가 있나?

라는 생각을 할 수 있습니다. 맞는 말입니다.

그러나 매번 새로운 FontFace 인스턴스를 생성하면서 web apitask queue를 거치는 불필요한 로직을 실행하기 때문에 해당 promise를 변수에 캐싱합니다.

let fontFace: Promise<void> | undefined;

모듈은 최초 로드 시점에 캐싱되기 때문에, 어떤 컴포넌트에서 해당 모듈을 쓰든 fontFace 변수는 공유됩니다.

이제 폰트 로딩에 대한 준비는 끝났습니다.





다음은 이미지 리소스를 기다리는 Promise입니다.

const promiseArr: Promise<void>[] = srcArr.map(src => {
    return new Promise<void>((resolve, reject) => {
      const image = new Image();
      image.src =  src;
      image.onload = () => resolve();
      image.onerror = () => reject();
    });
  });

한 화면에 중요한 이미지를 2개 이상 로딩해야 할 수도 있으므로 배열로 생성했습니다.

map을 돌면서 Image객체를 생성(이미지 경로는 인수로 전달받는다)하고 로딩이 되면 resolve합니다.

return Promise.all([fontFace, ...promiseArr]);

완성된 폰트 Promise와 이미지 Promise 배열을 Promise.all로 병렬 처리합니다.

이제 리소스를 기다리는 Promise는 완성됐습니다.

const useLoading = (srcArr: string[] = []) => {
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    loadResources(srcArr).then(() => setLoaded(true));
  }, []);
  return loaded;
};

로드 유무를 판별할 state를 선언해줍니다.

마운트 후 loadResources함수를 기다린 후 loadedtrue로 바꿔줍니다.

이제 이 훅은 리소스 로딩전엔 false, 로딩 후엔 true를 리턴합니다.

TestComponent.tsx 마지막으로 사용법을 알아보겠습니다

import useLoading from '../../hooks/useLoading';

const TestComponent = () => {
  const imgArr = ['mainImage.png', 'mainImage2.png'];
  const isLoaded = useLoading(imgArr);
  return (
    <div>
      {isLoaded || <로딩중컴포넌트 />}
      {imgArr.map(img => (
        <img src={img} key={img} />
      ))}
    </div>
  );
};

export default TestComponent;

이미지 두 개가 로딩될 때까지 기다려야하는 TestComponent를 작성했습니다.

이미지 경로 배열을 훅에 넘기고 로딩 상태값을 받습니다.

로딩 상태에 따라서 로딩 중(false) 일 경우 로딩중임을 나타내는 컴포넌트를 렌더링합니다.

이 경우에 로딩 컴포넌트에 gif같은 것을 사용할 경우 로딩 시간이 있기 때문에, html,css만을 이용한 스피너를 만드는 것을 추천드립니다.

그리고 이미지 배열을 순회하며 img 엘리먼트를 생성합니다.

이미지의 경우 useLoading에서 이미지 객체를 생성하면서 캐싱하기 때문에, 실제 사용되는 TestComponent에서는 이미지 로딩이 따로 필요하지 않습니다. 이를 이용해 이미지 preload를 구현하기도 합니다.

이렇게 커스텀훅을 사용하면 기다리고자 하는 리소스를 다 로딩한 후에 화면을 노출하여 사용자 경험을 향상 시킬 수 있습니다.

1개의 댓글

comment-user-thumbnail
2023년 3월 6일

우와 굉장한 실력이시네요 !
하트 꾸욱 누르고 갑니닷

답글 달기