담비의 개발블로그

이미지 최적화기술 본문

개발관련이야기

이미지 최적화기술

담비12 2024. 8. 28. 20:00

이미지를 최적화하는 기술들은 여러가지가 있다.

 

Lazy Loading

 

Lazy Loading은 페이지 로딩 시점에서 모든 이미지를 한꺼번에 로드하지 않고, 사용자가 실제로 필요한 시점에 이미지를 로드하는 기법이다. html5에선 아래처럼 코드를 삽입해서 사용한다. 

<img src="image.jpg" loading="lazy" alt="Example Image">

 

 

브라우저에서 기본 Lazy Loading을 지원하지 않는 경우, JavaScript를 사용하여 구현할 수도 있다. 별도 라이브러리 없이 Intersection Observer API를 사용하면 요소가 뷰포트에 들어올 때 이를 감지하여 해당 요소를 로드할 수 있다.

document.addEventListener("DOMContentLoaded", function() {
    const lazyImages = document.querySelectorAll("img.lazy");

    if ("IntersectionObserver" in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    lazyImage.src = lazyImage.dataset.src;
                    lazyImage.classList.remove("lazy");
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        });

        lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
        });
    } else {
        // Fallback for browsers without IntersectionObserver
        let lazyLoadThrottleTimeout;
        function lazyLoad() {
            if (lazyLoadThrottleTimeout) {
                clearTimeout(lazyLoadThrottleTimeout);
            }

            lazyLoadThrottleTimeout = setTimeout(function() {
                let scrollTop = window.pageYOffset;
                lazyImages.forEach(function(img) {
                    if (img.offsetTop < (window.innerHeight + scrollTop)) {
                        img.src = img.dataset.src;
                        img.classList.remove('lazy');
                    }
                });
                if (lazyImages.length == 0) { 
                    document.removeEventListener("scroll", lazyLoad);
                    window.removeEventListener("resize", lazyLoad);
                    window.removeEventListener("orientationChange", lazyLoad);
                }
            }, 20);
        }

        document.addEventListener("scroll", lazyLoad);
        window.addEventListener("resize", lazyLoad);
        window.addEventListener("orientationChange", lazyLoad);
    }
});

 

아래 영상은 사용예시이다.

 

 

▶ 특징

- 초기 로딩 시간 단축: 초기 페이지 로딩 시점에 불필요한 리소스를 로드하지 않기 때문에 페이지가 더 빨리 표시된다. 이는 특히 많은 이미지나 대용량 미디어 파일이 포함된 페이지에서 효과적이다.
- 대역폭 절약: 사용자가 실제로 보지 않는 리소스를 로드하지 않기 때문에 서버와 사용자의 대역폭을 절약할 수 있다.
- 사용자 경험 향상: 초기 페이지 로딩 속도가 빨라지면, 사용자들은 웹사이트를 더 쾌적하게 이용할 수 있다. 사용자가 스크롤할 때 필요한 리소스만 로드되므로, 페이지의 반응성도 향상된다.

 

유의할점

- SEO: 검색 엔진이 Lazy Loaded된 이미지를 크롤링하지 못할 수 있다. 이 문제를 해결하기 위해 noscript 태그를 사용하여 기본 이미지를 제공하는 것이 좋다.

<img src="placeholder.jpg" data-src="image.jpg" class="lazyload" alt="Lazy Loaded Image">
<noscript>
    <img src="image.jpg" alt="Lazy Loaded Image">
</noscript>


- 호환성: 일부 오래된 브라우저에서는 loading 속성이 지원되지 않을 수 있다. 이 경우 JavaScript 폴백을 제공하거나, Intersection Observer API를 사용하여 지원 범위를 넓힐 수 있다.
- 사용자 경험: Lazy Loading이 지나치게 많이 적용될 경우, 사용자가 스크롤할 때마다 이미지를 로드하는 딜레이로 인해 사용자 경험이 나빠질 수 있다. 적절한 사용이 중요하다.

WebP 포맷

 

웹 상에서 이미지 파일의 크기를 줄여 웹 페이지의 로딩 시간을 개선해준다.

 

▶ HTML에서의 사용

<picture> 요소와 <img> 태그를 사용하여 WebP를 사용하는 방법이다. 브라우저가 WebP를 지원하지 않을 경우 폴백 이미지(JPEG, PNG)를 제공할 수 있다.

<picture>
    <source srcset="image.webp" type="image/webp">
    <source srcset="image.jpg" type="image/jpeg">
    <img src="image.jpg" alt="Example Image">
</picture>



CSS에서의 사용

CSS에서도 WebP 이미지를 백그라운드 이미지로 사용할 수 있습니다. 필요에 따라 폴백 이미지를 제공할 수도 있다.

.example {
    background-image: url("image.webp");
    background-image: url("image.jpg");
}

 

폴백 이미지(fallback image)는 사용자가 브라우저나 디바이스에서 특정 이미지 포맷을 지원하지 않는 경우, 이를 대체하기 위해 제공하는 이미지이다. 예를 들어, 모든 브라우저가 WebP 포맷을 지원하지 않기 때문에, WebP 이미지를 지원하지 않는 브라우저에서 이미지가 제대로 표시되지 않을 수 있다. 이때 폴백 이미지로 JPEG나 PNG 같은 더 널리 지원되는 포맷의 이미지를 제공함으로써 모든 사용자에게 동일한 콘텐츠를 보여줄 수 있도록 하는 것이다.

 

▶ 특징

- 손실 압축 (Lossy Compression): WebP의 손실 압축은 JPEG와 유사하게 이미지를 압축하는 방식으로, 이미지의 일부 데이터가 손실되지만, 인간의 눈으로는 큰 차이를 느끼지 못할 정도의 품질을 유지한다. JPEG에 비해 같은 품질에서 약 25~34% 더 작은 파일 크기를 제공한다.
- 무손실 압축 (Lossless Compression): WebP의 무손실 압축은 이미지 데이터를 손실 없이 압축하는 방식이다. PNG와 비교했을 때, WebP 무손실 압축은 약 26% 더 작은 파일 크기를 제공한다. 이 방식은 알파 채널(투명도)을 지원하여, 투명한 배경이 있는 이미지를 효과적으로 압축할 수 있다.
- 알파 채널 (Alpha Channel): WebP는 알파 채널을 지원하여 이미지의 투명한 부분을 표현할 수 있다. 이는 PNG의 주요 기능이기도 하지만, WebP는 더 작은 파일 크기로 이 기능을 제공한다. 무손실 알파 이미지는 일반 PNG보다 3배 더 작은 파일 크기를 가질 수 있다.

- 애니메이션 지원: WebP는 GIF와 유사하게 애니메이션 이미지를 지원한다. 다만, 애니메이션 WebP는 GIF보다 더 적은 용량을 차지하면서도 더 많은 색상을 지원한다. GIF에 비해 같은 애니메이션에서 파일 크기를 줄일 수 있으며, 24비트 색상과 8비트 투명도를 지원한다.

 

유의할점

- 호환성 문제: WebP는 대부분의 최신 브라우저와 도구에서 지원되지만, 일부 오래된 브라우저나 특정 플랫폼에서는 여전히 지원되지 않을 수 있다. 따라서 호환성을 고려한 폴백(fallback) 이미지 전략이 필요할 수 있다.
- 변환 비용: 기존에 JPEG, PNG 등으로 저장된 이미지 파일을 WebP로 변환하는 데는 추가적인 작업이 필요하다. 특히 대규모 웹사이트에서는 모든 이미지를 WebP로 변환하는 것이 부담스러울 수 있다.
- 특정 사용 사례에서의 제한: 예를 들어, 인쇄용 고해상도 이미지를 다룰 때는 JPEG나 PNG가 여전히 더 적합할 수 있다.

 

 

Skeleton Screen

 

스켈레톤 스크린은 사용자에게 빈 페이지 대신 실제 콘텐츠의 구조를 미리 보여주는 시각적인 자리 표시자를 제공한다. 예를 들어, 이미지가 로드되기 전에는 해당 이미지의 자리 표시자가, 텍스트가 로드되기 전에는 해당 텍스트의 자리 표시자가 보이는 것이다.

 

 CSS를 이용한 구현

스켈레톤 스크린은 주로 CSS로 구현된다. 자리 표시자의 색상, 모양, 크기를 CSS로 정의하고, 애니메이션을 추가하여 로딩 중인 효과를 줄 수 있다.

.skeleton {
    background-color: #eee;
    height: 16px;
    width: 100%;
    margin-bottom: 8px;
    border-radius: 4px;
}

.skeleton-text {
    height: 16px;
    width: 80%;
    background-color: #eee;
    margin-bottom: 8px;
    border-radius: 4px;
}

.skeleton-image {
    width: 100%;
    height: 200px;
    background-color: #ddd;
    border-radius: 8px;
}
<div class="skeleton-image"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text"></div>

 

 

▶ JavaScript를 이용한 동적 구현: JavaScript를 사용하여 로딩이 완료되면 스켈레톤 스크린을 실제 콘텐츠로 대체할 수 있다. AJAX 요청이나 API 호출을 사용해 데이터를 가져오는 동안 스켈레톤 스크린을 보여준다.

document.addEventListener("DOMContentLoaded", function() {
    fetchData().then(data => {
        renderContent(data);
        removeSkeleton();
    });
});

function removeSkeleton() {
    const skeletons = document.querySelectorAll('.skeleton');
    skeletons.forEach(skeleton => {
        skeleton.remove();
    });
}

 

▶ 특징

- 사용자 경험 개선: 스켈레톤 스크린은 로딩 시간을 사용자에게 더 견딜만하게 만들어 준다. 사용자는 로딩이 끝나면 어떤 콘텐츠가 나타날지 예측할 수 있으므로 기다리는 동안의 불편함이 줄어든다.
- 인지 부하 감소: 사용자가 페이지 로딩을 기다리는 동안 아무 것도 보이지 않는 상태는 사용자에게 불안감을 줄 수 있다. 스켈레톤 스크린은 사용자가 무엇을 기대해야 하는지 명확히 보여주기 때문에 인지 부하를 줄이고, 불안감을 완화한다.
- 속도 인식 향상: 실제 로딩 속도가 느려도 스켈레톤 스크린 덕분에 사용자는 페이지가 더 빨리 로드된다고 느낄 수 있다. 이는 심리적인 효과로, 페이지가 즉각 반응하는 것처럼 보이게 한다.
- 브랜드 이미지: 스켈레톤 스크린은 브랜드의 일관된 시각적 스타일을 유지하는 데 도움을 줄 수 있다. 로딩 중에도 일관된 디자인 요소를 보여줌으로써 사용자에게 좋은 인상을 남길 수 있다.

 

유의할점

- 디자인 일관성: 스켈레톤 스크린의 스타일은 실제 콘텐츠의 스타일과 일관성을 유지해야 한다. 그렇지 않으면 콘텐츠가 로드된 후 사용자에게 혼란을 줄 수 있다.
- 지나친 사용 피하기: 모든 상황에서 스켈레톤 스크린을 사용하는 것이 적합하지 않을 수 있다. 너무 자주 사용하면 오히려 로딩이 느리다고 인식될 수 있다.
- 성능 최적화: 스켈레톤 스크린 자체도 로드되기 때문에, 이를 구현하는 데 드는 리소스가 과도하면 본래의 목적을 잃고 로딩 시간을 더 늘릴 수 있다.

 

 

Defer 키워드와 비동기/동기 로딩 조절

 

1. 동기 로딩 (Synchronous Loading)
기본적으로, <script> 태그에 아무런 속성도 사용하지 않으면, 스크립트는 동기적으로 로드된다. 즉, 브라우저는 HTML 문서를 위에서 아래로 파싱하다가 <script> 태그를 만나면 스크립트를 로드하고 실행할 때까지 HTML 파싱을 멈춘다.

<script src="script.js"></script>

 

▶ 특징

스크립트 파일이 클 경우 로딩 시간이 길어질 수 있으며, 이로 인해 HTML 문서의 나머지 부분이 늦게 렌더링되어 사용자에게 빈 화면이나 불완전한 페이지가 잠시 동안 표시될 수 있다.

 

 

2. defer 키워드
defer 키워드는 스크립트 로딩을 비동기적으로 수행하면서, 스크립트 실행을 HTML 문서의 파싱이 완료된 후로 미룬다. 즉, HTML 문서가 완전히 파싱된 후에야 스크립트가 실행된다.

<script src="script.js" defer></script>


▶ 특징
- 스크립트는 비동기적으로 로드되지만, 여러 개의 defer 스크립트는 선언된 순서대로 실행된다.
- 스크립트는 HTML 문서 파싱이 끝난 후 실행되므로, DOM에 안전하게 접근할 수 있다.

 

3. async 키워드
설명: async 키워드는 스크립트를 비동기적으로 로드하고, 로드가 완료되는 즉시 스크립트를 실행한다. HTML 파싱은 스크립트가 로드되는 동안 계속 진행된다.

 

<script src="script.js" async></script>


▶ 특징
- 스크립트 로딩이 비동기적으로 이루어지며, 로드가 끝나면 바로 실행된다.
- 여러 개의 async 스크립트가 있을 경우, 각각의 스크립트는 로드된 순서와 무관하게 먼저 로드된 스크립트가 먼저 실행된다.
- 스크립트가 로드되고 실행되는 시점에 HTML 파싱이 중단될 수 있다.

 

 

동기, defer, async의 비교
- 동기 로딩: 스크립트 로딩과 실행은 HTML 파싱을 중단시킨다. 중요한 스크립트에 사용하지만, 페이지 로딩 속도가 느려질 수 있다.
- defer: 스크립트 로딩은 비동기적으로 이루어지며, HTML 파싱이 끝난 후에 순서대로 실행된다. DOM에 의존적인 스크립트에 적합하며, 페이지 로딩 속도에 긍정적인 영향을 준다.
- async: 스크립트 로딩과 실행이 비동기적으로 이루어지며, 다른 스크립트나 HTML과의 의존성이 없는 경우에 사용된다. 외부 분석 도구나 광고 스크립트 등에 적합하다.

 

고려할 사항

- 페이지 로딩 성능 최적화: defer와 async를 적절히 사용하면, 페이지 로딩 속도를 개선하고, 사용자 경험을 향상시킬 수 있다. 특히, defer는 대부분의 상황에서 좋은 선택이 될 수 있다.
- 스크립트 간 의존성: 여러 스크립트가 서로 의존적인 경우, defer가 더 적합할 수 있다. async는 의존성이 없는 스크립트에만 사용해야 한다.
- 브라우저 호환성: 대부분의 최신 브라우저는 defer와 async를 지원하지만, 여전히 일부 구형 브라우저에서 이들 키워드를 지원하지 않을 수 있으므로, 이 점을 고려해야 한다.

 

 

적용예시

- 라이브러리 로딩: 웹 애플리케이션에서 jQuery와 같은 라이브러리를 사용할 경우, defer를 사용하는 것이 일반적입니다.
- 적 코드나 광고 스크립트 로딩: Google Analytics와 같은 스크립트는 페이지 로딩을 방해하지 않도록 async로 로드하는 것이 일반적입니다.

결론
defer와 async 키워드는 웹 페이지 성능 최적화의 중요한 요소로, 각각의 키워드를 적절히 활용하면 스크립트 로딩과 실행 방식을 최적화할 수 있다. defer는 HTML 파싱을 방해하지 않으면서 스크립트를 순차적으로 실행하고, async는 독립적인 스크립트를 빠르게 로드하고 실행하는 데 유용하다. 이러한 키워드들을 올바르게 사용하면 페이지 로딩 속도를 개선하고 사용자 경험을 향상시킬 수 있다.