오프스크린 이미지 지연 로드(lazy loading) 하기

안녕하세요. 이번 시간에는 웹페이지의 로딩 속도를 향상하는 데 중요한 역할을 하는 '오프스크린 이미지 지연 로딩'에 대해 이야기하려고 해요.

 

이 방법은 사용자가 아직 보지 않은 이미지의 로딩을 지연시킴으로써 페이지의 초기 로딩 속도를 크게 향상시키고, 불필요한 네트워크 트래픽을 줄이는 데 도움이 되죠.

 

이미지 지연 로딩을 구현하는 방법에는 두 가지가 있어요. 하나는 브라우저의 내장 기능을 활용하는 방법이고, 다른 하나는 자바스크립트를 이용하는 방법이에요. 이 두 가지 방법을 적절히 조합하여 사용하면 다양한 브라우저 환경에서도 최적의 로딩 성능을 달성할 수 있답니다.

 

1. 브라우저 자체 기능 

브라우저에서 지원하는 기능이기 때문에 이미지 로딩을 쉽게 이미지 구현할 수 있어요. 

<img src="example.jpg" loading="lazy" alt="Example Image" width='300' height='300'>

위의 코드에서 loading="lazy" 속성은 브라우저에게 이 이미지의 로딩을 지연시키도록 지시해요. 따라서 이미지는 사용자가 그 이미지의 위치에 도달할 때까지 로드되지 않죠. 이렇게 하면 웹 페이지의 초기 로드 시간을 줄이고 성능을 향상시킬 수 있습니다. 

 

하지만 모든 브라우저에서 지원되지 않아요! 대부분의 최신 브라우저(Chrome, Edge, Opera, Firefox)에서 지원하고 있지만 Safari, Explorer는 지원하지 않고 있어요.

 

주의할 점은 이미 뷰포트영역에 있는 이미지는 loading= 'lazy'를 사용하지 않는 게 좋아요. 즉시 로드되어야 하는 이미지인데 브라우저는 이미지의 페이지 위치에 대해 알 때까지 기다려야 하기 때문이죠. 

 

티스토리 경우 본문 댓글의 프로필  이미지에 적용하면  됩니다. 본문은 치환자를 통해 불러오기 때문에  아래 자바스크립트 코드를 사용해 주세요.

 

2. 자바스크립트 추가

loading='lazy'는 아주 쉽게 구현이 가능하지만  제어의 한계가 있어요. 브라우저가 이미지 로딩을 언제 시작할지 결정하기 때문에, 개발자가 로딩 타이밍에 대한 세밀한 제어를 할 수 없죠. 또한 브라우저는 거리 임계값을 높게 설정 하기때문에 뷰포트에서 멀리 떨어진 이미지도 미리 로드돼요.

 

자바스크립트를 사용하면 뷰포트로부터 거리 임계값을 원하는대로 설정할 수 있어요. 즉 이미지의 로딩을 얼마나 미리 시작할 것인지를 정할 수 있어요. 

 

아래 영상은 티스토리 블로그에 오프스크린 이미지 지연로딩을 적용한 거예요. 스크롤을 내릴 때마다 이미지들이 실시간으로 로딩되어 나타나게 되죠. 이로써 초기 로딩시간을 줄이고 대역폭을 절약할 수 있기 때문에 성능이 향상돼요.

 

 

자, 이제 자바스크립트를 사용하여 이미지 지연 로딩 코드를 만들어 볼게요. 이 코드는 티스토리를 기준으로 작성되었지만, 다른 웹 페이지에서도 사용 가능해요. 다른 웹 페이지에서 사용하려면 document.querySelectorAll('img id') 부분과 지연 로딩 중 대체 이미지를 설정하는 부분인 imageElement.src="이미지 url" 부분만 수정하면 됩니다.

// 해당 페이지의 HTML이 로드되면 실행
document.addEventListener("DOMContentLoaded", function() {
    // 이미지가 로딩 중인지 아닌지를 확인하는 플래그
    var isLoading = false;

    // 이미지를 로드하는 함수
    var loadImage = function(imageElement) {
        // 이미지가 이미 로드되었는지 확인
        if (!imageElement.classList.contains("loaded")) {
            // 데이터 속성에 저장된 실제 이미지 URL을 img 요소의 src 속성에 설정
            imageElement.src = imageElement.dataset.src;

            // 이미지 URL이 로드되었으므로 더 이상 필요하지 않은 data-src 속성을 삭제
            imageElement.removeAttribute("data-src");

            // 이미지가 srcset 속성을 가지고 있다면
            if (imageElement.dataset.srcset) {
                // 데이터 속성에 저장된 실제 이미지 URL을 img 요소의 srcset 속성에 설정
                imageElement.srcset = imageElement.dataset.srcset;

                // 이미지 URL이 로드되었으므로 더 이상 필요하지 않은 data-srcset 속성을 삭제
                imageElement.removeAttribute("data-srcset");
            }

            // 이미지가 로드되었다는 것을 표시하기 위해 loaded 클래스 추가
            imageElement.classList.add("loaded");
        }
    };

    // 이미지를 로드하기 전에 준비하는 함수
    var prepareImage = function(imageElement) {
        // img 요소의 src 속성을 data-src 속성으로 복사
        imageElement.dataset.src = imageElement.src;

        // img 요소가 srcset 속성을 가지고 있다면
        if (imageElement.srcset) {
            // img 요소의 srcset 속성을 data-srcset 속성으로 복사
            imageElement.dataset.srcset = imageElement.srcset;

            // 더 이상 필요하지 않은 srcset 속성을 삭제
            imageElement.removeAttribute("srcset");
        }

        // 임시 이미지 URL로 img 요소의 src 속성을 설정
        imageElement.src = "https://stat.tiara.tistory.com/track";
    };

    // 브라우저가 Intersection Observer API를 지원하는 경우
    if ("IntersectionObserver" in window) {
        // 이미지가 화면에 들어올 때 로드하는 Intersection Observer를 생성
        var observer = new IntersectionObserver(function(entries, observerInstance) {
            entries.forEach(function(entry) {
                // 이미지가 화면에 들어오지 않았다면 무시
                if (!entry.isIntersecting) {
                    return;
                }

                var imageElement = entry.target;

                // 이미지 로드
                loadImage(imageElement);

                // 이미지 로드가 완료되었으므로 해당 이미지의 관찰을 중지
                observerInstance.unobserve(imageElement);
            });
        }, {
            root: null,
            rootMargin: "200px"
        });

        // ".imageblock img" 및 ".imagegridblock img" 셀렉터에 해당하는 모든 이미지 요소들에 대해
        document.querySelectorAll(".imageblock img,.imagegridblock img").forEach(function(imageElement) {
            // 이미지 로드 전처리
            prepareImage(imageElement);

            // 이미지 관찰 시작
            observer.observe(imageElement);

            // 이미지가 관찰 중임을 나타내는 클래스 추가
            imageElement.classList.add("observing");
        });
    } else {
        // Intersection Observer API가 지원되지 않는 경우, 스크롤 이벤트를 사용해 이미지 로드
        var checkVisibilityAndLoad = function() {
            // 현재 스크롤 위치 가져오기
            var scrollPosition = window.scrollY;

            // ".imageblock img" 및 ".imagegridblock img" 셀렉터에 해당하는 모든 이미지 요소들에 대해
            document.querySelectorAll(".imageblock img,.imagegridblock img").forEach(function(imageElement) {
                // 이미지가 이미 로드되었다면 무시
                if (imageElement.classList.contains("loaded")) {
                    return;
                }

                // 이미지의 부모 요소가 페이지 상단으로부터 얼마나 떨어져 있는지 계산
                var parentTopPosition = imageElement.parentNode.offsetTop;

                // 이미지가 현재 보이는 영역에 있는지 확인하고, 만약 그렇다면 이미지 로드
                if (parentTopPosition + imageElement.offsetHeight > scrollPosition && scrollPosition + window.innerHeight > parentTopPosition) {
                    loadImage(imageElement);
                }
            });
        };

        // ".imageblock img" 및 ".imagegridblock img" 셀렉터에 해당하는 모든 이미지 요소들에 대해
        document.querySelectorAll(".imageblock img,.imagegridblock img").forEach(function(imageElement) {
            // 이미지 로드 전처리
            prepareImage(imageElement);

            // 스크롤 이벤트가 발생할 때 이미지가 보이는지 확인하고 보이는 이미지 로드
            window.addEventListener("scroll", function() {
                if (!isLoading) {
                    window.requestAnimationFrame(function() {
                        checkVisibilityAndLoad();
                        isLoading = false;
                    });

                    isLoading = true;
                }
            }, {
                passive: true
            });
        });
    }
});

 

브라우저가 Intersection Observer API를 지원한다면 이를 활용하고, 그렇지 않은 경우에는 스크롤 이벤트를 이용해요. 이로 인해 코드가 약간 길게 보일 수 있지만, 주석을 제거하고 가독성을 위해 추가한 줄 바꿈을 없애면 실제로는 훨씬 간결해집니다!

 

티스토리 블로그는 </body> 바로 윗줄 <script>여기</scritp> 에 넣어주시면 됩니다.  - 끝 -

오프스크린 썸네일