프론트엔드 관련 과제 테스트를 경험삼아 신청했을 때 Lazy Loading 관련한 문제가 나왔었습니다. 레이지 로딩이 무엇인지는 어렴풋이 알고 있었지만 구현 해본적이 없어서 굉장히 당황했었습니다. 하면 금방 하겠지라는 마음에 계속 미루다가 덜컥 테스트때 나오니 그동안 미루어 왔던 것이 후회는 순간 이었습니다. 그러한 기억이 있음에도 미루어 왔던 레이지 로딩 기법. 지금부터 사용자의 데이터 낭비를 막아줄 레이지 로딩에 관하여 알아보도록 하겠습니다.
아이디어?
레이지-로딩이란 이미지에 대한 로딩을 뒤로 미루는 것을 의미합니다. 레이지-로딩을 적용시키지 않은 웹 페이지를 열면 브라우저가 모든 이미지를 읽고 불러와서 DOM에 렌더링 할 것입니다. 이미지가 많지 않으면 상관이 없지만 정말 많은 이미지가 한 페이지에 있다면 그 페이지를 여는 시간도 오래 걸릴 것이고 만약 모바일 데이터를 이용하여 웹 페이지에 접속 중이라면 데이터도 굉장히 많이 들고 만약 실수로 이 웹페이지에 접속했거나 맨 위 이미지 두 어개 정도만 보고 나간다면 이 모든 리소스가 낭비됩니다. 따라서 우리는 어떤 이미지를 로딩할 필요가 있으면 그 때 이미지를 불러올 것이고 일반적으로 레이지 로딩은 스크롤 애니메이션을 통해 구현됩니다.
<div class="image-container">
<img class="image" data-lazy="./img/1.jpg" alt="img" />
<img class="image" data-lazy="./img/2.jpg" alt="img" />
<img class="image" data-lazy="./img/3.jpg" alt="img" />
<img class="image" data-lazy="./img/4.jpg" alt="img" />
<img class="image" data-lazy="./img/5.jpg" alt="img" />
</div>
먼저 맘에드는 이미지들로 HTML을 구성합니다.
.image {
width: 300px;
height: 300px;
object-fit: cover;
margin-bottom: 5em;
opacity: 0;
transform: translateX(-50%);
transition: all 0.5s;
}
.fade {
transform: translateX(0);
transition: all 0.5s;
opacity: 1;
}
CSS 역시 자기맘에 들도록 구성합니다. 단레이지-로딩 기법의 시각적인 확인을 위해 transform, transition, opacity
는 위와 같이 작성해줍시다.
const images = document.querySelectorAll("img"); // 모든 이미지 파일 선택
window.addEventListener("scroll", (event) => {
images.forEach((img) => { // 각 이미지마다
console.log("Scrolling...");
const rect = img.getBoundingClientRect().top;
if (rect <= window.innerHeight) { // 이미지가 보일 타이밍을 계산
const src = img.getAttribute("data-lazy"); // img 태그의 data-lazy에 저장해둔 이미지 경로를 붙여준다.
img.setAttribute("src", src);
img.classList.add("fade"); // 트랜지션 추가
}
});
});
getBoundingClientRect().top
은 페이지 가장 위부터 그 엘리먼트의 top까지의 크기를 말합니다. 따라서 scoll 할 때마다 각 이미지에 대한 상단으로 부터의 위치를 지정하고 그 위치가 페이지의 높이 내에 들어왔다면 ~ 을 통해 이미지가 로딩될 타이밍을 계산하도록 합니다.
실제로 이렇게 코드를 작성하면 작동을 제대로 됩니다. 하지만 이 경우는 이벤트 리스너를 스크롤에 주는 굉장히 리소스 낭비가 심한 방법입니다. 심지어 모든 이미지가 로딩이 끝나도 계속해서 이미지의 위치를 계산하게 됩니다.
더 좋은 방법
스크롤 이벤트 리스너보다 좋은 방법은 바로 IntersectionObserver
라는 API를 사용하는 것 입니다.
IntersectionObserver(이하 io)는 어떤 엘리멘트가 화면에 노출되었는지를 알려줍니다.
const lazyLoad = (target) => {
const io = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
console.log("reached!");
if (entry.isIntersecting) {
const img = entry.target; // 이미지 엘리멘트를 가져옵니다.
const src = img.getAttribute("data-lazy");
img.setAttribute("src", src);
img.classList.add("fade");
observer.disconnect();
}
});
});
io.observe(target);
};
위와 같이 자바스크립트 코드를 바꿔줍니다. io는 일반적으로 뷰포트의 가장 위부터 위치를 계산하여 노출 여부를 판단하는데 IntersectionObserver
의 두 번째 파라미터(첫번째는 entries
와 observer가 있는 콜백함수)로 옵션을 전달해줄 수도 있습니다.
entries는 들어오는 타겟 엘리먼트들입니다. isIntersecting
은 boolean
을 리턴하는데 현재 화면에 타겟이 들어왔는지 아닌지를 판단합니다. 만약 들어왔다면 위와 같이 이미지 경로를 알려주어 이미지를 로딩하게 됩니다.
만약 로딩이 완료되면 이미지의 위치를 계속 보고 있던 observer를 disconnect 메소드를 이용해 없애줍니다. 이로 인해 이벤트 리스너와 달리 이미지가 로딩되면 더이상 브라우저가 관여하지 않게 됩니다.
images.forEach(lazyLoad);
마지막으로 이제 querySelectorAll
을 통해 불러온 이미지들에게 lazyLoad 함수를 적용시킵니다.
이제 화면에 이미지 로딩은 단 한번만 하게되고 타겟 이미지의 로딩이 완료되면 더이상 observer가 화면을 지켜보고 있지 않게 되어 불필요한 리소스 낭비를 막게 되었습니다.
이번엔 바닐라 자바스크립트로 레이지 로딩 기법을 구현해 보았습니다. 모든 작업을 리액트로 하다보니 바닐라 자바스크립트에 대한 실력이 많이 부족함을 알게 되었습니다. 과제 테스트 때도 바닐라 자바스크립트 작업을 하게되어 조금 당황했는데 앞으로 레이지 로딩과 같은 자바스크립트 기법은 전부 바닐라로 구현하여 자바스크립트 역량을 키우는게 중요하다는 것을 알게 되었습니다.