웹 페이지 개발을 하다 보면 이미지의 로딩에 의해 렌더링이 부자연스럽거나 끊기는 현상이 종종 발생한다. 이를 처리하기 위한 다양한 방법 중 프리 로딩(Pre Loading)을 예제를 통해 적용시켜 보았다.
Image Pre Loading
이미지 프리 로딩 (Image Pre Loading)이란 사용자가 실제로 요청하기 전에 미리 이미지를 다운로드하여 캐시에 저장하는 웹 성능 최적화 기술이다. 사용자가 페이지를 방문할 때 브라우저는 이미 캐시에 저장된 이미지를 로드하여 페이지 로딩 속도를 향상시킬 수 있다.
Pre Loading은 다음과 같은 장점을 제공한다.
- 로딩 속도 향상 : 사용자가 실제로 요청하기 전에 미리 처리하기 때문에 로딩 속도가 크게 향상된다. 이는 이미지가 많은 페이지에서 효과적일 수 있다.
- 사용자 경험 향상 : 빠른 로딩 속도는 사용자 경험을 향상시키고 사용자 참여도를 높일 수 있다.
- 데이터 사용량 감소 : 이미지가 이미 캐시에 저장되어 있으므로 사용자는 네트워크를 통해 이미지를 다시 다운로드할 필요가 없어 데이터 사용량을 줄일 수 있다.
하지만 다음과 같은 단점도 가지고 있다.
- 서버 부하 증가 : Pre Loading을 사용하면 서버에 추가적인 부하가 발생할 수 있다. 특히 많은 사용자가 동시에 이미지를 Pre Loading하는 경우 서버 성능에 영향을 미칠 수 있다.
- 캐시 공간 사용 증가 : Pre Loading된 이미지는 사용자 기기의 캐시 공간을 차지한다. 캐시 공간이 부족하면 다른 데이터를 저장하는 데 영향을 미칠 수 있다.
Pre Loading 방법, 고려 사항
Pre Loading 방법에는 다음과 같은 것들이 있다.
<link rel="preload">
태그 사용: HTML5에서 도입된<link rel="preload">
태그는 이미지, CSS, JavaScript 파일 등을 미리 로딩하도록 지시하는 데 사용된다.- JavaScript 사용: JavaScript를 사용하여 이미지를 미리 로딩할 수 있다.
Image
객체를 사용하여 이미지를 로드하고onload
이벤트를 사용하여 이미지가 로드되었는지 확인할 수 있다. - Server Pre Loading: 서버 측에서 이미지를 미리 로딩하고 응답 헤더에
Cache-Control
및Expires
헤더를 사용하여 캐싱하도록 지시할 수 있다.
Pre Loading을 사용할 때는 다음 사항을 고려해야 한다.
- Pre Loading할 이미지 선택: 모든 이미지를 Pre Loading하는 것은 서버 부하와 캐시 공간 사용량을 증가시킬 수 있으므로 중요하거나 자주 표시되는 이미지만 Pre Loading하는 것이 좋다.
- 사용자 연결 속도 고려: 사용자의 연결 속도가 느린 경우 Pre Loading이 오히려 페이지 로딩 속도를 느리게 만들 수 있다. 따라서 사용자 연결 속도에 따라 Pre Loading 전략을 조정해야 한다.
- 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;
}
이미지는 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
컴포넌트가 마운트될 때 모든 이미지를 미리 로딩하여 preloadedImages 상태에 저장한다. isPreloaded 상태를 통해 모든 이미지가 로드되었는지 확인하고, 사용자가 버튼을 클릭하면 이미지를 순차적으로 표시한다.
이미지 렌더링이 끊기지 않고 부드럽게 잘 나온다.
결과적으로 Pre Loading을 적용했을 때 확실히 이미지 로드 시 끊김 현상이 없음을 알 수 있다. 위와 같은 방법은 초기 로딩 과정이 어느 정도 필요하다. 따라서 이러한 방식을 어떻게 알맞게 사용하느냐에 따라 사용자 경험을 크게 향상시킬 수 있다.