지연 로딩과 비동기 로딩 (Lazy Loading and Async Loading)

지연 로딩과 비동기 로딩 (Lazy Loading and Async Loading)

자바스크립트에서 성능을 높이고 초기 로딩을 빠르게 하려면 지연 로딩비동기 로딩이 유용하다. 필요할 때만 리소스를 불러오면 사용자 경험이 더 부드러워진다. 이번에는 이 두 가지 방법을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


이 방법을 잘 다루면 페이지 로드가 빨라지고 리소스 낭비도 줄일 수 있다. 하나씩 살펴보며 어떤 상황에서 어떻게 적용할지 알아보자.


지연 로딩 기본

지연 로딩은 필요할 때까지 리소스 로드를 미루는 방식이다. 이미지 로딩을 예로 들어보자:

<!-- HTML -->
<img data-src="image.jpg" class="lazy" alt="지연 로딩 이미지">

// JavaScript
function lazyLoadImages() {
    const images = document.querySelectorAll(".lazy");
    images.forEach(img => {
        if (img.getBoundingClientRect().top < window.innerHeight) {
            img.src = img.dataset.src;
            img.classList.remove("lazy");
        }
    });
}

window.addEventListener("scroll", lazyLoadImages);
lazyLoadImages();

화면에 보일 때만 이미지를 로드하니 초기 로딩 속도가 빨라졌다.


1. Intersection Observer로 지연 로딩

IntersectionObserver를 사용하면 더 효율적으로 관리할 수 있다:

const observer = new IntersectionObserver((entries, obs) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            obs.unobserve(img);
        }
    });
});

const images = document.querySelectorAll(".lazy");
images.forEach(img => observer.observe(img));

스크롤 이벤트를 직접 듣는 대신 브라우저가 교차 여부를 감지하니 성능 부담이 줄었다.


2. 모듈 비동기 로딩

필요한 모듈을 동적으로 불러오면 초기 부담이 줄어든다:

async function loadModule() {
    const module = await import("./heavyModule.js");
    console.log(module.doSomething());
}

const button = document.querySelector("#loadButton");
button.addEventListener("click", loadModule);

사용자가 버튼을 누를 때만 모듈을 로드하니 페이지 시작이 빨라졌다.


3. 스크립트 비동기 로딩

asyncdefer로 외부 스크립트를 효율적으로 불러오자:

<!-- HTML -->
<script src="script.js" async></script>
<script src="deferScript.js" defer></script>

// script.js
console.log("비동기 로딩 완료");

// deferScript.js
console.log("defer 로딩 완료");

async는 로드가 끝나는 대로 실행되고, defer는 DOM이 준비된 후 순서대로 실행된다. 상황에 맞게 선택하면 된다.


4. 이미지 프리로드와 지연 로딩 조합

중요한 이미지는 미리 로드하고 나머지는 지연 로딩으로 처리할 수 있다:

<!-- HTML -->
<link rel="preload" href="hero.jpg" as="image">
<img src="hero.jpg" alt="메인 이미지">
<img data-src="lazy.jpg" class="lazy" alt="지연 이미지">

// JavaScript
const lazyImages = document.querySelectorAll(".lazy");
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.src = entry.target.dataset.src;
        }
    });
});
lazyImages.forEach(img => observer.observe(img));

preload로 핵심 이미지를 먼저 로드하고, 나머지는 지연 로딩으로 균형을 맞췄다.


5. 컴포넌트 지연 로딩

프레임워크에서 컴포넌트를 필요할 때 불러오자 (React 예시):

import React, { lazy, Suspense } from "react";

const HeavyComponent = lazy(() => import("./HeavyComponent"));

function App() {
    return (
        <div>
            <Suspense fallback=<div>로딩 중...</div>>
                <HeavyComponent />
            </Suspense>
        </div>
    );
}

lazySuspense로 컴포넌트를 동적으로 로드하니 초기 번들 크기가 줄었다.


6. 비동기 데이터 로딩

데이터를 비동기로 불러와서 UI를 점진적으로 채울 수 있다:

async function loadData() {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    const list = document.querySelector("#list");
    list.innerHTML = data.map(item => `<li>${item.title}</li>`).join("");
}

window.addEventListener("load", loadData);

페이지가 먼저 렌더링되고 데이터가 나중에 채워지니 사용자 대기 시간이 줄었다.


7. 코드 스플리팅과 비동기 로딩

Webpack으로 코드 스플리팅을 적용해보자:

// app.js
function loadFeature() {
    import("./feature").then(module => {
        module.runFeature();
    });
}

const button = document.querySelector("#featureButton");
button.addEventListener("click", loadFeature);

// webpack.config.js
module.exports = {
    entry: "./app.js",
    output: {
        filename: "[name].[contenthash].js",
        chunkFilename: "[name].[contenthash].chunk.js"
    }
};

동적 임포트로 필요한 코드만 분리해서 로드하니 초기 번들이 가벼워졌다.


8. 비디오 지연 로딩

비디오도 필요할 때만 로드할 수 있다:

<!-- HTML -->
<video data-src="video.mp4" class="lazy-video" controls></video>

// JavaScript
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const video = entry.target;
            video.src = video.dataset.src;
            observer.unobserve(video);
        }
    });
});

const videos = document.querySelectorAll(".lazy-video");
videos.forEach(video => observer.observe(video));

비디오가 뷰포트에 들어올 때만 로드하니 대역폭이 절약됐다.


9. 조건부 비동기 로딩

특정 조건에서만 리소스를 로드할 수 있다:

async function loadIfNeeded(condition) {
    if (condition) {
        const module = await import("./optionalModule.js");
        module.run();
    } else {
        console.log("로드 필요 없음");
    }
}

window.addEventListener("scroll", () => {
    loadIfNeeded(window.scrollY > 500);
});

스크롤이 500px을 넘을 때만 모듈을 로드하니 불필요한 요청이 줄었다.


10. 서비스 워커와 비동기 로딩

서비스 워커로 리소스를 비동기적으로 관리해보자:

// sw.js
self.addEventListener("fetch", event => {
    event.respondWith(
        caches.match(event.request).then(response => {
            if (response) return response;
            return fetch(event.request).then(res => {
                const clone = res.clone();
                caches.open("dynamic").then(cache => 
                    cache.put(event.request, clone)
                );
                return res;
            });
        })
    );
});

// main.js
navigator.serviceWorker.register("/sw.js");

캐시가 없으면 비동기로 가져오고 저장하니 오프라인에서도 동작이 부드러워졌다.


마무리

지연 로딩과 비동기 로딩은 성능 최적화와 사용자 경험 개선에 큰 도움을 준다. 이미지, 모듈, 데이터, 서비스 워커까지 상황에 맞게 적용하면 더 빠르고 효율적인 애플리케이션을 만들 수 있다.


+ Recent posts