1. 개요
이미지는 웹페이지에 있어서 필수불가결한 요소이지만 그 크기가 클 경우에는 웹페이지의 로딩 성능을 떨어트리는 요소가 될 수도 있습니다. 또한 검색엔진 최적화(SEO, search engine optimization) 측면에서도 이미지 로딩이 다 끝나지 않아서 페이지의 색인이 제대로 되지 않으면 검색엔진이 웹페이지의 정보를 제대로 반영하지 못하는 문제가 발생할 수도 있습니다.
이 문제를 해결할 방법으로써 이미지 지연로딩 기법을 사용할 수 있습니다. 사용자가 보고 있는 화면의 이미지는 초기에 로딩하지만 사용자가 보지 않는 이미지의 로딩은 잠시 뒤로 미뤄두고 사용자의 요청에 따라 이미지가 보여져야 하는 순간에 이미지를 로딩함으로써 웹페이지의 선능을 개선시키는 방법입니다.
다시말해 다수의 큰 이미지 로딩이라는 비싼 비용의 작업을 한 번에 처리하지 않고 여러번에 나눠서 필요할때만 값을 치르는 방법이라고 하겠습니다.
2. 가이드
이미지 지연로딩을 구현하는 방법은 몇가지가 있습니다. 브라우저의 호환성을 고려하고 여러분의 상황에 맞는 패턴을 선택하시고 적용하면 됩니다.
아래에 나열된 방법들은 "어떻게하면 페이지 로딩을 조금이라도 더 빠르게 할 것인가?" 라는 물음에 답을 제시하기위해 위해 점진적으로 발전해온 패턴들이기도 합니다. 때문에 전통적인 이미지 지연로딩 방법들에서 남아있던 성능 문제는 무엇이었고 이것을 어떻게 해결하려고 시도했는지를 살펴보는 것도 도움이 되실 것입니다.
2.1. JavaScript를 이용해서 직접 구현하는 방법
JavaScript 를 이용해서 이미지 지연로딩을 구현하는 데 있어서 핵심은 아래의 두가지 입니다.
- 이미지 로딩을 지연시키기 위해 어떤 방법을 사용할 것인가?
- 이미지가 사용자에게 보여지는 시점을 어떻게 포착할 것인가?
이 두가지 측면에서 기능을 어떻게 구현했는지 예시와 함께 살펴보세요.
2.1.1. 이미지 로딩을 지연시키는 방법
2.1.1.1. <img> 태그를 이용하는 방법
<img> 의 src 속성을 부여하지 않음으로써 최초이미지 로딩을 차단하고 data-src 속성에 정보를 저장해 두었다가 이미지가 사용자게에 보여질 때 data-src의 정보를 src속성으로 이전함으로써 이미지를 로딩하고 사용자에게 보여주는 방식입니다.
1
2
|
<!-- src 속성이 없는 이미지 태그는 이미지를 로딩하지 않습니다.-->
|
(주의) 이미지의 width와 height를 명시하지 않고 지연로딩 패턴을 적용했을 때, 이미지가 로딩되면서 화면의 레이아웃이 의도치 않은 모습으로 틀어질 수 있습니다.
이러한 문제를 예방하기 위해 이미지의 width와 height를 지정해 주는 것이 좋습니다.
2.1.1.2. background-image CSS를 이용하는 방법
다음으로는 background-image CSS 이용하는 방식입니다. "background-image: none" 속성을 가진 스타일을 추가로 적용해 이미지 로딩을 차단하는 방식입니다.
아래의 예시에서는 lazy 클래스를 bg_image_example 클래스와 함께 사용해서 이미지로딩을 차단했다가 이미지가 보여야 하는 시점에 lazy 클래스를 제거함으로써 사용자에게 이미지를 로딩 한 뒤 노출시키는 방법입니다.
1
2
3
4
5
6
7
8
9
10
11
|
/*background-image: none 속성이 적용되면서 background-image에 정의된 이미지 로딩은 중단됩니다.*/
.bg-image-example.lazy {
background-image: none;
background-color: #F1F1FA;
}
.bg-image-example {
background-image: url("http://image/exmaple1.jpeg");
max-width: 600px;
height: 400px;
}
|
2.1.2. 이미지가 사용자에게 보여져야할 시점을 측정하는 방법
2.1.2.1. event와 window.pageYOffset 혹은 getBoundingClientRect() 이용하는 방법
scroll - Web API | MDN (mozilla.org)
resize - Web API | MDN (mozilla.org)
orientationchange - Web API | MDN (mozilla.org)
Window.pageYOffset - Web API | MDN (mozilla.org)
Element.getBoundingClientRect() - Web API | MDN (mozilla.org)
아래와 같이 지연로딩의 대상이 되는 이미지가 있다고 가정합니다. 이후의 예시에서는 img의 src 속성을 이용해서 이미지로딩을 지연시키는 패턴을 사용해서 이미지 지연로딩을 구현하는 방법에 대해서 설명하겠습니다.
1
|
|
위의 이미지를 조작하면서 지연로딩을 구현하는 예시는 아래와 같습니다.
화면의 변화를 감지하는 이벤트로서 scroll, resize 뿐만 아니라 orientationChange 이벤트도 포착해서 처리하는 것이 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var lazyloadImages = document.querySelectorAll("img.lazy");
function lazyload () {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
//viewport에 노출된 이미지는 data-src 정보를 src 속성으로 옮겨줌으로써 이미지를 로딩하게 됩니다.
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
//지연로딩이 완료되면 이벤트 핸들러를 해제해 side-effect가 발생하는 것을 차단합니다
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
|
(주의) scroll 이벤트는 처리비용이 큰 기능힙니다. 매우 민감하게 반응하는 이벤트이기때문입니다. scroll 이벤트를 다룰때는 throttle 을 적용하는 것이 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var lazyloadThrottleTimeout;
var lazyloadImages = document.querySelectorAll("img.lazy");
function lazyload() {
if (!lazyloadThrottleTimeout) {
//throttle을 적용해서 이벤트의 처리를 일정한 간격을 두고 실행하도록 합니다.
lazyloadThrottleTimeout = setTimeout(function () {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function (img) {
//viewport에 노출된 이미지는 data-src 정보를 src 속성으로 옮겨줌으로써 이미지를 로딩하게 됩니다.
if (img.offsetTop < window.innerHeight + scrollTop) {
img.src = img.dataset.src;
img.classList.remove("lazy");
}
});
//지연로딩이 완료되면 이벤트 핸들러를 해제해 side-effect가 발생하는 것을 차단합니다.
if (lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
lazyloadThrottleTimeout = null;
}, 100);
}
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
|
pageYOffset을 이용하지 않고 getBoundingClientRect() 메소드를 이용할 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
var lazyloadThrottleTimeout;
var lazyloadImages = document.querySelectorAll("img.lazy");
function lazyload() {
if (!lazyloadThrottleTimeout) {
//throttle을 적용해서 이벤트의 처리를 일정한 간격을 두고 실행하도록 합니다.
lazyloadThrottleTimeout = setTimeout(function () {
lazyloadImages.forEach(function (img) {
var rect = img.getBoundingClientRect();
//viewport에 노출된 이미지는 data-src 정보를 src 속성으로 옮겨줌으로써 이미지를 로딩하게 됩니다.
if (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth)
) {
img.src = img.dataset.src;
img.classList.remove("lazy");
}
});
//지연로딩이 완료되면 이벤트 핸들러를 해제해 side-effect가 발생하는 것을 차단합니다.
if (lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
lazyloadThrottleTimeout = null;
}, 100);
}
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
|
2.1.2.2. Intersection Observer API를 이용하는 방법
앞서 언급했듯이 scroll 이벤트는 처리 비용이 비싼 동작입니다.
그리고 getBoundingClientRect()메소드를 사용하는 것도 reflow를 발생시킬 가능성이 있다는 단점도 있습니다.
이것들은 모두 브라우저의 메인쓰레드에 부담이 되는 작업입니다.
이 문제를 해결하기 위해 등장한 것이 Intersection Observer API 입니다. Intersection Observer API 는 비동기방식으로 작동하기때문에 메인쓰레드에 부담을 적게 가져갑니다. 또한 IntersectionObserverEntry 의 속성을 이용하면 reflow를 발생시키지 않고도 getBoundingClientRect()을 사용하는 것과 같은 효과를 얻을 수 있습니다. 자세한 스펙은 아래의 링크를 참고해 주세요.
Intersection Observer API - Web API | MDN (mozilla.org)
Intersection Observer API를 사용하면 아래와 같이 지연로딩을 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var lazyloadImages = document.querySelectorAll("img.lazy");
var imageObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var image = entry.target;
image.src = image.dataset.src;
image.classList.remove("lazy");
imageObserver.unobserve(image);
}
});
});
lazyloadImages.forEach(function (image) {
imageObserver.observe(image);
});
|
intersectionObserver API의 브라우저 호환성을 아래의 표를 참고해서 확인한 다음에 적용하시기 바랍니다.
Chrome | 51 |
Edge | 15 |
Firefox | 55 |
Webkit | Safari 12.1 && iOS 12.2 |
"intersectionObserver" | Can I use... Support tables for HTML5, CSS3, etc
|
대부분의 경우 intersectionObserver API를 이용해서 효율적인 화면처리를 구현할 수 있습니다.
만약 intersectionObserver API를 지원하지 않는 환경도 고려해야 한다면 event를 이용한 지연로딩 처리와 병행해서 적용하거나, intersectionObserver의 pollyfill(링크)을 사용하면 됩니다.
2.2. 브라우저의 Native API를 사용하는 방법
웹용 브라우저 수준 이미지 지연 로딩 (web.dev)
chrome 브라우저와 chromium을 사용하는 브라우저들(Edge, Firefox 등, Firefox는 일부지원) 에서 지원하는 기능으로 img Tag의 loading 속성을 이용하는 방법이 있습니다.
1
2
|
<!--브라우저 Native 기능을 이용하면 아주 간편하게 지연로딩을 적용할 수 있습니다.-->
|
loading 속성은 아래의 값을 지원합니다.
- lazy: 뷰포트로부터 계산된 거리에 도달할 때까지 리소스 로딩을 지연시킵니다.
- eager: 페이지에서의 위치에 관계없이 리소스를 즉시 로드합니다.
intersectionObserver API보다 호환성은 낮은 편이므로 아래의 링크를 참고하시기 바랍니다.
"loading" | Can I use... Support tables for HTML5, CSS3, etc
3. 마무리
이상으로 지연로딩을 구현하는 방법에 대해 알아보았습니다.
지연로딩을 구현하면서 적용한 패턴은 infinite-scroll이나 scroll에 따라서 화면 속 요소들이 반응하는 동적인 화면을 만들때에도 유용하게 사용할 수 있습니다.
이미지가 많은 페이지의 성능을 높이기 위해 지연로딩(lazyloading)을 사용한다. 라는 것을 교과서적인 느낌으로 아는 것에서 더 나아가 코드레벨로 어떻게하면 구현할 수 있는지도 학습하면 좋을 것 같습니다.
최근댓글