[번역] 리액트로 점진적 이미지 로딩 구현하기: 튜토리얼

eunbinn·2022년 6월 5일
65

FE 번역

목록 보기
8/10
post-thumbnail

원문: https://blog.logrocket.com/progressive-image-loading-react-tutorial/

이미지는 웹사이트에 많은 영향을 줍니다. 그 존재만으로 사용자 경험을 증진시키고 더 몰입할 수 있게 하죠. 하지만 고화질의 이미지를 로딩하는 것은 시간이 걸리고 특히 인터넷이 느릴 경우 사용자들은 더 실망스러운 경험을 하게 될 수 있습니다.

이 문제를 해결하려면 개발자는 긍정적인 로딩 경험을 지원하는 전략을 전개해야 합니다. 그러한 전략 중 하나가 바로 점진적 이미지 로딩입니다.

이 튜토리얼에서는 점진적 이미지 로딩이 무엇인지, 이 전략을 리액트에서 어떻게 적용하는지 등에 대해 안내하려 합니다.

점진적 로딩이 유용한 이유

점진적 로딩을 사용하면 실제 이미지가 로드되기 전까지 저화질 혹은 미리 보기 이미지를 표시할 수 있습니다. 이는 이미지가 곧 보일 것임을 인식하게 함으로써 사용자 경험을 향상시킵니다.

아래 GIF는 기본 <img /> 요소를 사용하여 이미지를 렌더링 하는 모습을 보여줍니다.

보시다시피, 페이지는 이미 로드되었지만 이미지가 렌더링 되기까지 시간이 더 걸려 빈 공간이 발생하는 것을 확인할 수 있습니다. 인터넷 연결이 매우 느릴 때 이 경험은 더 악화됩니다.

점진적 로딩 기술을 사용하여 이미지의 작은 사이즈 버전을 렌더링 해 로드 시간을 줄일 수 있습니다. 고화질 버전이 로드되면 그때 이미지 파일을 교환합니다. 아래 GIF 데모를 참고하세요.

플레이스 홀더 이미지가 거의 즉시 로드되므로 이 전략은 웹 페이지 이미지로 인한 레이아웃 변경 문제를 줄이는 데에도 도움이 됩니다. 레이아웃 변경은 주로 브라우저가 이미지를 위해 예약할 공간을 인식하지 못하기 때문에 발생합니다.

이미지에 widthheight 속성을 추가함으로써 레이아웃 변경을 방지할 수 있습니다. 이는 브라우저에게 이미지가 차지할 공간을 예약할 수 있도록 미리 알려줍니다. 그 후에 이미지가 반응형 레이아웃에 올바르게 동작하게끔 CSS 파일의 image에 max-width: 100%height: auto 속성을 적용해야 합니다.

이 튜토리얼에서는 리액트에서 이미지를 점진적으로 로드하여 사용자 경험을 개선하고 레이아웃 변경을 방지하는 방법에 대해 알아봅니다. 또한 동일한 결과를 얻기 위해 외부 라이브러리를 사용하는 방법도 배울 것입니다.

이 튜토리얼을 진행하기 위해서는 리액트에 대한 실무 지식이 필요합니다.

리액트의 점진적 로딩 기술

점진적 이미지의 마법 같은 효과는 실제 이미지와 크기가 작은 버전의 이미지(보통 2kB 미만을 사용합니다)의 두 가지 이미지 버전을 만듦으로써 얻을 수 있습니다.

저화질 이미지는 빠른 표시를 위해 처음에 로드되고 메인 이미지가 다운로드되는 동안 메인 이미지 너비에 맞게 확대됩니다. 그 후 블러 효과와 적절한 CSS 트랜지션이 적용됩니다.

Gatsby와 Next.js와 같은 리액트 프레임워크도 이미지 컴포넌트에서 이 패턴을 사용합니다. 사용자가 직접 작은 버전의 이미지를 만드는 대신 프레임워크가 소스 이미지에서 자동으로 이미지를 생성합니다.

또한 이러한 프레임워크에서는 더 발전된 이미지 처리 옵션을 사용하고 화면 밑에 있는 이미지의 경우 지연 로딩을 지원합니다.

여기에선 리액트에서의 점진적 이미지 로딩에 초점을 맞춥니다. 그럼 이제 구현을 시작하겠습니다.

이미지 컴포넌트 만들기

ProgressiveImg라는 이미지 컴포넌트를 생성하여 <img/> 엘리먼트와 점진적 로딩 로직을 캡슐화할 것입니다. 이 컴포넌트는 기본 <img /> 엘리먼트를 대체할 수 있습니다.

const ProgressiveImg = ({ placeholderSrc, src, ...props }) => {
  return (
    <img
      {...{ src: placeholderSrc, ...props }}
      alt={props.alt || ""}
      className="image"
    />
  );
};
export default ProgressiveImg;

위 코드에서 컴포넌트는 실제 이미지 소스, 플레이스 홀더 소스, 그리고 이외 다른 props를 전달받습니다. 그 후 이 props를 <img /> 엘리먼트 속성에 할당합니다.

...전개 연산자를 사용하여 컴포넌트가 받는 다른 props를 주입한 방법에 주목하세요. 예를 들어 컴포넌트는 이미지의 widthheight를 전달받습니다. 그동안 앞서 언급했듯이 빠른 표시를 위해 src에 플레이스 홀더 이미지 소스를 할당합니다.

다음으로 위에서 언급한 props를 다음과 같이 <ProgressImg />에 전달합니다.

import ProgressiveImg from "./components/ProgressiveImg";
import image from "./images/large*.jpg";
import placeholderSrc from "./images/tiny*.jpg";

export default function App() {
  return (
    // ...
    <ProgressiveImg
      src={image}
      placeholderSrc={placeholderSrc}
      width="700"
      height="465"
    />
    // ...
  );
}

코드에서 볼 수 있듯이 이미지와 2kB 미만으로 크기를 조정한 작은 버전을 전달했습니다. 또한 레이아웃 이동을 방지하기 위해 이미지의 widthheight를 전달해야 합니다.

이미지 크기가 지정된 값보다 큰 경우 가로 세로 비율을 유지해야 합니다. 이를 통해 프런트엔드는 다음과 같아야 합니다.

썸네일을 실제 이미지로 업데이트하기

imgsrc를 업데이트하고 실제 이미지를 렌더링 하기 위해서는 useState 훅을 통해 이미지 소스를 상태 변수에 저장해야 합니다. 그 후 실제 이미지가 로드되면 useEffect 훅 내부의 변수를 업데이트할 수 있습니다.

다음과 같이 ProgressiveImg 컴포넌트를 업데이트해봅시다.

import { useState, useEffect } from "react";

const ProgressiveImg = ({ placeholderSrc, src, ...props }) => {
  const [imgSrc, setImgSrc] = useState(placeholderSrc || src);

  useEffect(() => {
    // 이미지를 업데이트 합니다.
  }, []);

  return (
    <img
      {...{ src: imgSrc, ...props }}
      alt={props.alt || ""}
      className="image"
    />
  );
};
export default ProgressiveImg;

img에 대한 src 속성은 이제 상태 변수의 값으로 할당되었습니다. 기본적으로 이 값은 플레이스 홀더 소스(있을 경우)로 설정됩니다. 그렇지 않으면 기본 이미지가 할당됩니다.

이제 다음과 같이 useEffect 훅을 업데이트해봅시다.

useEffect(() => {
  const img = new Image();
  img.src = src;
  img.onload = () => {
    setImgSrc(src);
  };
}, [src]);

이 훅에서는 Image() 객체를 인스턴스화하고 src 속성을 실제 이미지 소스로 세팅하여 img 엘리먼트를 생성하는 것으로 시작합니다.

이미지 객체의 onload 이벤트 핸들러를 사용하여 실제 이미지가 백그라운드에서 완전히 로드된 시점을 감지할 수 있습니다. 그 후 이미지 src를 실제 이미지로 업데이트합니다.

결과는 아래와 같습니다.

트랜지션 블러 구현하기

자연스러운 효과를 위해 CSS 트랜지션을 추가해 봅시다. ProgressiveImg 컴포넌트에서 return문 위에 다음 코드를 추가합니다.

const customClass =
  placeholderSrc && imgSrc === placeholderSrc ? "loading" : "loaded";

로드 상태에 따라 이미지에 클래스를 동적으로 추가합니다.

<img /> 가 커스텀 클래스를 갖도록 업데이트하겠습니다.

return (
  <img
    // ...
    className={`image ${customClass}`}
  />
);

만약 실제 이미지가 아직 로드 중이라면, 이미지에 loading 클래스를 추가합니다. 로드되었다면 loaded 클래스를 추가합니다. 그 후 다음 스타일 규칙을 포함하도록 CSS를 업데이트합니다.

.loading {
  filter: blur(10px);
  clip-path: inset(0);
}
.loaded {
  filter: blur(0px);
  transition: filter 0.5s linear;
}

저장 후 프런트엔드의 변화를 확인하세요. 전체 코드는 CodeSandbox에서 확인할 수 있습니다.

라이브러리를 사용한 점진적 이미지 로드

react-progressive-graceful-image 라이브러리를 사용하여 이미지를 점진적으로 로드할 수도 있습니다. 이를 사용하려면 먼저 라이브러리를 설치해야 합니다.

npm i react-progressive-graceful-image

그 후 ProgressiveImage 컴포넌트를 불러와 다음과 같이 구현할 수 있습니다.

import ProgressiveImage from "react-progressive-graceful-image";
import image from "./images/large_.jpg";
import placeholderSrc from "./images/tiny.jpg";

export default function App() {
  return (
    // ...
    <ProgressiveImage src={image} placeholder={placeholderSrc}>
      {(src, loading) => (
        <img
          className={`image${loading ? " loading" : " loaded"}`}
          src={src}
          alt="sea beach"
          width="700"
          height="465"
        />
      )}
    </ProgressiveImage>
    // ...
  );
}

ProgressiveImage 컴포넌트는 render props 기술을 사용하여 점진적 이미지 로딩을 구현합니다. 자식 함수 prop에서 렌더 콜백의 srcloading 인수에 접근 가능합니다.

loading 인수로 img 엘리먼트에 동적으로 클래스를 추가할 수 있습니다. 실제 이미지가 로딩 중 일 경우엔 loading이 true를 반환하고 그렇지 않을 경우엔 false를 반환합니다.

CSS 파일에 각 클래스의 스타일 규칙 또한 추가하였습니다. 전체 코드와 데모는 CodeSandbox에서 확인하실 수 있습니다.

결론

점진적 이미지 로딩 기술을 적용함으로써 리액트 프로젝트에서 사용자 경험을 크게 향상시킬 수 있었습니다.

이 튜토리얼에서는 외부 라이브러리를 사용하지 않고 리액트에서 이미지를 점진적으로 로드하는 방법에 대해 다뤘습니다. 이 가이드가 재밌고 도움이 됐기를 바랍니다.

3개의 댓글

comment-user-thumbnail
2022년 6월 7일

지연 로딩 말고도 점진적 로딩을 적용하면 훨씬 자연스러운 서비스 사용성 제공이 가능해지겠어요.
혹시 두가지 방법을 적절히 섞어서 사용하는 방법은 어떨까요?

답글 달기
comment-user-thumbnail
2022년 6월 8일

점진적 로딩에 tiny 한 이미지 파일이 필수인 듯 한데, 이미지를 따로 작은 용량으로 처리하는 방법이 있을까요 ??

답글 달기
comment-user-thumbnail
2022년 6월 14일

이 튜토리얼에서는 외부 라이브러리를 사용하지 않고 리액트에서 이미지를 점진적으로 로드하는 방법에 대해 다뤘습니다. 이 가이드가 재밌고 도움이 됐기를 바랍니다. https://gmailguide.io

답글 달기