Tistory View

web/javascript

HTML Image lazy loading[느린 로딩] #2

God Dangchy What should I do? 2020. 12. 14. 07:00

#1편에서 언급했듯이 img태그를 쓰지 못하는 경우가 있다. 특히 img태그를 쓰지 않고 이미지를 화면에 표시하는 방법을 쓰는 경우가 있다. 예를 들면 div태그에 background-image를 이용하여 표시하는 경우가 이에 해당한다. 당연히 이 div태그에는 loading속성은 존재하지 않기에(css도 없는 듯) Lazy-Loading은 javascript로 구현해 낼 수 밖에 없다. 이로인해 img태그의 loading속성은 무용지물이 되지만, 그래도 javascript로 만들면 제어가 가능하기에 오히려 장점도 많으니 이 방법을 이용해 보자.

 

 

시나리오는 다음과 같다.

[사용자가 "스크롤"할 때마다~]

1. 페이지내의 "lazy"가 있는 모든 요소를 가지고 온다. 이 때 이 요소에는 data-imgsrc속성에 로드될 이미지 위치를 미리 넣어둔다.

2. 이 요소들 중에 뷰포트내의 요소만 걸러낸다.

2. 새로운 img태그를 만들어 이미지를 로드한다.

3. 이미지가 로드되면 해당요소에 background-image로 이미지 그린다.

 

위의 시나리오를 구현하려면 대충 다음의 함수들을 미리 만들어 두자.

 

뷰포트를 구하는 함수

function getViewPort() {
		
	var left, top, right, bottom;
	
	var d = document;
	var r = d.documentElement;
	var b = d.body;
		
	left   = r.scrollLeft || b.scrollLeft || 0;
	top    = r.scrollTop || b.scrollTop || 0;
	right  = left + window.innerWidth;
	bottom = top  + window.innerHeight;
	
	return { "left": left, "top": top, "right": right, "bottom": bottom };
}

 

요소의 위치와 크기를 구하는 함수

function getBoundingClientRectOnDom( elm ) {

	var domRect = elm.getBoundingClientRect();

	var scrollPos = getViewPort();
	
	
	return {
		"left"  : domRect.left   + scrollPos.left,
		"right" : domRect.right  + scrollPos.left,
		"top"   : domRect.top    + scrollPos.top,
		"bottom": domRect.bottom + scrollPos.top
	};
}

[요소].getBoundingClientRect함수는 현재의 뷰포트의 왼쪽상단을 기준으로 값을 돌려주기 때문에, 페이지내의 위치를 구하려면 뷰포트의 위치만큼 이동을 시켜줘야 해서 아예 함수로 따로 만들었다.

 

 

 

화면내에 이미지가 있는 지 검사하기위해 겹침을 테스트하는 함수

function isIntersect( b1, b2 ) {
		return (
          ( 
          ( b1.left   >= b2.left && b1.left   <= b2.right  )
          ||
          ( b1.right  >= b2.left && b1.right  <= b2.right  )
          ||
          ( b1.left   <= b2.left && b1.right  >= b2.right  )
          )
        && 
        ( 
          ( b1.top    >= b2.top  && b1.top    <= b2.bottom ) // 1, 2
          ||
          ( b1.bottom >= b2.top  && b1.bottom <= b2.bottom ) // 3
          ||
          ( b1.top    <= b2.top  && b1.bottom >= b2.bottom ) // 4
          )
        );

}

상당히 복잡해 보이지만 그림으로 보면 그리 어렵지 않다.

총 4가지 경우가 존재하는 데, 4가지 경우는 다음과 같다. 세로만 보자(가로도 마찬가지)

경우 1. 이미지가 내부에 있는 경우, 이미지의 top이 Viewport내에 있는 경우

경우 2. 1과 동일하다 (따라서 1의 경우만 체크하면 된다)

경우 3. 이미지의 bottom이 Viewport내에 있는 경우

경우 4. 이미지가 아예 전체를 덮고 있는 경우(빼먹지 말자)

 

참 쉽죠!

 

 

 

 

 

lazy loading될 태그는 보통 다음과 같이 생겼다.

<div class="lazy" data-imgsrc="/불러올이미지1" style="width:160px;height:160px;" ></div>

#1편에서도 언급했듯이 이 경우도 layout이 잡혀있어야 하기때문에 html코드를 생성할 때 화면에 정확한 위치를 잡아 둔다. width, height를 쓴 이유다.

 

 

 

이제 scroll 이벤트를 걸자

document.addEventListener("DOMContentLoaded", function(event) {
	
    // fade-in을 위해 opacity를 전부 0으로 만든다.
    var elms = document.querySelectorAll( ".lazy" );
    for( var i = 0 ; i < elms.length ; i++ ) {
    	let elm = elms[i];
    	elm.style.opacity = '0';
    }

    
    window.addEventListener( "scroll", function( evt ) {
    	loadLazyImage();
    }, false );

	window.addEventListener( "resize", function( evt ) {
    	loadLazyImage();
    }, false );

	window.addEventListener( "orientationchanged", function( evt ) {
    	loadLazyImage();
    }, false );
    
    
	//처음 화면이 로드된 경우 현재화면에 이미지를 불러오기위해
	loadLazyImage();
});

단순히 lazyload 함수를 호출한다.

따로 분리한 이유는 lazyload함수가 페이지가 맨 처음 열릴 때 호출해줘야 화면에 이미지를 불러오기 때문이다. 맨 처음 열릴 때 'scroll' 이벤트가 발생되지 않을 테니...

게다가 화면 크기가 변경되거나, 스마트폰을 돌렸을 때에도 같은 작업을 해야 하기 때문에 추가를 했다.

(orientationchange가 호출되면 resize도 같이 호출 되니 필요 없기는 한데...)

 

loadLazyImage함수

function loadLazyImage() {
	
	var elms = document.querySelectorAll( ".lazy" );
	var viewport = getViewPort();
	
	for( var i = 0 ; i < elms.length ; i++ ) {
		
		let elm = elms[i];
		var uri = elm.getAttribute('data-imgsrc');
		var bound = getBoundingClientRectOnDom( elm );
		
        // 이미 로드된 경우 이 값이 없다.(1)
		if( !uri ) { 
			continue;
		}
		// 뷰포트내에 있는 지를 체크
		if( !isIntersect( viewport, bound ) ) {
			continue;
		}
		
        // 이게 없으면 스크롤 될 때마다 계속 로드된다.(1)과 연동된다.
		elm.removeAttribute('data-imgsrc');
		// 가상에 이미지를 만든다.
		let img = document.createElement('img');
		
		img.addEventListener( 'load', myHandlerOnImageLoad, false );
		img.src = uri;  // 이미지 로드
		img.elmDiv = elm; //해당하는 요소를 링크해 둔다.
	}
}

맨 위 부분에서 설명한 시나리오 대로 동작하는 코드다.

 

마지막으로 이미지가 로드 되었을 경우 화면에 그리는 기능

function myHandlerOnImageLoad( evt ) {
	
	var img = evt.target;
	var elm = img.elmDiv;
	
	// opacity를 바꾸고
	elm.style.opacity = 1;
	// transition 적용
	elm.style.transition = 'opacity .5s ease-out';
	
    // 이미지를 배경으로 그려 버린다.
	elm.style.backgroundImage = "url('" + img.src + "')";
	
}

 

끝~

 

이면 좋지만, 좀 맘에 안드는 부분을 수정해보자

화면에 lazy-load할 그림에 1000개면 그림이 다 로드되어도 [scroll]이벤트가 발생할 때마다 1000번씩 루프를 돈다.

이미 로드된 이미지는 제외하도록 loadLazyImage함수의 코드를 수정한다.

function loadLazyImage() {
	
	//var elms = document.querySelectorAll( ".lazy" );
	// class에 lazy가 있는 것 중 속성에 'data-imgsrc'가 있는 것
	var elms = document.querySelectorAll( ".lazy[data-imgsrc]" );
			
	var viewport = getViewPort();
			
	for( var i = 0 ; i < elms.length ; i++ ) {
		
		let elm = elms[i];
		var uri = elm.getAttribute('data-imgsrc');
		var bound = getBoundingClientRectOnDom( elm );
					
		// data-imgsrc가 존재하는 것만을 가지고 오니 이 코드는 필요없다.
		//if( !uri ) { 
			//continue;
		//}
		
		if( !isIntersect( viewport, bound ) ) {
			continue;
		}
		
        // 이 코드로 인해 다음번 호출시 css selector에 걸리지 않게 된다.
		elm.removeAttribute('data-imgsrc');
		
		// 가상에 이미지를 만든다.
		let img = document.createElement('img');
		
		img.addEventListener( 'load', myHandlerOnImageLoad, false );
		img.src = uri;  // 이미지 로드
		img.elmDiv = elm;
	}
}

 

동작하는 화면은 다음의 링크를 참조하길 바란다. 서버가 잘 동작할 지는...

jamesphk.iptime.org:8084/~blog/lazy_load/lazy_img2.php

 

 

 

 

 

 

이 정도로 끝나면 정말 좋겠지만, "악마는 디테일에 있다"고 Viewport내에 이미지가 들어와야 로드가 되고 그로 인해 이쁘지만 현란한 화면이 되어 버린다. Viewport 근처에 있는 것을 미리 로드하면 마치 처음에 다 로드한 듯한 효과를 볼 수 있다.

이 내용을 적용한 것은 #3편에 계속된다.

 

 

Replies
Reply Write