본문 바로가기

프론트엔드

이미지 최적화 & 지연로딩을 통한 블로그 Web Vital개선기

성능

체감상으로도 블로그게시글을 불러오는게 느리다고 판단되어, 라이트하우스로 측정한 결과 성능이 55점이라는 처참한? 결과가 나왔다.

  1. 첫번째로는 초기 서버 응답 시간이 길었다. 이유는 스크롤하면 캐릭터가 움직이는 3d 모델링을 로딩하는게 가장 큰 원인이었다. 3d모델링은 gltf파일인데 무려 10,782kb나 된다.
  2. 두번째로는 전체 게시글 리스트를 한번에 받아와서이다. 서버에서 페이징처리를 해서 불러올까 했지만 아직 게시글이 그렇게 많지 않아서 지연로딩을 통해 개선하기로 하였다.
  3. 세번째로는 이미지 최적화를 하지 않아서였다. 이미지 최적화를 하지 않고 업로드한 이미지를 그대로 firebase에 업로드 하였기에, 이미지를 로드하는데도 시간이 어느정도 걸린 것 같다.

1. 웹 3D모델링(gltf파일) 최적화

https://d2.naver.com/helloworld/6152907

위 게시글에서 gltf의 최적화에 대한 내용이 다루어져있지만, 아직은 조금 어려운 내용이어서 일단은 모델링을 주석처리하여 렌더링하지 않게 하였다.

추후 적용해보게 된다면 다시 다뤄볼 예정이다.

2. 지연로딩

import { useState, useEffect, RefObject } from 'react';

function useVisibility<T extends HTMLElement>(ref: RefObject<T>): boolean {
  const [isVisible, setIsVisible] = useState<boolean>(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsVisible(entry.isIntersecting);
      },
      {
        root: null,
        rootMargin: '0px',
        threshold: 0.03, // 구성 요소가 표시되는 것으로 간주되어야 하는 시기를 제어하려면 이 값을 조정합니다.
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, [ref]);

  return isVisible;
}

export default useVisibility;

스크롤 이동에 따라 ref의 children(PostCard컴포넌트)이 보이는 상태가 되면 해당 PostCard컴포넌트를 렌더링하도록 수정해주었다.

3. 이미지 최적화

에디터를 통해 firebase로 업로드 시, browser-image-compression라이브러리를 사용하여 이미지를 압축하여 업로드 하였다.

const onUploadImage = async (blob:Blob, callback: (url: string, altText?: string) => void) => {
    const fileName = `${Date.now().toString()}_${blob.name}`;
    const storageRef = ref(storage, `images/${fileName}`);
    const imageFile = new File([blob], fileName, { type: 'image/jpeg' });

    const options = {
      maxSizeMB: 1,
      maxWidthOrHeight: 1920,
      useWebWorker: true,
    }

    try {
      const compressdFile = await imageCompression(imageFile, options) // 이미지 압축
      const snapshot = await uploadBytes(storageRef, compressdFile); // 이미지 업로드
      const url = await getDownloadURL(snapshot.ref); // 에디터에 넣을 이미지 url
      images.current = Array.isArray(images.current) ? 
      	[...images.current, {fileName: fileName, url: url.replaceAll(/&/g, '&amp;')}] 
        :
        [{fileName: fileName, url: url.replaceAll(/&/g, '&amp;')}] 
        // 이미지가 업로드된 후 다시 제거할 경우 최종 업로드 시 firebase에서 해당 이미지를 지워주기 위해 사용되는 배열
  
      callback(url, 'image')

    } catch (error) {
      console.log('error', error)
      alert('이미지 업로드 실패')
    }
  }

👇🏻 일부러 큰 이미지를 넣어보았고, 위쪽이 압축전 기존 코드로 올린 이미지파일이고 아래가 이미지 최적화 진행 후 업로드한 이미지파일이다. 꽤나 크기차이가 있다.

접근성

 

이유는 버튼과 링크에 접근 가능한 이름이 없어서인데, 버튼에 이름을 넣어주어야 브라우저 리더기가 무슨 버튼인지 읽어주므로 접근성이 높아진다.

  • 버튼: 텍스트가 들어가지 않는 아이콘버튼이기에 title을 넣어주었다.
<button type="button" onClick={() => setOpenSearch(true)} title="search">
            <svg className="h-4 w-4 fill-current text-gray-700 dark:text-pink-50" xmlns="http://www.w3.org/2000/svg"
                version="1.1" id="Capa_1" x="0px" y="0px"
                viewBox="0 0 56.966 56.966" 
                width="512px" height="512px">
                <path
                d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23  s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92  c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17  s-17-7.626-17-17S14.61,6,23.984,6z" />
            </svg>
        </button>
  • 링크: aria-label 추가
<Link href={`/detail/${post._id}`} ref={ref} aria-label={post.title}>

개선 결과

성능: 55 -> 93

접근성: 85 -> 97

 

버튼의 색상대비가 충분하지 않긴하지만, 간단한 개선으로 어느정도 접근성을 올렸고, 리액트로 구현을 하더라도 기본 Html태그의 기본에 충실해야함을 인지하게 되었다.