Tistory View

web/javascript

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

God Dangchy What should I do? 2020. 12. 16. 08:20

이제 화면 내부만이 아닌 화면의 근처에 있는 이미지를 미리 로드하는 코드를 추가하도록 해보자. 트래픽과 시스템리소스는 조금 더 먹겠지만, 사용자에게 가장 깔끔하게 보여지는 것이 더 중요할 것이다. 이왕하는 김에 다른 내용도 추가한다.

 

 

할 일

1. 스크롤시 화면근처에 있는 이미지를 로드한다.

2. 이미지가 로드되면 화면 내부(화면에 조금이라도 겹친다면)일 경우 fade-in처리를 하지만, 화면 바깥이라면 그냥 그린다.

3. (추가로) 기본 이미지를 미리 깔아 놓고, 로드된 이미지를 위에 덮어 그리기를 한다.(로딩중 같은 이미지에 적용하면 좋을 듯하다)

 

어디까지 로드할 것인가?

필자가 테스트한 바로는 현재 보고있는 화면크기에서 위로 한화면, 아래로 한화면이 가장 적당한 듯 했다. 마우스로 스크롤하기도 하지만 키보드의 페이지 업/다운으로 할 경우 조금씩 다르기는 하지만 한화면씩 이동한다. 빠르게 스크롤하는 것은 제외되고, 사용자는 현재위치의 화면을 보는 동안 다음화면 로드되기에 가장 적당하다.

 

 

관련함수를 추가하자

function getToLoadBoundRect( fX, fY ) {
	
	var v = getViewPort();
	var w = v.right  - v.left;
	var h = v.bottom - v.top ;
	
	var l = v.left   - fX * w;
	var r = v.right  + fX * w;
	var t = v.top    - fY * h;
	var b = v.bottom + fY * h;
	
	return { 'left': l, 'right' : r, 'top' : t, 'bottom': b };
}

위 함수는 ViewPort를 기준으로 상하좌우로 크기를 넓히는 함수다.

fX와 fY는 얼마나 넓힐 것인가를 정하는 값으로 0을 넣으면 Viewport와 같고, 1을 넣으면 상하, 좌우로 각각 Viewport만큼 커지게 된다.

 

fX, fY 값에 따른 변화, 검은색 Viewport, 빨간색 로드할 범위

이 빨간색 영역과 조금이라도 겹친다면, 이미지를 로드하도록 변경한다.

function loadLazyImage() {
	
	// class에 lazy가 있는 것 중 속성에 'data-imgsrc'가 있는 것
	var elms = document.querySelectorAll( ".lazy[data-imgsrc]" );
	
			
	//var viewport = getViewPort();     // <----------------------------------------- 삭제
	// 가로스크롤이 없는 상황으로 가정하여 세로로만 1화면크기를 더 추가 한다.
	var rectToLoad = getToLoadBoundRect( 0, 1 ); //  <------------------------------- 변경
			
	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( rectToLoad, bound ) ) {      // <-------------------------- 번경
			continue;
		}
		
		// 이 코드로 인해 다음번 호출시 css selector에 걸리지 않게 된다.
		elm.removeAttribute('data-imgsrc');
		
		// 가상에 이미지를 만든다.
		let img = document.createElement('img');
		
		img.addEventListener( 'load', myHandlerOnImageLoad, false );
		img.src = uri;  // 이미지 로드
		img.elmDiv = elm; //해당하는 요소를 링크해 둔다.
	}
}

 

할일 2

이미지가 로드된 경우의 코드도 수정한다.

function myHandlerOnImageLoad( evt ) {
	
	var img = evt.target;
	var elm = img.elmDiv;
	
	var viewport = getViewPort();
	var bound    = getBoundingClientRectOnDom( elm );
	
	
	// 이미지가 화면내에 있다면 transition 아닌경우 그냥 그려진다.
	if( isIntersect( viewport, bound ) ) {
		elm.style.transition = 'opacity .5s ease-out';
	}
	
	elm.style.opacity = 1;
	elm.style.backgroundImage = "url('" + img.src + "')";
	
}

이제 사용자가 아주 빠르게 스크롤하지 않는 이상 일반 페이지가 그냥 동작하는 듯 하게 보일 것이다.

 

기본이미지 깔고 느린로딩

<div class="def_img" style="width:160px;height:120px;">
	<div class="lazy"
		data-imgsrc="느린로딩할 이미지 경로"
		style="width:160px;height:120px;"
	></div>
</div>

def_img가 붙어있는 녀석에 배경으로 기본이미지를 깔 것이다. 내부에 div는 우리가 이 글들에서 늘 써 왔던 녀석이다.

 

문제는 기본이미지가 보여줄 이미지보다 늘 더 빨리 로드되어야 한다는 것이다. 보여줄 이미지가 더 빨리 로드되면 그냥 보여줘 버리면 끝이기 때문에, 기본이미지를 무조건 더 빨리 가지고 와야 하는 데, 기본이미지를 코드상에서 먼저 가지고 오게해도 이미지의 크기나 네크워크 상황에 따라 어떤 것이 먼저 로드될지는 모르는 것이다. 물론 기본이미지를 다 로드한 다음부터 lazy로딩을 할 수도 있지만, 자칫 잘못하면(기본이미지를 못 가지고오거나) 아예 동작하지 않을 수도 있다.

 

그래서 기본이미지를 아예 HTML코드에 넣어 버리도록 한다.

<img id="def_back" src="data:image/jpeg;base64,[이미지데이터]" style="display:none;" />

 

DOMContentLoaded에 다음의 코드를 추가하자

document.addEventListener("DOMContentLoaded", function(event) {


	var defImg = document.getElementById('def_back');
	
	var elmTargets = document.querySelectorAll( ".def_img" );
	for( var i = 0 ; i < elmTargets.length ; i++ ) {
		let elm = elmTargets[i];
		elm.style.backgroundImage = "url('" + defImg.src + "')";
	}

      .
      .
      .
});

 

 

 

 

이 예제는 기본이미지를 1개만 쓰지만 각각 다른 이미지를 사용할 경우 html파일이 그만큼 커지게 되기 때문에 비효율 적이게 된다. 그런 경우는 되도록 만들지 않는 것이 좋지만, 그래야 할 경우, html에 넣지말고, 그냥 로딩하는 것이 더 좋을 것이다.

 

 

갑자기 사장이 왔다. 페이지가 처음 로드될 때 보여지는 그림은 fade-in없이 보여졌으면 한단다.

 

 

초기에 보여지는 이미지에 초기화면이라는 값을 설정해두고, 이미지 로드시 초기화면이면 그냥 보여지도록 변경해보자

또, 초기에 보여지는 이미지는 기본 배경을 깔 지 않도록 한다.

document.addEventListener("DOMContentLoaded", function(event) {


	var defImg = document.getElementById('def_back');
	var viewport = getViewPort();
	
	var elmTargets = document.querySelectorAll( ".def_img" );
	for( var i = 0 ; i < elmTargets.length ; i++ ) {
		let elm = elmTargets[i];
		let b = getBoundingClientRectOnDom( elm );
		
        
		if( !isIntersect( viewport, b ) ) {
			elm.style.backgroundImage = "url('" + defImg.src + "')";
		}
	}



	var elms = document.querySelectorAll( ".lazy" );
	for( var i = 0 ; i < elms.length ; i++ ) {
		let elm = elms[i];
		elm.style.opacity = '0';
		
		
		let b = getBoundingClientRectOnDom( elm );
		if( isIntersect( viewport, b ) ) {
			elm.setAttribute('data-isInitial', "1" );
		}
		else {
			elm.setAttribute('data-isInitial', "0" );
			
		}
	}
	

	window.addEventListener( "scroll", function( evt ) {
			loadLazyImage();
		}, false );
	
	//화면이 로드된 경우 현재화면에 이미지를 불러오기위해
	loadLazyImage();
});
function myHandlerOnImageLoad( evt ) {
	
	var img = evt.target;
	var elm = img.elmDiv;
	
	var viewport = getViewPort();
	var bound    = getBoundingClientRectOnDom( elm );
	
	
	// 이미지가 화면내에 있다면 transition 아닌경우 그냥 그려진다.
	if( isIntersect( viewport, bound ) && elm.getAttribute('data-isInitial') != '1' ) {  // <-- 변경
		elm.style.transition = 'opacity .5s ease-out';
	}
	
	elm.style.opacity = 1;
	elm.style.backgroundImage = "url('" + img.src + "')";
	
}

이렇게 두 부분을 수정한다.

 

이제 빠른 스크롤이나 갑자기 이동하는 것을 제외하고는 fade-in이 거의 적용되지 않아 마치 페이지의 이미지를 다 불려오는 것처럼 느낄 것이다.

 

다음의 링크에서 로드 후 키보드의 [End]키를 누르면 fade-in이 동작되는 것을 확인 할 수 있다.

 

 

동작 링크 -> jamesphk.iptime.org:8084/~blog/lazy_load/lazy_img3.php

 

Replies
Reply Write