[Next.js] SEO - 5. Improving your Core Web Vitals
Introducing Lighthouse
앞서 봤듯, Core Web Vitals은 로딩 수행력(LCP), 상호작용(FID), 시가적 안전성(CLD)를 통해 사용자 경험에 집중한다.
이번에는 Lighthouse라는 시뮬레이션 환경을 사용하여 Core Web Vitals을 어떻게 측정하는지 배운다.
Lighthouse는 제공된 URL에 대한 일련의 감사를 실행하여 작동한다. 이러한 감사를 기반으로 페이지가 얼마나 잘 수행되었는지에 대한 보고서를 생성한다. 개선이 필요한 영역이 있으면, 보고서는 개선방법에 대한 통찰력을 제공한다.
Lighthouse 보고서의 두 가지 예를 살펴보고 건강한 Core Web Vitals가 있는 사이트와 그렇지 않은 사이트의 차이점을 살펴보자.
Optimized Example
Lighthouse가 작동하는 것을 보기 위해서 우리의 홈페이지를 사용할 것이다: https://nextjs.org
- 크롬을 열고
- https://nextjs.org 에 들어가서
- Lighthouse 탭을 누른다.
- Generate Report를 클릭한다.
이제 60초 미만이 소요되는 보고서가 실행된다.
참고: 타사 플러그인이 보고서에 영향을 미치므로 시크릿 창에서 보고서를 실행하는 것이 중요하다.
또한 관고 차단기는 스크립트 로드를 차단하여 불완전한 검사를 제공할 수 있다. 성능을 측정하기 위해서 깨끗한 개인 설정을 고려하자.
예시 검사 결과이다:
Unoptimized Example
이 튜토리얼의 목적을 위해서, 어떠한 최적화 없이 앱을 만들었다.
Project Setup
이는 방문자가 두 가지 작업을 수행할 수 있도록 하는 최적화되지 않은 기본 앱이다: 인구를 검색할 국가를 검색하고 팝업 모달을 읽으려면 버튼을 클릭한다. 이 앱은 타사 라이브러리의 사용을 피할 수 없는 대규모 응용 프로그램에서 작업하는 현실을 모방하기 위한 것이다.
Download Starter Code
yarn create next-app nextjs-lighthouse --example "https://github.com/vercel/next-learn/tree/master/seo"
Run Production Build
Lighthouse에서 정확한 보고서를 얻으려면 애플리케이션을 항상 프로덕션 빌드로 테스트해야 한다. 프로덕션 빌드를 실행하려면 프로젝트 디렉토리를 변경한다.
cd nextjs-lighthouse
next build 를 실행하여 앱을 빌드하고 next start를 실행하여 프로덕션 모드에서 서버를 시작한다.
yarn build && yarn start
Chrome DevTools로 Lighthouse 보고서를 실행한다. 보고서가 완료되면 사이트를 최적화하고 핵심 기능을 개선해보자.
Image Component and Automatic Image Optimization
Unoptimized Images
일반 HTML을 사용하여 다음과 같이 영웅 이미지를 추가한다.
<img src="large-image.jpg" alt="Large Image" />
하지만 이는 다음과 같은 몇 가지를 수동으로 최적화해야 함을 의미한다.
- 다양한 화면 크기에서 이미지가 반응하도록 한다.
- 타사 도구 또는 라이브러리를 사용하여 이미지 최적화 한다.
- 뷰포트에 들어갈 때 이미지를 지연로딩 한다.
The Image Component
Next는 이러한 최적화를 즉시 처리할 수 있는 Image 컴포넌트를 제공한다.
next/image는 현대 웹용으로 발전된 HTML img엘리먼트의 확장이다. 이는 next/image를 사용하여 WebP와 같은 최신 형식의 이미지 크기 조정, 최적화 및 제공을 자동으로 수행할 수 있다.
이 컴포넌트는 뷰포트가 더 작은 장치에 큰 이미지를 배송하는 것을 방지하고 Next.js가 미래의 이미지 형식을 채택하고 해당 이미지를 지원하는 브라우저에 제공할 수 있도록한다.
자동 이미지 최적화는 모든 이미지 소스에서 작동한다. 이미지가 CMS와 같은 외부 데이터 소스에서 호스팅되는 경우에도 여전히 최적화할 수 있다.
How does Automatic Image Optimization Work?
On-demand Optimization
빌드 시 이미지를 최적화하는 대신, 유저가 요청할 때 이미지를 최적화한다. 정적 사이트 생성기 및 정적 전용 솔루션과 달리, 10개의 이미지를 전송하든 천만개의 이미지르르 전송하든 빌드 시간이 늘어나지 않는다.
Lazy Loaded Images
이미지는 기본적으로 지연로드된다. 뷰포트 외부에 보관된 이미지에 대해서는 페이지 속도에 영향을 미치지 않는다. 이미지는 뷰에 들어올 때에만 로드된다.
Avoids CLS
이미지는 CLS(Cumulative Layout Shift - 전체 레이아웃 안전성 측정)를 피하기위해 항상 렌더링된다.
Using the Image Component
next/image를 사용하여 앱을 업데이트하여 영웅 이미지를 보여주자. 높이 및 너비 props는 소스 이미지와 비율이 동일한 렌더링 크기여야 한다.
pages/index.js 파일을 열고 Image를 import한다.
import Image from 'next/image'
그리고 영웅 img를 Image 컴포넌트로 변경한다.
// Before
<img src="large-image.jpg" alt="Large Image" />
// After
<Image src="/large-image.jpg" alt="Large Image" width={3048} height={2024} />
footer에도 이미지가 있다. 이것도 변경한다:
// Before
<img src="/vercel.svg" alt="Vercel Logo" />
// After
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
마지막으로 크롬 개발자 도구에서 다른 LightHouse 보고서를 실행하고 결과를 비교한다.
Dynamic Imports
이번에는 타사 라이브러리에서 초기 페이지 로드 중에 로드되는 JavaScript의 양을 줄일것이다.
Next.js는 JavaScript를 위한 ES2020의 동적 import()를 지원한다. 이를 가지고 JavaScript 모듈을 동적으로 가져와서 작업할 수 있다으며, SSR(서버사이드 렌더링)과 함께 작동한다.
동적 import는 코드를 관리 가능한 청크로 분할하는 또 다른 방법으로 생각한다.
pages/index.js 파일을 열고 파일 시작 부분에서 이 두가지 import를 제거한다. 파일을 더 아래쪽에서 동적으로 가져올 것이다.
import Fuse from 'fuse.js'
import _ from 'lodash'
아래를 추가로 제거한다:
const fuse = new Fuse(countries, {
keys: ['name'],
threshold: 0.3
})
이제 코드를 삭제했으므로, 동적으로 import한 라이브러리를 사용하기위한 검색 input을 설정하자.
input을 아래의 코드로 변경한다:
<input
type="text"
placeholder="Country search..."
className={styles.input}
onChange={async e => {
const { value } = e.currentTarget
// Dynamically load libraries
const Fuse = (await import('fuse.js')).default
const _ = (await import('lodash')).default
const fuse = new Fuse(countries, {
keys: ['name'],
threshold: 0.3
})
const searchResult = fuse.search(value).map(result => result.item)
const updatedResults = searchResult.length ? searchResult : countries
setResults(updatedResults)
// Fake analytics hit
console.info({
searchedAt: _.now()
})
}}
/>
동적 import를 사용하면 페이지 로드 시 "사용하지 않는 JavaScript 제거" 문제가 해결된다. 또한 TTI(Time to Interactive)를 개선하여 FID(First Input Delay)를 개선하는데 도움이 된다.
Dynamic Imports for Components
다음으로 초기 페이지 로드 시 필요하지 않은 React 컴포넌트에 대해 알아보자.
React 컴포넌트는 동적 import를 사용하여 가져올 수도 있지만, 이 경우 next/dynamic과 함께 사용하여 다른 React 컴포넌트처럼 작동하도록 한다.
Hello World 코드 샘플로 모달 로드를 지연하기 위해 이 방법을 사용할 것이다. 이렇게 하면 초기 페이지 로드에서 두 개의 추가 타사 라이브러리도 제거된다.
pages/index.js 파일을 열고 dynamic을 import하여 파일 시작부분에 코드를 작성한다:
import dynamic from 'next/dynamic'
아래 코드를 제거한다:
import CodeSampleModal from '../components/CodeSampleModal'
이제 dynamic 컴포넌트를 import한다.
const CodeSampleModal = dynamic(() => import('../components/CodeSampleModal'), {
ssr: false
})
CodeSampleModal은 ../components/CodeSampleModal에서 반환되는 기본 구성 요소이다. 일반 React 컴포넌트처럼 작동하며 평소처럼 props를 전달할 수 있다.
서버에서 이 컴포넌트를 필요하지 않을 때, ssr: false를 사용하여 비활성화 한다.
다음으로 사용자가 요구할 때까지 이 컨포넌트의 로딩을 연기하고자 한다. 이렇게 하려면 모달이 열려 있어야 하는지 확인하는 조건으로 컴포넌트를 래핑할 수 있다. 열려 있다면 로드될것이다.
CodeSampleModal 컴포넌트를 아래와 같이 감싼다.
{
isModalOpen && (
<CodeSampleModal
isOpen={isModalOpen}
closeModal={() => setIsModalOpen(false)}
/>
)
}
이제 isModalOpen이 처음으로 true로 바뀌면, 필요한 JavaScript가 요청된다.
이러한 최적화를 통해 이제 vitals가 더 건강해 보일 것이다. 크롬 개발자 도구에서 다른 LightHouse 보고서를 실행해 확인해보자.
Optimizing Fonts
데스크탑용 웹 페이지의 82%는 웹 폰트를 사용한다. 사용자 폰트는 사이트의 브랜딩, 디자인, 크로스 브라우저 및 디바이스 일관성을 위해 중요하다. 그러나 웹 폰트는 사용하는 것이 성능을 희생시켜서는 안된다.
Next.js는 자동 웹폰트 최적화를 내장으로 가지고 있다. 기본적으로 Next.js는 빌드 시 자동적으로 인라인 폰트 CSS가 되므로 글꼴 선언을 가져오기 위한 추가 왕복이 필요하지 않다. 이는 FCP(First Contentful Paint) 및 LCP(Largest Contentful Paint) 개선을 가져온다.
최적화 전에는 추가적인 네트워크 요청이 필요하다:
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
최적화 이후에 Next.js는 폰트 CSS는 바로 적용할 수 있다.
<style data-href="https://fonts.googleapis.com/css2?family=Inter">
@font-face{font-family:'Inter';font-style:normal.....
</style>