이미지는 웹사이트의 시각적 매력을 높이는 중요한 요소지만, 동시에 웹 성능에 큰 영향을 미친다. 브라우저에서 이미지를 표시하려면 이미지를 다운로드해야 하는데, 이미지 파일의 크기가 클수록 다운로드 시간이 길어져 페이지 로딩 속도가 지연될 수 있다. 특히, 이미지의 해상도와 용량이 사용자의 화면 크기와 요구를 초과할 경우, 불필요하게 큰 데이터 전송으로 인해 성능 저하가 발생한다.
대부분의 기기는 매우 고해상도를 필요로 하지 않는다. 따라서, 화면 크기에 비해 지나치게 큰 이미지를 사용하면 실제로 필요한 데이터보다 더 많은 용량을 다운로드하게 되어 로딩 속도가 느려질 뿐만 아니라 사용자가 콘텐츠를 확인하는 데까지의 시간이 길어질 수 있다.
이미지 최적화는 이러한 문제를 해결하는 효과적인 방법이다. 최적화를 통해 이미지 용량을 줄이면 브라우저가 데이터를 더 빠르게 다운로드할 수 있어 페이지 로딩 속도가 향상된다. 이는 사용자 경험을 크게 개선하며, 사이트를 방문하는 사용자들에게 더 빠르고 매끄러운 인터페이스를 제공한다.
또한, 최적화된 이미지는 서버에서 전송되는 데이터 양을 줄여 호스팅 비용과 대역폭 사용량을 절감할 수 있다. 특히, 방문자가 많은 웹사이트의 경우 최적화는 리소스를 효율적으로 관리하는 데 필수적인 역할을 한다. 결과적으로 이미지 최적화는 사용자 경험과 사이트 운영 비용 모두를 개선할 수 있는 중요한 전략이다.
클라이언트 내에 정적 이미지 파일을 로드할 때 제일 간단한 방법은 이미지 파일 자체의 픽셀 사이즈를 줄이는 것이다.
예를 들어, 화면에 렌더링되는 이미지의 크기가 100×100 픽셀이라고 가정해보자. 하지만 실제 이미지의 크기가 1000×1000 픽셀이면, 필요한 크기보다 넓이 기준으로 10배, 넓이와 높이를 곱한 면적 기준으로는 100배 큰 이미지를 사용하는 셈이다. 이렇게 불필요하게 큰 이미지를 사용하면 데이터 낭비와 함께 성능 저하를 초래한다.
일반적으로, 렌더링 크기(100×100 픽셀)로 이미지를 사용하는 것이 적합해 보이지만, 요즘 많이 사용되는 Retina 디스플레이에서는 한 공간에 더 많은 픽셀을 그릴 수 있으므로, 너비와 높이 기준으로 약 2배 큰 이미지를 사용하는 것이 적절하다.
즉, 위 예시의 경우 200×200 픽셀 크기로 이미지를 준비하는 것이 바람직하다. 이를 통해 디스플레이에서 품질 저하를 방지하면서도 불필요한 리소스 사용을 줄일 수 있다.
✅아래 converter사이트에서 이미지를 원하는 형식이나 사이즈로 바꿀 수 있다.
위에서는 클라이언트 내 정적 이미지 파일을 사용시에 대한 설명이었다. 반대로 서버에서 API 요청으로 받은 이미지의 사이즈는 어떻게 최적화 하면 좋을까? 그럴때는 CDN을 사용할 수 있다.
❓ CDN(Contents Delivery Network)
CDN은 물리적 거리의 한계를 극복하기 위해 소비자(사용자)와 가까운 곳에 컨텐츠 서버를 두는 기술을 의미한다.
한국에 있는 사용자가 미국에 있는 서버에서 이미지를 다운로드하려고 할 때, 인터넷 속도가 아무리 빨라졌다고 해도 물리적인 거리 때문에 다운로드에 시간이 걸릴 수 있다. 이를 해결하기 위해, 미국 서버의 데이터를 미리 한국 서버로 복사해 두면, 사용자가 이미지를 다운로드할 때 물리적 거리가 줄어들어 전송 시간이 크게 단축된다. 이러한 개념이 바로 CDN(Content Delivery Network)의 기본 원리이다.
그렇다면 이미지 CDN은 무엇일까? 이미지 CDN은 일반적인 CDN의 기능을 넘어, 이미지를 사용자에게 전송하기 전에 특정 요구 사항에 맞게 가공한다. 이미지 크기를 조정하거나 이미지 포맷을 변경한 뒤, 최적화된 형태의 이미지를 사용자에게 제공할 수 있다.

위 URL은 이미지 CDN의 예시이다. 전달하고자 하는 원본 이미지의 소스와 원하는 결과의 이미지 정보를 파라미터로 넘겨주면, 원본 이미지를 파라미터 형태로 가공을 해서 사용자에게 전달해준다.
이미지 CDN은 직접 구축하거나 이미지 CDN 솔루션을 사용해서 최적화 할 수 있다.
대표적인 이미지 CDN 솔루션 사이트는 아래 링크를 걸어놓을테니 참고해보면 좋을 거 같다.
파일 용량과 이미지의 품질 간의 균형을 고려하여 각 포맷(형식)을 적절하게 선택하면 이미지를 최적화할 수 있다.
대표적인 이미지 파일 확장자는 아래와 같다.
PNG : 투명도를 지원하는 비트맵 이미지 포맷이다. 손실 없는 압축 방식으로, 이미지 품질이 그대로 유지됩니다. 다만, 일반적으로 파일 크기가 JPEG보다 크기 때문에 복잡한 이미지에서는 비효율적일 수 있다.
JPEG : 사진과 같은 복잡한 이미지를 저장할 때 적합한 포맷이다. 손실 압축 방식으로, 이미지 품질을 유지하면서 파일 크기를 줄일 수 있다. 이미지의 투명도가 필요하지 않는 이상 보통은 JPEG 이미지를 사용하도록 권장한다.
WEBP : 구글에서 나온 차세대 이미지 포맷이다. JPEG, PNG, GIF보다 파일 크기가 작으면서도 더 좋은 품질을 유지할 수 있다. 손실 압축과 무손실 압축을 모두 지원하며, 대부분의 현대 브라우저에서 지원되긴 하지만 인터넷 익스플로어는 아직 지원되지 않는다.
AVIF: 최신 이미지 포맷으로, WebP보다 더 좋은 압축 성능을 제공한다. 손실 압축을 지원하며, 높은 이미지 품질을 유지하면서도 매우 작은 파일 크기를 구현할 수 있다. 그러나 아직 일부 구형 브라우저에서는 지원되지 않을 수 있다.
SVG : 벡터 형식의 이미지로, 크기를 확장해도 해상도가 손상되지 않는다. 주로 로고나 아이콘 등 선명한 라인으로 구성된 이미지에 사용된다. 파일 크기가 매우 작고, CSS나 JavaScript로 스타일을 변경할 수 있는 장점이 있다.
이미지 사이즈 비교시 아래와 같다.(JPEG 10MB 기준)
PNG(15MB ~ 20MB) > JPEG(10MB) > WebP(6.5MB ~ 7.5MB) > AVIF(5MB)
하지만 위에 설명에 나와있듯이 WEBP와 AVIF는 일부 구형 브라우저에서 지원되지 않을 수 있다.
WEBP

AVIF

그렇다면 WEBP나 AVIF가 지원되지 않는 브라우저에는 이미지를 아예 볼 수가 없을텐데, 이를 어떻게 해결할까? 그럴땐 이미지 분기가 필요하다. 예를들면 특정 브라우저가 WEBP가 지원되는 브라우저면 WEBP를 로드하고, WEBP가 지원되지 않는다면 JPG를 로드할 수 있도록 이미지 분기 처리가 필요하다. 이미지 분기 처리는 <picture> 태그를 사용하면 된다.
❓ <picture>태그
- HTML의
<picture>태그는 반응형 이미지를 제공하거나 상황에 따라 적합한 이미지를 선택할 수 있도록 도와주는 태그 이다.- 브라우저의 화면 크기, 픽셀, 또는 미디어 조건에 따라 서로 다른 이미지를 렌더링할 수 있도록 구성해준다. 성능 최적화와 다양한 디바이스 환경에서 적절한 이미지를 제공하는 데 유용하다.
<picture>
<source srcset="awesome_new_main.avif" type="image/avif">
<source srcset="new_main.webp" type="image/webp">
<img src="old_main.jpeg" alt="구형 이미지 포맷">// 마지막에는 구형 이미지 포맷을 모든 브라우저에서 사용할 수 있는 이미지 포맷으로 배치해야 함.
</picture>
위 코드는 브라우저가 지원하는 이미지 형식에 따라 최신 포맷(AVIF, WebP)을 우선 로드하고, 지원하지 않는 경우 호환성을 위해 기본 포맷(JPEG) 이미지를 로드하는 구조이다. <picture>태그를 사용하면 브라우저 환경에 따라 최적화된 이미지 포맷을 보여줄 수 있다.
웹사이트에 진입했을 때, 이미지가 늦게 뜨거나 위에서부터 천천히 로드되는 것을 본 적이 있을 것이다. 이는 이미지가 서버에서 로드되기까지 시간이 걸리기 때문으로, 사용자 경험(UX)에 부정적인 영향을 미칠 수 있다. 이러한 문제는 이미지 프리로딩(preloading)을 통해 최적화할 수 있다.
❓ 이미지 preloading
이미지가 화면에 노출되기 전에 미리 로드해두는 작업을 말한다. 이미지를 화면에 표시하지 않아도 특정 코드나 메커니즘을 통해 로드할 수 있다. 이렇게 하면 사용자가 해당 이미지를 보려는 시점에 로딩이 이미 완료된 상태가 되어 즉각적으로 표시된다.
✅ 리액트에서 이미지 프리로딩 방법
React에서는 useEffect 훅을 활용하여 컴포넌트가 마운트된 후 즉시 이미지 로드를 시작할 수 있다. 이미지를 로드하기 위해 JavaScript의 Image 객체를 사용하며, 로드 완료 및 실패 시 콜백을 추가해 상태를 추적할 수도 있다.
아래 코드를 참고해보자.
각각의 이미지 로드 완료 여부는 독립적으로 처리되며, 전체 로드 상태를 한 번에 관리하지 않기 때문에 이미지의 로드 상태를 개별적으로 처리하거나 단순히 로드 작업만 수행할 때 적합하다.
import React, { useEffect } from "react";
const App = () => {
const preloadImagesSequentially = (urls: string[]) => {
urls.forEach((url) => {
const img = new Image();
img.src = url;
img.onload = () => {
console.log(`로드 성공: ${url}`);
};
img.onerror = (error) => {
console.error(`로드 실패: ${url}`, error);
};
});
};
useEffect(() => {
const imageUrls = [
"https://example_1.jpg",
"https://example_2.jpg",
"https://example_3.jpg",
];
preloadImagesSequentially(imageUrls);
}, []);
return <div>이미지 프리로딩: 순차 처리</div>;
};
export default App;
Promise와 Promise.all을 활용해 병렬적으로 이미지를 로드하고, 모든 이미지가 성공적으로 로드되었을 때 한 번에 완료 처리를 할 수 있다. 모든 이미지가 로드된 후에 UI를 업데이트하거나 특정 작업을 진행해야 하는 경우에 적합하다.
import React, { useEffect } from "react";
const App = () => {
const preloadImagesWithPromise = (urls: string[]) => {
const loadImages = urls.map((url) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => {
console.log(`로드 성공: ${url}`);
resolve();
};
img.onerror = (error) => {
console.error(`로드 실패: ${url}`, error);
reject(error);
};
});
});
return Promise.all(loadImages); // 모든 Promise가 완료될 때까지 대기
};
useEffect(() => {
const imageUrls = [
"https://example_1.jpg",
"https://example_2.jpg",
"https://example_3.jpg",
];
const preload = async () => {
try {
await preloadImagesWithPromise(imageUrls);
} catch (error) {
console.error(`이미지 로드 오류 발생: ${error}`);
}
};
preload();
}, []); // 의존성 배열에서 preloadImagesWithPromise가 빠짐
return <div>이미지 프리로딩: 병렬 처리</div>;
};
export default App;
웹 사이트에 서버에 다운받아야 할 이미지 리소스가 1000개 정도라고 생각해보자. 그 많은 이미지를 한 번에 로드할 경우 초기 페이지 로드되는 시간이 꽤나 걸리며 사용자 경험이 느려질 수 있다. 이러한 문제는 이미지 레이지 로딩(lazy loading) 을 통해 최적화 할 수 있다.
❓ 이미지 lazy loading
이미지가 초기 페이지 로드 시점에 즉시 로드되지 않고, 사용자가 해당 이미지를 볼 가능성이 있는 시점에 로드되도록 하는 작업을 말한다. 화면에 보여지는 이미지만 우선 로드하므로 초기 로딩 시간이 단축되고, 사용자가 해당 이미지를 볼 필요가 없으면 네트워크 리소스를 아낄 수 있어 효율적이다.
lazy loading을 하는 방법은 대표적으로 아래 두가지가 있다.
Intersection Observer API는 요소가 뷰포트에 들어왔는지, 또는 다른 요소와 교차하는지를 비동기적으로 감지하는 API 이다. 이 API를 사용하여 요소가 화면에 보일 때(예: 이미지, 비디오 등) 해당 요소를 로드하거나 처리할 수 있다.
import React, { useEffect, useRef } from "react";
const App = () => {
const imageUrls = [
"https://example_1.jpg",
"https://example_2.jpg",
"https://example_3.jpg",
];
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
const handleObserver: IntersectionObserverCallback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;;
if (img.dataset.src) {
img.src = img.dataset.src; // data-src 속성에서 실제 이미지 URL을 가져옴
observer.unobserve(img); // 이미지가 보이면 더 이상 관찰하지 않음
}
}
});
};
const options = {
threshold: 0.1 // 10% 이상 보일 때 로드
};
observerRef.current = new IntersectionObserver(handleObserver, options);
return () => {
// 컴포넌트 언마운트 시 Observer 해제
observerRef.current?.disconnect();
};
}, []);
return (
<div className="img-container">
{imageUrls.map((image, index) => (
<div key={image}>
<img
data-src={image} // 실제 이미지는 data-src에 저장
alt={`Image ${index + 1}`}
width="300"
height="200"
/>
</div>
))}
</div>
);
};
export default App;
react-lazyload는 React에서 간편하게 이미지 또는 기타 콘텐츠에 대해 Lazy Loading을 구현할 수 있는 라이브러리 이다. 이 라이브러리는 Scroll 이벤트를 내부적으로 활용하여, 요소가 뷰포트에 들어왔을 때 자동으로 콘텐츠를 로드한다.
react-lazyload 라이브러리 설치npm install react-lazyload
import React from "react";
import LazyLoad from "react-lazyload";
const App = () => {
const imageUrls = [
"https://example_1.jpg",
"https://example_2.jpg",
"https://example_3.jpg",
];
return (
<div className="img-container">
{imageUrls.map((image, index) => (
<LazyLoad key={image} height={200} offset={100}> //height, offset 등의 옵션을 설정하여 더 세부적으로 제어할 수 있다.
<img src={image} alt={`Image ${index + 1}`} />
</LazyLoad>
))}
</div>
);
};
export default App;
지금까지 대표적인 이미지 최적화 기법들에 대해 살펴보았다. 각 상황에 맞게 이미지 최적화 기법을 적용한다면 웹 성능을 더욱 향상시킬 수 있을 것이다.
- https://www.cloudflare.com/resources/assets/slt3lc6tev37/6BVIJvRAQBrUfcZVyhY8hY/2a593ed9c28283fd0b81b07d52c01e64/Whitepaper_Getting-Faster-Know-your-website-know-whats-slowing-it-down_Korean_2021022.pdf
- https://caniuse.com/
- https://www.inflearn.com/course/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%A6%AC%EC%95%A1%ED%8A%B8-1
- https://www.inflearn.com/course/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%A6%AC%EC%95%A1%ED%8A%B8-2