이번 포스트에서는 이전 포스트에서 정리한 개선방법으로 Next.js 프로젝트의 Lighthouse 성능과 접근성을 개선한 사례를 설명하도록 하겠습니다.
Next.js에서 next/image
를 이용하면 이미지 최적화를 제공해줍니다. 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에서 다음의 이미지 최적화 방식을 자동으로 제공합니다.
next/image
에서 width
와 height
를 자동으로 제공해줌으로써 CLS를 방지합니다.next/image
에서 선택적으로 사용할 수 있는placeholder="blur"
와 함께 기본 브라우저에서 loading="lazy"
을 사용하여 뷰포트에 들어갈 때만 로드됩니다.next/image
에서 외부 서버에 저장된 이미지라도 On-demand image resizing를 제공합니다.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 통계자료)
(압축하지 않은) 일반 이미지와 텍스트 이미지의 크기 비교
일반 이미지와 텍스트 이미지의 압축 성능 비교
입력 코드
<Image
src="/main.webp"
alt="메인 이미지"
priority // 또는 priority=true
/>
priority
를 설정한 이미지는 높은 우선순위를 가지며, preload
의 이미지로 간주됩니다.preload
는 html에서 해당 리소스를 찾아내기 이전에 가능한 빠르게 브라우져가 알 수 있도록 중요한 리소스라고 알려주는 역할을 합니다.priority
를 설정하면, loading="lazy"
는 비활성화됩니다.입력 코드
// 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
이미지 형식을 사용했을 때 )
.avif
이미지 형식을 사용했을 때 ) 첫 번째 이미지 크기 약 2.5배 감소(60.9kB=>24.6kB), 두 번째 이미지 크기 약 3.2배 감소(78.4kB=>27.2kB)
이미지와 마찬가지로 Next.js에서 next/font를 이용하면 기본적인 이미지 최적화를 제공해줍니다.
next/font
에서는 프로젝트에서 설정한 폰트가 적용되기 이전의 기본 폰트(Flash of Unstyled text, FOUT)와 같은 크기의 폰트를 제공하여 주변 레이아웃에 영향을 주지 않습니다.next/font
의 adjustFontFallback
이란 속성으로 FOUT와 적용한 폰트의 glyph outline과 metrics가 같도록 size-adjust
라는 css 속성을 이용하여 폰트의 height
와 같은 속성을 같게 설정합니다.html
파일이 로드된 후, 이 파일이 link
하고 있는 font.googleapis.com에서 폰트를 다운로드합니다.html
파일이 이 로컬 파일을 link
하도록 구현합니다..eot
, .ttf
, .woff
, .woff2
등이 있습니다..eot
, .ttf
, .woff
보다 .woff2
의 폰트 크기가 작습니다. .woff2
의 내부 압축은 Brotli를 사용하여 .woff
보다 최대 30% 향상된 압축 기능을 제공합니다.subsets
을 설정하면, 여러 유니코드 범위로 분할하여 특정 페이지에 필요한 glyph만 전달할 수 있습니다. 이렇게 하면 파일 크기가 줄어들고 리소스 다운로드 속도가 향상됩니다.subsets
는 preload
가 참일 경우 head
의 link
태그 안에 rel="preload"
로 삽입되어 빠르게 사용할 수 있습니다.next/font/local
경우에는 사용할 수 없으므로, 폰트 변환을 한 뒤, 사용할 유니코드의 범위 만큼 서스셋 폰트를 만들어주어야 합니다.display="swap"
는 폰트가 다운로드하는 동안에 텍스트가 보이지 않는 현상을 방지합니다. (하지만, swap
을 이용하면 예상하지 못한 레이아웃 변경(CLS)을 일으킬 수 있으니 주의하세요. next/font
를 이용하면 CLS를 최소화시킬 수 있습니다.)// 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>
//...
)
}
visually-hidden는 사용자에게는 보여지지 않지만, 스크린 리더와 같은 보조 기술은 읽을 수 있도록 하는 CSS 패턴을 의미합니다.
(visually-hidden은 공식적인 명칭은 아니지만, 직관적으로 이해할 수 있는 이름이라 생각되어 그대로 설명하겠습니다.)
버튼 또는 링크 안에 텍스트 없이 아이콘을 사용하는 경우, 스크린 리더는 이해하지 못하게 됩니다. 왜냐하면 스크린 리더는 아이콘이나 이미지만을 가지고 개발자의 해당 요소의 의도를 완벽하게 이해할 수 없기 때문입니다. 만약 스크린 리더를 의존하는 사용자라면, 웹을 이용하는데 불편함을 느끼게 될 것입니다. 따라서 아이콘이나 이미지를 사용하되 그 안에 해당 요소를 설명하는 텍스트를 함께 입력하고 그 텍스트를 보이지 않게 CSS처리를 해주어야 합니다. 이때 텍스트를 보이지 않게 하는 CSS 패턴이 visually-hidden 패턴입니다. 아래 패턴의 코드를 이용하면 접근성을 높일 수 있습니다.
기존 코드
// 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;
}
리팩토링 이전 웹 성능과 접근성
리팩토링 이후 웹 성능과 접근성
이미지와 폰트와 같은 리소스를 최적화하는 방법에 대해 알아보았다. 개발하면서 자주 사용하는 이미지와 폰트를 설정해주는 것에도 아주 섬세한 작업이 필요하다는 것을 느끼게 되었다.
결과만 보았을 때 성능적으로 많이 좋아졌으나 여전히 LCP의 경우에는 좋은 점수를 얻지 못했다. 최종의 LCP는 이미지였는데, 크롬 개발자 도구 - Network부분에서 해당이미지의 Waterfall을 확인하면, 대부분의 시간을 Waiting for server response에서 소요하고 있었다. 서버에서 응답받는 속도를 줄일 수 있다면, 더 좋은 결과를 얻을 수 있을 것이다.
이번 포스트에서는 절대적인 리소스의 파일 크기를 줄여 성능을 높이는 것에 집중했다면, CDN(Content Delivery Network)과 같은 서버 네트워크를 이용한 캐싱 작업으로도 성능을 높일 수 있다는 글을 읽고, 추후에 공부하고 싶다는 생각이 들었다. 이번 리팩토링과정을 통해서 웹 성능과 접근성을 높인 과정에서 이미지와 폰트에 대해 더욱 자세히 알게 되어 흥미로웠고, 리소스에 대해 더 잘 다룰 수 있을 것 같은 자신감을 얻었다.