본문 바로가기

FRONT-END/JAVASCRIPT

자바스크립트 레이지로드(lazy load)

반응형

자바스크립트 레이지로드(lazy load)

<script>는 HTML에서 자바스크립트를 사용하는 태그이다.

이 태그는 자바스크립트를 인라인으로 HTML에 직접 포함하기도 하고, 다른 파일에 있는 스크립트를 불러오기도 한다.

이때 <script> 태그의 위치에 따라 때로는 심각하게 다른 사용자 경험을 제공하게 될 수 있다.


<script>태그의 위치가 잘못 선정하면 최악의 경우 웹페이지의 <title> 태그가 로드 되지 않고 실행되서 <title>도 나타나지 않는다.

이렇게 웹페이지가 몇 초간 로드되지 않으면 사용자들은 인터넷 접속에 문제가 있다고 생각할 수 도 있다.

이러한 상황은 다소 극단적인 예이지만, 처리 시간이 다소 소모되는 스크립트를 HTML 문서 상단에 두는 것은 사용자 경험이 안좋은 개발 방법이다.

이렇게 웹페이지가 보이지 않고 멈춰있으면 사용자가 반복해서 새로고침을 눌러서 불필요한 트래픽이 증가할 수도 있고, 재방문을 꺼릴수도 있다.


이러한 현상이 발생하는 이유는 <script> 태그에 있다.

즉 <script> 태그로 실행되는 자바스크립트는 블록(block) 모드로 실행된다.

따라서 해당 자바스크립트가 전부 실행되어야하지만 이후의 DOM을 화면에 출력한다.

<script> 태그에서 실행되는 자바스크립트가 간단한 스크립트면 큰 차이를 느끼기 어렵지만, 복잡한 처리를 수행하는 스크립트라면 화면이 전체적으로 잠시 멈춘 듯한 느낌을 준다.

이처럼 브라우저들이 <script> 태그를 블록 모드로 실행하면서 먼저 완료되기를 기다리는 이유는 자바스크립트 자체적으로 HTML을 그릴 수 있는 기능을 제공하기 때문이다.

(function(){

  document.write("<div>Dynamic HTML written by javascript</div>");

}());


자바스크립트는 DOM에 직접 HTML을 그릴 수 있는 기능이 있어서 이후의 HTML은 해당 스크립트에 따라서 전혀 다른 모습으로 나타날 수 있다.

대표적으로 <ul>이나 <table> 태그를 자바스크립트로 그릴때 이다.

<style>

  .my_table {

    border: 1px solid black;

  }

</style>

<script>

(function(){

  document.write("<div>Dynamic HTML written by javascript</div>");

}());


(function(){

  document.write("<table class=\"my_table\">");

}());

 </script>

   <tr>

       <td>My table</td>

       <td>Table tag written by javascript</td>

  </tr>

<script>

(function(){

  document.write("</table>");

}());

브라우저에서는 자바스크립트에서 HTML태그를 그릴 것인지 명확하게 구분하기 어렵다.

따라서 웹페이지 로드중 <script> 태그에 도달하면 아래의 HTML을 화면에 출력하는 것을 멈추고 <script> 태그의 자바스크립트가 전부 실행되기를 기다리는데, 이를 블록모드로 실행된다고 한다.

이때 자바스크립트가 외부 파일일 때는 파일을 가져오는 시간과 파싱, 실행 시간까지 포함하여 기다린다.

이러한 블록 모드 때문에 사용자 경험이 나빠질수 있으므로 개발자들은 이러한 상황에 유의해야한다.


이러한 상황을 방지하기 위한 가장 기본적인 해결 방안은 <script> 태그를 주요 내용이 표시되는 HTML 보다  아래에 작성하는 방법이다.

전체적으로 로딩 시간에 대한 최적화 없이 <script> 태그만 아래로 옮기면 페이지가 로드되는데 걸리는 시간은 같다. 하지만 웹페이지에 주요 내용이 보이고 나서 자바스크립트의 로딩과 처리하는 시간을 소모하는 것이 빈 웹페이지에서 자바스크립트를 로딩하고 처리하는 것보다 사용자 경험 면에서 월등하게 좋다.


잠시 멈칫거리는 로딩 시간도 현재 웹페이지의 내용이 일부 보이는 상태라면 사용자는 웹페이지가 로딩 중이라고 인지할 수 있다.

그러므로 필요한 경우가 아니라면 <script> 태그를 웹페이지의 하단에 작성하는 것이 좋다. 


<scripnt> 태그를 <body>의 아래에 작성후 <div>안에 있는 내용들은 DOM의 흐름상 먼저 로드돼서 화면에 보이지만, 이미지는 아직 보이지 않는 것을 확인할 수 있다.

이렇게 텍스트가 나오는데 이미지는 나오지 않는 이유는 기본적으로 브라우저에서 DOM을 처리하고 있는 방식 때문이다.

브라우저에서는 이미지 등을 먼저 화면에 표시하기전에 DOM을 전체적으로 파싱하면서 화면 레이아웃에 영향을 미치는 모든 CSS 태그, 그리고 파일들을 파싱한다.

그리고 양쪽 모두 파싱이 끝나면 화면에 표시하기위해 레이아웃을 잡고 렌더링 단계를 거친다.

이때 DOM에 대한 파싱이 전부 이루어져야 화면에 레이아웃을 잡고 표시를 실행한다.

따라서 실제 이미지 태그는 불러왔지만, CSS로 이지미에 대한 설정이나 크키, 배경등이 변경될수 있으므로 DOM과 CSS에 대한 파싱이 끝나고 나서 화면에 출력을 시작한다.



HTML->DOM파싱 -> 화면 render 

CSS 파일/태그 -> 규칙파싱 ->화면 render


이처럼 <script> 태그를 하단에 작성하여 텍스트 콘텐츠는 사용자에게 먼저 보여줌으로써 사용자 경험을 개선할 수 있다.

하지만 이미지도 중요해서 먼저 보여주도록 사용자 경험을 개선한다면 해당 <script>태그에 실행되는 자바스크립트의 양을 분산하는 것이 필요하다.

이렇게 최초 페이지가 로드될때 실행되는 자바스크립트를 분산하는 방법은 다양한데, 그중 대표적인 방법은 자바스크립트의 레이지 로드 방식이다.


레이지 로드(lazy load)방식은 말 그대로 자바스크립트를 지금 바로 로드해서 실행하는 것이 아니라 페이지 로드가 이루어지고 나서 조금 늦게 로드해서 실행하는 방법이다.

별도로 자바스크립트 파일을 구분해 놓고 동적으로 새로운 <script> 태그를 추가함으로써 구현한다.

<!DOCTYPE html>

<html lang="en">

 <head>

   <meta charset="utf-8">

   <title>hello</title>

 </head>

 <body>

 <script>

(function(){

  var previousOnload;

  if(window.addEventListener){

    window.addEventListener("load", lazyload);

  }else if(window.attachEvent){

    window.attachEvent("onload", lazyload);

  }else if(window.onload){

    previousOnload = window.onload;

    window.onload = function(){

      previousOnload.call();

      lazyload();

    }


  }else{

      window.load = lazyload;

  }


  function lazyload(){

    var scriptTag = document.createElement("script"),

        headTag = document.getElementsByTagName("head")[0];

    scriptTag.setAttribute("src", "./sleep.js");

    scriptTag.setAttribute("type","text/javascript");

    scriptTag.setAttribute("async","true");


    if(!headTag){

      headTag = document.getElementsByTagName("script")[0].parentNode;

    }

    headTag.appendChild(scriptTag);    

  }

}());

</script>

 </body>

</html>

최초에 실행되는 자바스크립트는 onload 이벤트 핸들러 함수를 등록하는 과정만 거친다.

그리고 웹페이지에 대한 로드가 끝나면  onload 이벤트 핸들러를 실행함으로써 새로운 <script> 태그를 추가해서 그 이후에 추가적인 자바스크립트를 로드하고 실행한다.

window.onload 대신 addEventListener()함수 또는 attachEvent()함수를 사용하고 있다.

이것은 window.onload를 사용하면 브라우저의 호환성을 고려하지 않고 바로 적용할 수 있지만, 여러 자바스크립트 파일이나 라이브러리를 이용할 때는 좋지 않는 방법이다.

왜냐하면 addEventListener()와 attachEvent() 함수를 사용하면 같은 이벤트에 대하여 여러 이벤트 핸들러를 호출할 수 있지만, window.onload를 바로 함수로 설정하는 방법은 자바스크립트에서 다시  window.onload를 설정하면 덮어씌울 수 있는 위험성이 있다.

따라서 기존 window.onload가 있으면 이를 백업해두고 같이 호출할 수 있도록 보완하고 있다.

그러나 다른 소스에서도 이렇게 호출할수 있는지 명확하지 않으므로 최대한 방어적으로 addListener()나 attachEvent() 함수 이용을 권장한다.

이 두가지 함수만 사용하더라도 사실 거의 모든 브라우저에서 처리할 수 있지만, 만약을 대비한 소스를 작성해 놓은 것도 좋다.


lazyload() 함수에서는 <script> 태그를 만들어두고 거기에 원하는 자바스크립트 파일을 로드하도록 src, type 그리고 async 속성을 설정하고 있다.

여기서 src 속성은 레이지 로드를 적용하려는 자바스크립트 파일명을 설정하면되고,

async는 HTML5 부터 표준에 정의된 속성으로 스크립트를 비동기로 실행한다.

최신 브라우저들에서는 해당 속성을 처음부터 부여하여 블록모드에서 <script> 태그가 실행되는 것을 막을 수 있지만, 아직 지원하지 않는 브라우저들은 위의 lazyload()함수를 사용하면 문제 없다.


레이지 로드 방식을 채택하면 DOM과 CSS 파싱이 끝나고 나서 화면 렌더링 시작된 뒤에 스크립트를 실행한다.

이것을 응용해서 레이지 로드하는 함수가 별도로 인자를 받도록 하여 여러 개의 스크립트 파일을 레이지 로드로 처리할 수 도 있다.

또한 레이지 로드는 <script> 태그뿐만 아니라 내려받는데 오래 걸리는 <img> 태그에도 적용해서 전체적인 웹페이지에 대한 사용자 경험을 개선할 수 있다


<!DOCTYPE html>

<html lang="en">

 <head>

   <meta charset="utf-8">

   <title>hello</title>

 </head>

 <body>

   <img width="300" height="250" class="lazyload" data-src="./ranifit-ad.png"/>

   <img width="320" height="200" class="lazyload" data-src="./ranifit-ad2.jpg"/>

 <script>

(function(){

  var previousOnload;

  if(window.addEventListener){

    window.addEventListener("load", lazyloadImages);

  }else if(window.attachEvent){

    window.attachEvent("onload", lazyloadImages);

  }else if(window.onload){

    previousOnload = window.onload;

    window.onload = function(){

      previousOnload.call();

      lazyloadImages();

    }


  }else{

      window.load = lazyloadImages;

  }


  function lazyloadImages(){

    var imgList = document.getElementsByClassName("lazyload"),

        length = imgList.length,

        i;

    for (var i = 0; i <length; i++) {

      imgList[i].src = imgList[i].getAttribute("data-src");

    }

  }

}());

</script>

 </body>

</html>

레이지 로드하고자 하는 <img> 태그들은 "lazyload"라는 CSS class를 부여해주고 스크립트에서는 이러한 <img> 태그를 탐색하여 또 다른 속성으로 설정해놓은 data-src 속성값을 실제 src로 바꿔주면서 전체 페이지 로드가 끝난 뒤부터 이미지를 내려받도록 하고 있다.

HTML 처리와 페이지 로드가 끝나고 난 다음부터 이미지를 내려받기 시작한는 것을 확인할 수 있다.


용량이 큰 이미지를 많이 표시하는 웹페이지는 때로는 더 빠르게 내려받아야 하는 중요한 스크립트나 CSS 파일을 지연시키는 요인이 되기도 한다.

특히 인터넷 환경이 불안한 모바일이나 인터넷이 느린 브라우저 환경에서는 이미지가 내려받기 가능한 네트워크 속도 대역을 차지하여 사용자 경험이 안좋아지는 상황이 발생할 수 있다.

따라서 중요하지 않는 큰 용량의 이미지나 초기 화면에 안 보이거나 백그라운드에 있는 <img> 태그들은 이렇게 레이지 로드 방식으로 처리하여 사용자 경험을 좋게 하는 방법을 사용하자.


<img> 태그 중 레이지 로드 방식으로 처리할 대상을 선정할 때는 이미지의 중요성과 위치, 그리고 용량을 보고 선택하는 것이 좋다.

전면에 보이는 이미지 중 오른쪽에 표시되는 이미지를 보면 3개의 이미지를 차례로 보여주고 있다.

여기서 해당 이미지는 가장 중요한 위치에 있으므로 처음에 사용자가 웹페이지를 열었을 때 최대한 빠르게 표시해주는 것이 좋다.

그러나 이후 두번째, 세번째 롤링하면서 보여주는 이미지는 바로 보여주지 않아도 되므로 레이지 로드 방식으로 처리하면 좋다.


기타 오른쪽에 있는 이미지들도 레이지 로드 대상으로 선정할 수 있지만, 용량이 그렇게 크지 않을 것을 파악되면 그냥 최대한 빠르게 보이도록 <img> 태그의 src 속성을 HTML에서 설정하는 것이 좋다.

레이지 로드 방식으로 처리할 대상을 선정하기 전에 <img> 태그에 width와 height 속성은 될 수 있으면 HTML에서 바로 설정해주는 것도 사용자 경험을 개선할 수 있는 좋은 방법이다.

왜냐하면 이러한 width나 height를 설정하지 않고 src 속성만 설정하면 <img> 태그가 처음에는 기본크기로 있다가 나중에 이미지가 로드되면서 그 크기를 다시 조정한다.

따라서 사용자 관점에서는 처음에 페이지가 로드되었다가 레이아웃이 다시 변경되는 느낌을 받을 수 있다.

HTML에 이미지의 크기를 직접 넣는 것은 브라우저의 렌더링 단계에서 먼저 DOM을 로드하고 CSS를 나중에 적용하기 때문이다.

또한 <img> 태그의 크기가 변하면 이미지가 로드될때마다 DOM reflow가 일어나서 전체적인 웹페이지의 로딩 속도는 더 느려진다.

따라서 레이지 로드 방식으로 처리하려면 HTML에서 사전에 width와 height 속성을 설정해서 이미지가 차지할 영역 만큼 미리 <img> 태그가 자치하도록 하여 페이지 로드 중 레이아웃이 변하지 않도록 하는 것이 좋다.


화면에 보이지 않는 백그라운드에서 이미지 불러오기

이미지를 레이지 로드 방식으로 처리할 수 있는 또 다른 방법은 화면에 보이지 않는 백그라운드에서 이미지를 불러오는 방법이다.

이 방법은 앞의 방법과 동일하게 전체 페이지 로드가 끝나고 난 뒤부터 이미지를 내려받게 적용할 수도 있고, 필요하면 이미지를 바로 내려받게할 수도 있다.

<!DOCTYPE html>

<html lang="en">

 <head>

   <meta charset="utf-8">

   <title>hello</title>

 </head>

 <body>

<style>

  .lazyload {

    background-image: url("./loading.gif");

    background-size: 64px 64px;

    background-repeat: no-repeat;

    background-position: center;

  }

</style>

   <img width="300" height="250" class="lazyload" data-src="./ranifit-ad.png"/>

   <img width="320" height="200" class="lazyload" data-src="./ranifit-ad2.jpg"/>

 <script>

(function(){

  var previousOnload;

  if(window.addEventListener){

    window.addEventListener("load", lazyloadImages);

  }else if(window.attachEvent){

    window.attachEvent("onload", lazyloadImages);

  }else if(window.onload){

    previousOnload = window.onload;

    window.onload = function(){

      previousOnload.call();

      lazyloadImages();

    }


  }else{

      window.load = lazyloadImages;

  }


  function lazyloadImages(){

    var imgList = document.getElementsByClassName("lazyload"),

        length = imgList.length,

        i;


    for (var i = 0; i <length; i++) {

      (function(imgTag){

        var imgBackground = document.createElement("img");

        imgBackground.setAttribute("src", imgTag.getAttribute("data-src"));

        imgBackground.setAttribute("width", imgTag.getAttribute("width"));

        imgBackground.setAttribute("height", imgTag.getAttribute("height"));


        imgBackground.onload = function(){

          imgTag.parentNode.replaceChild(imgBackground, imgTag);

        };

      }(imgList[i]));


    }

  }

}());

</script>

 </body>

</html>

각각의 이미지는 백그라운드에서 불러오고, 내려받기가 완료되면 현재 보이는 <img> 태그 위치에 replaceChild로 교체된다.

이는 이미지 용량이 매우 클 때 응용하면 좋다.

원래의 <img> 태그 스타일 클래스인 .lazyload에 저용량 "로딩중" 이미지를 넣어둔다면 사용자 경험은 더 좋아질 수 있다.

하지만 이 경우에는 DOM이 교체되므로 앞의 방법보다는 reflow가 여러번 일어난다는 점에서 컴퓨팅 자원의 소모가 더 심하다.

따라서 이미지를 로드하는 것이 사용자 경험에 심각하게 영향을 미치거나 다수의 이미지를로드할때 사용하면 좋다.


<script>나 <img> 태그에 레이지 로드를 활용할 때 주의할점은 웹페이지에서 사용자가 빠르게 보거나 실행해야하는 자바스크립트를 대상으로 레이지 로드를 적용하면 안된다는 것이다.

예를 들면, <form> 을 통해서 입력 내용을 검증하고 다음 화면으로 넘어가는 자바스크립트나 사용자 인증이나 로그인을 처리하는 자바스크립트 등이다.

만약 이러한 자바스크립트에 레이지 로드를 적용하고 사용자가 레이지 로드가 끝나기전에 빠르게 로그인을 시도한다면 자바스크립트가 아직 정상으로 로드되지 않아서 로그인에 실패할수도 있다.

그러므로 로그인처럼 해당 웹페이지에서 제공해야하는 핵심 기능을 수행하는 자바스크립트에 레이지 로드를 적용할 때는 신중해야한다.


출처-속깊은 자바스크립트 양성익 지음

반응형