React에서 이미지 Pre Loading 처리 테스트 해보기

2024년 06월 08일

TOC

React

웹 페이지 개발을 하다 보면 이미지의 로딩에 의해 렌더링이 부자연스럽거나 끊기는 현상이 종종 발생한다. 이를 처리하기 위한 다양한 방법 중 프리 로딩(Pre Loading)을 예제를 통해 적용시켜 보았다.

Image Pre Loading

이미지 프리 로딩 (Image Pre Loading)이란 사용자가 실제로 요청하기 전에 미리 이미지를 다운로드하여 캐시에 저장하는 웹 성능 최적화 기술이다. 사용자가 페이지를 방문할 때 브라우저는 이미 캐시에 저장된 이미지를 로드하여 페이지 로딩 속도를 향상시킬 수 있다.

Pre Loading은 다음과 같은 장점을 제공한다.

  1. 로딩 속도 향상 : 사용자가 실제로 요청하기 전에 미리 처리하기 때문에 로딩 속도가 크게 향상된다. 이는 이미지가 많은 페이지에서 효과적일 수 있다.
  2. 사용자 경험 향상 : 빠른 로딩 속도는 사용자 경험을 향상시키고 사용자 참여도를 높일 수 있다.
  3. 데이터 사용량 감소 : 이미지가 이미 캐시에 저장되어 있으므로 사용자는 네트워크를 통해 이미지를 다시 다운로드할 필요가 없어 데이터 사용량을 줄일 수 있다.

하지만 다음과 같은 단점도 가지고 있다.

  1. 서버 부하 증가 : Pre Loading을 사용하면 서버에 추가적인 부하가 발생할 수 있다. 특히 많은 사용자가 동시에 이미지를 Pre Loading하는 경우 서버 성능에 영향을 미칠 수 있다.
  2. 캐시 공간 사용 증가 : Pre Loading된 이미지는 사용자 기기의 캐시 공간을 차지한다. 캐시 공간이 부족하면 다른 데이터를 저장하는 데 영향을 미칠 수 있다.

Pre Loading 방법, 고려 사항

Pre Loading 방법에는 다음과 같은 것들이 있다.

  1. <link rel="preload"> 태그 사용: HTML5에서 도입된 <link rel="preload"> 태그는 이미지, CSS, JavaScript 파일 등을 미리 로딩하도록 지시하는 데 사용된다.
  2. JavaScript 사용: JavaScript를 사용하여 이미지를 미리 로딩할 수 있다. Image 객체를 사용하여 이미지를 로드하고 onload 이벤트를 사용하여 이미지가 로드되었는지 확인할 수 있다.
  3. Server Pre Loading: 서버 측에서 이미지를 미리 로딩하고 응답 헤더에 Cache-ControlExpires 헤더를 사용하여 캐싱하도록 지시할 수 있다.

Pre Loading을 사용할 때는 다음 사항을 고려해야 한다.

  1. Pre Loading할 이미지 선택: 모든 이미지를 Pre Loading하는 것은 서버 부하와 캐시 공간 사용량을 증가시킬 수 있으므로 중요하거나 자주 표시되는 이미지만 Pre Loading하는 것이 좋다.
  2. 사용자 연결 속도 고려: 사용자의 연결 속도가 느린 경우 Pre Loading이 오히려 페이지 로딩 속도를 느리게 만들 수 있다. 따라서 사용자 연결 속도에 따라 Pre Loading 전략을 조정해야 한다.
  3. A/B 테스트 수행: Pre Loading이 실제로 페이지 로딩 속도와 사용자 경험에 어떤 영향을 미치는지 확인하기 위해 A/B 테스트를 수행하는 것이 좋다.

다음은 JavaScript를 사용한 Pre Loading 예시이다.

// Javascript
function preLoad(arr) {
  arr.forEach((url) => {
    const image = new Image()
    image.src = url
  })
}

preLoad(["a.png", "b.png", "c.png"])

preLoad 함수는 이미지 URL 배열을 받아 각 URL에 대해 Image 객체를 생성하고, 해당 URL을 src 속성에 할당하여 이미지를 로드한다. 이렇게 하면 사용자가 해당 이미지를 필요로 할 때 브라우저가 이미지를 캐시에서 빠르게 로드할 수 있다.

React 예제 만들기

이미지 Pre Loading의 효과를 확인하기 위해 많은 양의 이미지를 출력하는 웹 페이지를 작성하고 테스트해 보았다. 먼저 일반적으로 작성한 경우이다.

// React App
import { useState, useEffect } from "react"
import "./App.css"

function App() {
  const totalImages = 300
  const arrImg = Array.from(
    { length: totalImages },
    (_, index) => `https://picsum.photos/id/${index}/500/400`
  )

  const [num, setNum] = useState(0)
  const [isStarted, setIsStarted] = useState(false)

  useEffect(() => {
    let interval
    if (isStarted) {
      interval = setInterval(() => {
        setNum((prevNum) => {
          if (prevNum >= totalImages) {
            clearInterval(interval)
            return prevNum
          }
          return prevNum + 1
        })
      }, 30)
    }
    return () => clearInterval(interval)
  }, [isStarted, totalImages])

  // 로딩되지 않는 picsum 이미지는 1번 이미지로 대체
  const handleImageError = (e) => {
    e.target.src = "https://picsum.photos/id/1/500/400"
  }

  return (
    <div className="App">
      <div className="num-grid">
        <h1>PreLoad OFF</h1>
        <h1 className="num">{num}</h1>
      </div>
      <button className="button" onClick={() => setIsStarted(true)}>
        Start
      </button>
      <div className="image-grid">
        {arrImg.slice(0, num).map((src, index) => (
          <div key={index} className="image-container">
            <div className="image-number">{index + 1}</div>
            <img
              className="image"
              alt={index}
              src={src}
              onError={handleImageError}
            />
          </div>
        ))}
      </div>
    </div>
  )
}

export default App
// CSS
.App {
  text-align: center;
}

.num-grid {
  justify-content: center;
  display: flex;
}

.num {
  width: 100px;
  color: blue;
}

.button {
  width: 100px;
  height: 30px;
  font-size: 15px;
  margin: 0 20px;
}

.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
  gap: 10px;
  padding: 20px;
}

.image-container {
  position: relative;
  height: 56.6px;
}

.image-number {
  position: absolute;
  top: 3px;
  left: 3px;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  border-radius: 50%;
  width: 25px;
  height: 25px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: bold;
}

.image {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 10px;
}
preloadoff

이미지는 Lorem Picsum의 더미 이미지를 활용하였다.

Start 버튼을 누르면 1번 ~ 300번의 Picsum 이미지를 순차적으로 로드하는 방식이다. setInterval을 사용하여 일정 시간 간격으로 num 상태를 증가시키고, num에 따라 이미지를 로드한다. 위의 결과로 보다시피 로딩 시간이 걸려 렌더링이 지연되는 것을 볼 수 있다.

Pre Loading 적용 테스트

이제 이 코드에 Pre Loading을 적용시켜 보았다.

// React App
import { useState, useEffect } from "react"
import "./App.css"

function App() {
  const totalImages = 300
  const arrImg = Array.from(
    { length: totalImages },
    (_, index) => `https://picsum.photos/id/${index}/500/400`
  )

  const [num, setNum] = useState(0)
  const [isStarted, setIsStarted] = useState(false)
  const [preloadedImages, setPreloadedImages] = useState([])
  const [isPreloaded, setIsPreloaded] = useState(false)

  useEffect(() => {
    function preloadImages(arr) {
      const loadedImages = []
      let loadedCount = 0
      arr.forEach((url, index) => {
        const image = new Image()
        image.src = url
        image.onload = () => {
          loadedImages[index] = image
          loadedCount++
          if (loadedCount === arr.length) {
            setPreloadedImages(loadedImages)
            setIsPreloaded(true)
          }
        }
        // 로딩되지 않는 picsum 이미지 처리
        image.onerror = () => {
          const fallbackImage = new Image()
          fallbackImage.src = "https://picsum.photos/id/1/500/400"
          loadedImages[index] = fallbackImage
          loadedCount++
          if (loadedCount === arr.length) {
            setPreloadedImages(loadedImages)
            setIsPreloaded(true)
          }
        }
      })
    }

    preloadImages(arrImg)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  useEffect(() => {
    let interval
    if (isStarted) {
      interval = setInterval(() => {
        setNum((prevNum) => {
          if (prevNum >= totalImages) {
            clearInterval(interval)
            return prevNum
          }
          return prevNum + 1
        })
      }, 10)
    }
    return () => clearInterval(interval)
  }, [isStarted, totalImages])

  const handleImageError = (e) => {
    e.target.src = "https://picsum.photos/id/1/500/400"
  }

  return (
    <div className="App">
      <div className="num-grid">
        <h1>PreLoad {isPreloaded ? "ON" : "OFF"}</h1>
        <h1 className="num">{num}</h1>
      </div>
      <button
        className="button"
        onClick={() => setIsStarted(true)}
        disabled={!isPreloaded}
      >
        Start
      </button>
      <div className="image-grid">
        {preloadedImages.slice(0, num).map((image, index) => (
          <div key={index} className="image-container">
            <div className="image-number">{index + 1}</div>
            <img
              className="image"
              alt={index}
              src={image.src}
              onError={handleImageError}
            />
          </div>
        ))}
      </div>
    </div>
  )
}

export default App
preloadcache

컴포넌트가 마운트될 때 모든 이미지를 미리 로딩하여 preloadedImages 상태에 저장한다. isPreloaded 상태를 통해 모든 이미지가 로드되었는지 확인하고, 사용자가 버튼을 클릭하면 이미지를 순차적으로 표시한다.

preloadon

이미지 렌더링이 끊기지 않고 부드럽게 잘 나온다.

결과적으로 Pre Loading을 적용했을 때 확실히 이미지 로드 시 끊김 현상이 없음을 알 수 있다. 위와 같은 방법은 초기 로딩 과정이 어느 정도 필요하다. 따라서 이러한 방식을 어떻게 알맞게 사용하느냐에 따라 사용자 경험을 크게 향상시킬 수 있다.


Reference

Lorem Picsum

Image Preload (이미지 미리 불러오기)

React Image preload!

이미지 미리 로드하기 (Image Preload, useLayoutEffect)

Written by

yhuj79

🌱 Junior Developer