자바스크립트 풀스택개발자인 Bojan Gvozderac가 작성한 React Image Lazy Loading Component 글로 폴리머(Polymer)의 iron-image 컴포넌트를 모방하여 리액트 컴포넌트로 구현한 튜토리얼을 참고해 웹사이트 개발에 적용한 내용이다.

미디엄(Medium) 프로그레스 이미지 로딩 효과를 django 블로그에 적용하기에서 구현한 프로그레스 이미지 로딩 효과와 동일하다. 리액트 컴포넌트에서는 어떻게 적용되는지 궁금했다.

Bojan Gvozderac의 로직은 간단하다. 용량이 큰 이미지를 로딩하는 동안 용량이 작은 사이즈를 보여주고, 원 이미지 로드가 완료되면 fade 효과로 이미지가 선명하게 보이는 시각적인 효과를 사용한다. 일종의 눈속임이다.

축소된 저용량 이미지가 없을 경우, 포토샵 등 이미지 편집툴을 사용해 원본 이미지의 width와 height를 1% 크기로 조정해서 이미지를 만들면 된다.

App.js

<IronImage /> 컴포넌트를 사용할 컨테이너가 되는 파일(예를 들면 App.js)에 컴포넌트를 불러오고 각 이미지 경로를 props로 전달한다.

import IronImage './IronImage'

//  주 이미지를 표시할 준비가 될 때까지 대체 이미지로 사용할 축소 이미지를 가져온다.
import smallImage from './background-small.jpg';

//  원 이미지를 가져온다
const ironImageHd = 'https://images.unsplash.com/photo-1478562853135-c3c9e3ef7905';

// 각 이미지 src를 props로 전달한다.
<IronImage srcPreload={smallImage} srcLoaded={ironImageHd} />

이미지가 public에 있다면 process.env환경 변수를 통해 이미지 파일 경로를 설정할 수 있다.

const image = `${process.env.PUBLIC_URL + "/background.jpg"}`

이미지 경로를 선언하기 위해 모듈로 가져오거나, 경로를 설정하는 변수를 만든다.

<IronImage/>

import React, { Component } from 'react';
import './IronImage.css';

class IronImage extends Component {

  constructor(props) {
    super(props);
    this.ironImageHd = null;
  }

  componentDidMount() {

    const hdLoaderImg = new Image();

    hdLoaderImg.src = this.props.srcLoaded;

    hdLoaderImg.onload = () => {
      this.ironImageHd.setAttribute(
        'style',
        `background-image: url('${this.props.srcLoaded}')`
      );
      this.ironImageHd.classList.add('iron-image-fade-in');
    }

  };

  render() {
    return (
      <div className="iron-image-container">

        <div 
          className="iron-image-loaded" 
          ref={imageLoadedElem => this.ironImageHd = imageLoadedElem}>
        </div>

        <div 
          className="iron-image-preload" 
          style={{ backgroundImage: `url('${this.props.srcPreload}')` }}>
        </div>

      </div>
    )
  }
}

export default IronImage;

컴포넌트는 간단하다.

먼저  <IronImag /> 컴포넌트에 대한 React 항목과 스타일을 가져온 다음, React 컴포넌트를 확장하여 컴포넌트를 작성한다.

생성자 호출에서는 ironImageHd를 초기화하고 로드가 완료되면 주 이미지를 렌더링 할 요소의 레퍼런스를 유지해야한다.

render() 를 보면 두 개의 <div /> 에서 이미지를 표시한다. 이미지를 표시할 때, <img />태그가 아닌 div의  css 속성으로 background-image 사용하면 작업하기 쉽고 일관성있게 사용할 수 있다는 장점이 있다.

미리보기용 이미지(srcPreload)를 props로 가져와 배경으로 표시한다. 이제 흐릿한 이미지로 보일 것이다.

두 번째 이미지는 약간 다르다. 메인 이미지가 즉시 표시 될 준비가 되지 않았기 때문에 배경을 풀 사이즈 이미지로 설정할 수 없다. 따라서 ref={...}를 사용해 풀 사이즈 이미지가 생기면 배경 이미지를 설정하고 사라지게 할 수 있다.

일반적으로 리액트는 단방향 데이터 흐름이다. 부모 컴포넌트와 자식과 컴포넌트가 상호 작용할 수있는 유일한 방법은  props이며, 자식을 수정하려면 새로운 props로 다시 렌더링해야한다. 예외로 외부의 자식을 수정해야하는 경우가 있다. 수정할 자식 요소는 리액트 컴포넌트 인스턴스이거나 DOM Element일 수 있다. 이러한 경우에 refs로 예외 처리를 할 수 있다. 그러나 매번 refs 남용해서는 안된다.

componentDidMount()에서 새 이미지 Image를 생성하고 경로(src)를 설정한다. 이미지가 로드되면 콜백을 추가한다. 이미지 로드가 끝나면 ironImageHd 요소의 배경 이미지를 설정하고 iron-image-fade-in 클래스를 추가해 이미지가 보여지게 된다. (opacity: 0에서 1로 변화된다)

CSS

.iron-image-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.iron-image-preload {
  position: absolute;
  z-index: 1;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
    filter:blur(5px);
  background-size: cover;
}

.iron-image-loaded {
  position: absolute;
  z-index: 2;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-size: cover;
  opacity: 0;
  transition: opacity 1s ease;
}

.iron-image-fade-in {
  opacity: 1;
}

프리로드 이미지 위에 전체 크기 이미지를 배치하고 컨테이너에 맞게 이미지를 늘린다. 전체 크기 이미지 div를 숨겨서 iron-image-preload가 로드가 완료 될 때까지 표시되고, 작업이 끝나면 불투명도가 1이 된다. transition 전환 시간을 1초로 하여 fade in 효과를 주었다.

fade-in 대신 새로운 애니메이션을 구현하거나,  특정 조건이 충족 될 때 이미지가 로드될 수 있게 처리해볼 수 있다.

https://sujinleeme.github.io/data-visualization-experiments/ 첫 화면에 컴포넌트를 적용했다. 이미지 로딩 중에도 끊김없이 부드럽게 전환이 되어 고해상도 이미지가 표시된다.