[리팩토링] Next.js 13로 Lighthouse 웹 성능 23점, 접근성 27점 개선하기

강풍윤·2023년 11월 12일
1

이번 포스트에서는 이전 포스트에서 정리한 개선방법으로 Next.js 프로젝트의 Lighthouse 성능과 접근성을 개선한 사례를 설명하도록 하겠습니다.

lighthouse 웹 성능 개선 전,후 이미지

1) 이미지 최적화

Next.js에서 next/image를 이용하면 이미지 최적화를 제공해줍니다. Next.js에서 어떤 이미지 최적화를 제공하는지 그리고 추가로 이미지 최적화를 할 수 있는 방법은 무엇이 있는지 정리해보았습니다.

Next.js에서 제공하는 이미지 최적화

입력 코드

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      alt="Picture of the author"
      // width={500} automatically provided
      // height={500} automatically provided
      // blurDataURL="data:..." automatically provided
      // loading="lazy" automatically provided
      // placeholder="blur" // Optional blur-up while loading
    />
  )
}

위의 코드는 next/image를 사용하는 예제코드입니다.
다음의 코드만 작성해주어도 Next.js에서 다음의 이미지 최적화 방식을 자동으로 제공합니다.

[1] Visual Stability(시각적 안정성, CLS 방지)

  • Cumulative Layout Shift(CLS)란 페이지가 로드될 때 예기치 않는 레이아웃의 변화없이 얼마나 안정적으로 보여주는지에 대한 지표입니다.
  • next/image에서 widthheight를 자동으로 제공해줌으로써 CLS를 방지합니다.

[2] Faster Page Loads(빠른 페이지 로드)

  • next/image에서 선택적으로 사용할 수 있는placeholder="blur"와 함께 기본 브라우저에서 loading="lazy"을 사용하여 뷰포트에 들어갈 때만 로드됩니다.

[3] Asset Flexibility(유연한 이미지 해상도 처리)

  • next/image에서 외부 서버에 저장된 이미지라도 On-demand image resizing를 제공합니다.
  • On-demand image resizing이란 이미지 요청시에 원본의 이미지를 원하는 해상도에 맞게 리사이징하여 전송하는 아키텍쳐입니다.
  • 원본 이미지 하나만 저장하고 필요에 따라 원하는 해상도의 이미지를 제공하기 때문에 스토리지 비용이 감소하고, 다양한 해상도의 이미지 처리를 쉽게 제공합니다.

[4] Size Optimization(이미지 크기 최적화)(여기서 크기는 용량을 의미합니다)

  • next/image에서 비교적 최신의 이미지 형식인 .webp.avif을 사용하여 각 장치에 맞는 올바른 크기의 이미지를 제공합니다.
  • .webp는 압축하지 않은 일반 이미지에 가장 적합한 형식입니다(.jpeg보다 1.42배 작음,.png보다 1.70배 작음, .avif보다 1.75배 작음)
  • .png는 압축하지 않은 텍스트 이미지에 가장 적합한 형식입니다(.jpeg보다 6.25배 작음, .webp보다 3.06배 작음, .avif보다 3.88배 작음)
  • 하지만, 압축한 일반 이미지의 경우 .avif가 가장 적합한 형식입니다(.webp보다 평균적으로 1.1배 작음)
  • 압축한 텍스트 이미지의 경우 .png가 가장 적합한 형식입니다(다른 형식의 이미지보다 1.01~1.4배 작음)
  • 텍스트가 들어간 이미지인지, 특정 브라우져에서 지원하는 이미지 형식인지 등의 조건에 따라 이미지 형식을 적절히 선택해야 합니다.

(위의 내용은 Matic Broz님이 약 6천 장의 이미지를 가지고, Manhattan distance, SSIM, MS-SSIM의 세 가지의 방법론으로 이미지의 크기와 질의 상관관계를 분석한 결과 내용입니다. Image Format Comparison (JPEG, PNG, WEBP, & AVIF) – 2023 Statistics 통계자료)

(압축하지 않은) 일반 이미지와 텍스트 이미지의 크기 비교
(압축하지 않은) 이미지와 텍스트 이미지의 크기 비교

일반 이미지와 텍스트 이미지의 압축 성능 비교
(압축한) 이미지와 텍스트 이미지의 크기 비교

직접 설정한 이미지 최적화

[1] priority 설정

입력 코드

<Image
  src="/main.webp"
  alt="메인 이미지"
  priority // 또는 priority=true
/>
  • priority를 설정한 이미지는 높은 우선순위를 가지며, preload의 이미지로 간주됩니다.
  • preload는 html에서 해당 리소스를 찾아내기 이전에 가능한 빠르게 브라우져가 알 수 있도록 중요한 리소스라고 알려주는 역할을 합니다.
  • priority를 설정하면, loading="lazy"는 비활성화됩니다.
  • Largest Contentful Paint(LCP)에 감지된 이미지에 설정하는 것이 적절합니다.

[2] avif, webp 이미지 형식 변환

입력 코드

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  // ...
  images: {
    formats: ["image/avif", "image/webp"],
  },
};

module.exports = nextConfig;
  • .avif를 지원하는 브라우저에는 .avif를 사용하고, 지원하지 않는 브라우저에는 .webp로 설정할 수 있도록 next.config.js를 위의 코드처럼 작성해주었습니다.

.webp 이미지 형식을 사용했을 때 )
webp 형식 이미지 사용시 용량 및 속도

.avif 이미지 형식을 사용했을 때 ) 첫 번째 이미지 크기 약 2.5배 감소(60.9kB=>24.6kB), 두 번째 이미지 크기 약 3.2배 감소(78.4kB=>27.2kB)
avif 형식 이미지 사용시 용량 및 속도

2) 폰트 최적화

이미지와 마찬가지로 Next.js에서 next/font를 이용하면 기본적인 이미지 최적화를 제공해줍니다.

Next.js에서 제공하는 폰트 최적화

[1] Zero layout shift

  • next/font에서는 프로젝트에서 설정한 폰트가 적용되기 이전의 기본 폰트(Flash of Unstyled text, FOUT)와 같은 크기의 폰트를 제공하여 주변 레이아웃에 영향을 주지 않습니다.
  • next/fontadjustFontFallback이란 속성으로 FOUT와 적용한 폰트의 glyph outline과 metrics가 같도록 size-adjust라는 css 속성을 이용하여 폰트의 height와 같은 속성을 같게 설정합니다.

[2] Pre-download Google Font

  • next.js 13 이전에서는 html 파일이 로드된 후, 이 파일이 link 하고 있는 font.googleapis.com에서 폰트를 다운로드합니다.
  • next.js 13부터는 빌드 시 google font를 다운로드하여서 로컬 디렉터리에 저장한 뒤, html 파일이 이 로컬 파일을 link 하도록 구현합니다.
  • 서로 다른 도메인 간 Connection 연결을 위한 handshaking 과정 없이, 이미 HTML 파일을 다운로드하기 위해 생성했던 Connection을 그대로 사용할 수 있기 때문에 전자보다 비교적 빠른 속도로 파일을 다운로드할 수 있습니다.

직접 설정한 폰트 최적화

[1] 폰트 크기 최적화

  • 폰트 형식에는 .eot, .ttf, .woff, .woff2 등이 있습니다.
  • 레거시 폰트 형식인 .eot, .ttf, .woff 보다 .woff2의 폰트 크기가 작습니다. .woff2의 내부 압축은 Brotli를 사용하여 .woff보다 최대 30% 향상된 압축 기능을 제공합니다.
  • cloudconvert와 같은 폰트 형식 변환 프로그램을 이용하여 폰트 형식을 변환시킬 수 있습니다.

[2] subset 설정(next/font/google만 가능)

  • subsets을 설정하면, 여러 유니코드 범위로 분할하여 특정 페이지에 필요한 glyph만 전달할 수 있습니다. 이렇게 하면 파일 크기가 줄어들고 리소스 다운로드 속도가 향상됩니다.
  • subsetspreload가 참일 경우 headlink 태그 안에 rel="preload"로 삽입되어 빠르게 사용할 수 있습니다.
  • next/font/local 경우에는 사용할 수 없으므로, 폰트 변환을 한 뒤, 사용할 유니코드의 범위 만큼 서스셋 폰트를 만들어주어야 합니다.
  • 서브셋 폰트는 サブセットフォントメーカー(이하 서브셋 폰트 메이커)fontTools 라이브러리를 사용해 만들 수 있습니다.

[3] display="swap" 설정

  • 텍스트 노드에 의해 Largest Contentful Paint(LCP)의 요소로 지정될 수 있습니다. 이런 경우 가능한 빠르게 텍스트를 보여줌으로써 해결해줄 수 있어야 합니다.
  • display="swap"는 폰트가 다운로드하는 동안에 텍스트가 보이지 않는 현상을 방지합니다. (하지만, swap을 이용하면 예상하지 못한 레이아웃 변경(CLS)을 일으킬 수 있으니 주의하세요. next/font를 이용하면 CLS를 최소화시킬 수 있습니다.)

[4] 입력 코드(사용 사례)

// src/app/styles/font.ts
import localFont from "next/font/local";

const GowunBatang = localFont({
  src: [
    { path: "./GowunBatang-Regular.woff2", weight: "400", style: "normal" },
    { path: "./GowunBatang-Bold.woff2", weight: "700", style: "normal" },
  ],
  display: "swap",
});

const Agdasima = localFont({
  src: [
    { path: "./Agdasima-Regular.woff2", weight: "400", style: "normal" },
    { path: "./Agdasima-Bold.woff2", weight: "700", style: "normal" },
  ],
  display: "swap",
});

export { GowunBatang, Agdasima };
// page.tsx
import { GowunBatang, Agdasima } from "@/app/styles/font";

export default function page() {
	return ( 
      //...
    	<h2 className={GowunBatang.className}>
  		  제목
  		</h2>
		<p className={Agdasima.className}>
          글 내용
        </p>
      //...
    )
}  

3) 접근성 개선

visually-hidden 패턴 설정

visually-hidden는 사용자에게는 보여지지 않지만, 스크린 리더와 같은 보조 기술은 읽을 수 있도록 하는 CSS 패턴을 의미합니다.
(visually-hidden은 공식적인 명칭은 아니지만, 직관적으로 이해할 수 있는 이름이라 생각되어 그대로 설명하겠습니다.)

접근 가능한 이름이 없는 경우의 문제

버튼 또는 링크 안에 텍스트 없이 아이콘을 사용하는 경우, 스크린 리더는 이해하지 못하게 됩니다. 왜냐하면 스크린 리더는 아이콘이나 이미지만을 가지고 개발자의 해당 요소의 의도를 완벽하게 이해할 수 없기 때문입니다. 만약 스크린 리더를 의존하는 사용자라면, 웹을 이용하는데 불편함을 느끼게 될 것입니다. 따라서 아이콘이나 이미지를 사용하되 그 안에 해당 요소를 설명하는 텍스트를 함께 입력하고 그 텍스트를 보이지 않게 CSS처리를 해주어야 합니다. 이때 텍스트를 보이지 않게 하는 CSS 패턴이 visually-hidden 패턴입니다. 아래 패턴의 코드를 이용하면 접근성을 높일 수 있습니다.

[1] 입력 코드(사용 사례)

기존 코드

// page.tsx
<button className="m-navbar-tabmenu" onClick={toggle}>
  <FontAwesomeIcon icon={faBars} style={{ color: "#2f3438" }} /> // 메뉴 아이콘
</button>

변경 코드

// page.tsx
<button className="m-navbar-tabmenu" onClick={toggle}>
  <FontAwesomeIcon icon={faBars} style={{ color: "#2f3438" }} /> // 메뉴 아이콘
  <span className="visually-hidden">메뉴 버튼</span> // visually-hidden 요소
</button>
/** globals.css */
.visually-hidden {
  overflow: hidden;
  position: absolute;
  width: 1px;
  height: 1px;
  white-space: nowrap;
}

4) 결과

리팩토링 이전 웹 성능과 접근성
리팩토링 이전의 웹 성능과 접근성

리팩토링 이후 웹 성능과 접근성
리팩토링 이후의 웹 성능과 접근성

  • First Contentful Paint의 경우, 1.4초에서 1.0초로 약 0.4초를 단축시켰다.
  • Largest Contentful Paint의 경우, 11.5초에서 4.7초로 약 6.8초를 단축시켰다.
  • Total Blocking Time의 경우, 0.29초에서 0.12초로 약 0.17초를 단축시켰다.
  • Cumulative Layout Shift의 경우, 0.027에서 0으로 약 0.027만큼 감소시켰다.(=>레이아웃이 바뀌는 현상이 전혀 없습니다.)
  • Speed Index의 경우, 24.0초에서 1.7초로 약 22.3초를 단축시켰다.
  • 최종적으로 성능(Performance)면에서 23점을 개선하였고, 접근성(Accessibility)면에서는 27점을 개선했습니다.

5) 마치며

이미지와 폰트와 같은 리소스를 최적화하는 방법에 대해 알아보았다. 개발하면서 자주 사용하는 이미지와 폰트를 설정해주는 것에도 아주 섬세한 작업이 필요하다는 것을 느끼게 되었다.

결과만 보았을 때 성능적으로 많이 좋아졌으나 여전히 LCP의 경우에는 좋은 점수를 얻지 못했다. 최종의 LCP는 이미지였는데, 크롬 개발자 도구 - Network부분에서 해당이미지의 Waterfall을 확인하면, 대부분의 시간을 Waiting for server response에서 소요하고 있었다. 서버에서 응답받는 속도를 줄일 수 있다면, 더 좋은 결과를 얻을 수 있을 것이다.

이번 포스트에서는 절대적인 리소스의 파일 크기를 줄여 성능을 높이는 것에 집중했다면, CDN(Content Delivery Network)과 같은 서버 네트워크를 이용한 캐싱 작업으로도 성능을 높일 수 있다는 글을 읽고, 추후에 공부하고 싶다는 생각이 들었다. 이번 리팩토링과정을 통해서 웹 성능과 접근성을 높인 과정에서 이미지와 폰트에 대해 더욱 자세히 알게 되어 흥미로웠고, 리소스에 대해 더 잘 다룰 수 있을 것 같은 자신감을 얻었다.

참고 사이트

profile
https://github.com/KANGPUNGYUN

0개의 댓글