원문 : https://www.lydiahallie.io/blog/optimizing-webfonts-in-nextjs-13
웹 폰트는 현대 웹 디자인의 필수 요소입니다. 웹상에서 아름다운 타이포그래피를 가능하게 하며 여러분의 제품에 유니크함을 더해줍니다. ✨
그러나 웹 폰트를 사용하면 문제가 생길 수도 있습니다. 여러분이 인터넷을 자주 사용하신다면 다음과 같은 현상을 경험했을 가능성이 있습니다.
웹 폰트가 다운로드되어 설치될 때까지는 Times New Roman 및 Arial 등의 기본 폰트를 처음 표시하는 웹 사이트로 이동합니다. 그 다음 폰트를 기본값에서 웹 폰트로 빠르게 전환해서 종종 레이아웃 시프트(layout shift)를 일으킵니다.
레이아웃 시프트는 몇 픽셀 이동부터 큰 폰트나 다른 줄 바꿈 위치에 따른 큰 시프트까지 다양할 수 있습니다.
이 현상을 FOUT(Flash Of Unstyled Text)이라고 합니다. 웹 폰트를 다운로드하는 데 시간이 오래 걸리면 페이지의 텍스트가 "깜빡임"으로 표시되거나 기본 폰트에서 웹 폰트로 변경될 수 있습니다.
다른 경우에는 웹 폰트가 다운로드될 때까지 텍스트가 표시되지 않을 수 있습니다.
이를 FOIT(Flash Of Invisible Text)라고 하며, 이 경우 브라우저는 보이지 않는 대체 폰트를 그립니다. 이는 font-display
CSS 속성이 block
으로 설정된 경우에 발생합니다.
FOUT과 FOIT가 모두... 산만하고 혼란스럽다는 데 우리 모두 동의할 수 있습니다. 그러나 FOUT 및 FOIT를 완전히 피하는 것은 어렵습니다.
웹 폰트를 사용할 때의 주요 문제 중 하나는 브라우저가 폰트를 렌더링하고 페이지에 표시하기 전에 폰트를 다운로드해야 한다는 것입니다. 특히 사용자의 인터넷 연결 속도가 느리거나 웹 폰트가 큰 경우 시간이 걸릴 수 있습니다.
다행히 웹 폰트로 작업할 때 조금 편하게 할 수 있는 몇 가지 최적화 방법이 있습니다!
최적화
최적화를 다루기 전에 먼저 우리가 다루고 있는 문제를 이해하기 위해 가장 기본적인 최적화되지 않은 접근 방식을 살펴보겠습니다.
다음 예에서는 웹 폰트를 사용하는 경우 Next.js 구현을 나타냅니다. 단, 이러한 예와 기법은 Next.js에 고유한 것이 아니라 다른 프레임워크 또는 HTML만을 사용하는 경우에도 적용할 수 있다는 점에 주의하는 것이 중요합니다.
HTTP 연결
Next.js 프로젝트에 웹 폰트를 추가하기 위해 _document.js
파일의 head
에서 link
태그를 사용할 수 있습니다. _document.js
파일에서 Document
컴포넌트를 수정하고 폰트 스타일시트를 Head
컴포넌트의 자식으로 전달할 수 있습니다.
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap" rel="stylesheet" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
위에서 언급한 코드를 _document.js
의 콘텐츠로 사용해서 페이지를 로드하면 브라우저는 웹 폰트가 페이지에 제대로 로드되고 표시되는지 확인하기 위해 여러 단계를 거칩니다.
- 브라우저는 초기 HTML을 읽는 것으로 시작해서 외부 스타일시트로 식별되는
<link>
태그를 발견합니다. - 그런 다음 HTML 파서는
rel
속성을 확인해서 링크가 참조하는 리소스 유형을 파악합니다. 이 경우는 CSS 스타일시트(stylesheet)입니다. - 그 다음 HTML 파서는 지정된 URL
fonts.googleapis.com
에서 서버로 HTTP 요청을 보내고 웹 폰트의 CSS 규칙과 스타일이 포함된 응답을 받습니다.이 경우는 다음과 같습니다.
/* latin */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/nunitosans/v12/....woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, ...;
}
가독성을 위해 위의 예시에서는 라틴어 하위 집합만 포함했습니다. 실제로 응답에는 더 많은 부분 집합이 포함되어 있습니다.
- 스타일시트가 동기적으로 다운로드된 후 브라우저는 DOM 및 CSSOM을 사용하여 렌더링 트리를 구성합니다. 이 초기 렌더링 트리는 사용자에게 대체 폰트를 보여줍니다.
- 렌더링 트리가 완전히 구성되고 첫 번째 페인트가 발생하면 브라우저는
fonts.gstatic.com
에 요청을 보내@font-face
의src
속성에 지정된 실제 폰트 파일을 다운로드합니다. - 마지막으로 브라우저는 폰트 파일을 사용하여 페이지의 폰트를 렌더링하여 웹 폰트가 올바르게 표시되도록 합니다.
스타일시트와 폰트 요청을 모두 자세히 살펴보면 단순히 리소스만 다운로드하는 것만이 아니라는 것을 알 수 있습니다. 우리는 다른 서버에 접속하고 있기 때문에 브라우저는 먼저 DNS 조회, TLS 협상 및 TCP 핸드셰이크를 사용하여 연결을 설정해야 합니다.
연결을 설정하는 데 총 요청 시간이 꽤 걸릴 수 있습니다. 요청 시 이를 방지하기 위해 https://fonts.googleapis.com
및 https://fonts.gstatic.com
양쪽에 preconnect
브라우저 힌트를 사용하여 두 개의 <link>
태그를 추가할 수 있습니다.
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Nunito+Sans&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
이 태그는 페이지 로드가 시작되는 즉시 리소스가 호스트되고 있는 서버에 대한 연결을 설정하도록 브라우저에 지시합니다. 이를 통해 요청하고 응답을 받는데 걸리는 시간을 단축할 수 있습니다.
preconnect
브라우저 힌트는 요청 시간을 단축하고 웹 사이트에서 웹 폰트를 로드할 때 FOIT 및 FOUT를 방지하는 데 큰 도움이 됩니다. 이 기술을 사용하면 페이지 로드가 시작되는 즉시 폰트가 호스팅되고 있는 서버에 대한 연결을 설정할 수 있습니다. 이렇게 하면 폰트 다운로드 및 표시 속도가 크게 향상됩니다.
Font-Face 지시어(Descriptors)
우리는 웹 폰트를 가져오는 데 걸리는 시간을 줄이는 방법을 방금 알아봤지만 아직 레이아웃 시프트를 해결하지 못했습니다.
폰트를 다룰 경우 폰트의 특성을 설명하는 데 사용되는 몇 가지 다른 측정값이 있습니다. 폰트 내에서 가장 중요한 측정값은 다음과 같습니다.
- 어센더(Ascender): x-높이 위로 뻗는
b
,d
,f
,h
,k
및l
과 같이 특정 문자에서 어센더의 상단을 정의하는 라인입니다. - 민 라인(Meanline):
o
,d
처럼 그릇 모양의 소문자 상단을 정의하는 라인입니다. - 베이스 라인(Baseline): 대부분의 글자가 있는 줄로 텍스트 줄에 글자를 배치하기 위한 기준점으로 사용됩니다.
- X-높이(X-Height): 베이스 라인과 민 라인(meanline)의 차이를 정의하는 높이로 이 속성은 문자 크기와 간격을 결정하는 데 도움이 됩니다.
- 디센더(Descender):
g
,j
,p
,q
, 및y
처럼 특정 문자에서 디센더의 하단을 정의하는 선으로 베이스 라인 아래로 뻗는 글자 부분을 말합니다.
폰트는 크기가 각기 다르기 때문에 대체 폰트와 웹 폰트의 font-size
가 같더라도 레이아웃 시프트가 발생할 수 있습니다.
분홍색 라인은
x-높이
를 나타내며 민 라인(meanline)과 베이스 라인의 차이를 의미합니다.
따라서 font-size
외에 렌더링된 폰트의 크기를 더 많이 제어해서 레이아웃 시프트를 줄여야 합니다. 한 가지 처리 방법은 size-adjust
font-face 지시어를 사용하는 것입니다.
size-adjust
size-adjust
font-face 지시어는 폰트와 관련된 모든 지표를 지정된 백분율로 조정합니다. 웹 폰트에 맞게 크기를 조정하면 대체 폰트에서 웹 폰트로 폰트를 교체하더라도 텍스트 크기가 거의 동일하게 유지되어 페이지 레이아웃의 예기치 않은 변경을 방지할 수 있습니다.
웹 사이트에서 Poppins 웹 폰트를 사용하고 싶지만 웹 폰트가 로드되지 않는 경우를 대비해서 Baskerville 글꼴을 대체 폰트로 사용하고 싶다고 가정해 보겠습니다. 이 경우 size-adjust
속성을 사용해서 사용 중인 폰트에 관계없이 텍스트 크기가 일관되게 유지되도록 할 수 있습니다. 즉, Baskerville 폰트와 일치하도록 Poppins 폰트의 크기를 조정하거나 그 반대로 조정할 수 있습니다.
분홍색 라인은 x-높이를 나타내며 민 라인(meanline)과 베이스 라인의 차이를 의미합니다.
font-size
는 CSS에서 같은 엘리먼트로 설정되어 있는데 이 속성을 사용해서 대체 폰트의 크기를 변경할 수 없습니다. 그 이유는 대체 폰트와 웹 폰트의 크기 차이를 구별할 수 없기 때문입니다. 단, font-face
속성에서 size-adjust
속성을 사용하면 폰트 크기를 설정할 수 있습니다. 그러면 해당 폰트를 사용하는 모든 텍스트에 조정된 측정값이 적용됩니다.
h1 {
font-family: "Poppins", "Baskerville";
font-size: 64px;
}
@font-face {
font-family: "Poppins";
src: url(https://fonts.googleapis.com/css2?family=Poppins) format("woff2");
}
@font-face {
font-family: "Baskerville";
src: local("Baskerville");
size-adjust: 125%;
}
이제 대체 폰트의 크기가 웹 폰트와 거의 일치하므로 대부분의 레이아웃 시프트를 피합니다.
size-adjust
속성을 조정하면 웹 폰트를 사용할 때 레이아웃 시프트를 방지할 수 있지만 항상 충분하지는 않습니다.
렌더링된 글꼴을 더 많이 제어하려면 ascent-override
, descent-override
및 line-height-override
와 같은 다른 CSS 지시어를 사용해서 각각 ascent 지표(민 라인 위의 높이), descent 지표(베이스 라인 아래 높이) 및 라인 높이를 정의할 수 있습니다. 이렇게 하면 대체 폰트와 웹 폰트의 레이아웃 및 모양이 거의 일치하는 것을 확인할 수 있습니다.
웹 폰트가 sans-serif인 경우에도 serif의 대체 폰트로 사용해 왔는데, 사용 중인 폰트의 종류와 일치하는 대체 폰트를 선택하면 더 좋은 결과를 얻을 수 있습니다. 예를 들어 sans-serif 웹 폰트를 사용하는 경우 Arial 같은 sans-serif 대체 폰트를 선택해야 합니다. 이를 통해 폰트 측정값이 가능한 한 유사하도록 할 수 있습니다.
@next/font
모듈
preconnect
및 font-face
지시어를 추가하면 로딩 시간을 단축하고 특성을 정의해서 웹 사이트에서 폰트의 성능을 향상시킬 수 있습니다. 그러나 이러한 최적화를 수동으로 추가하는 것은 지루하고 오류가 발생하기 쉽습니다. 이상적으로는 폰트가 자동으로 최적화되기를 원합니다.
여기서 @next/font
모듈이 등장합니다. 이 모듈은 Next.js 13에 도입되었으며 최적화 세부 사항에 대해 걱정하지 않고 웹 사이트에 로컬 및 Google 폰트를 쉽게 추가할 수 있습니다.
@next/font
모듈을 사용하면 자동으로 로컬 및 웹 폰트를 훨씬 쉽게 조작할 수 있습니다.
- 빌드 시 웹 폰트를 다운로드하고 로컬에서 제공
- 대체 폰트를 추가하고 선택한 웹 폰트와 최대한 유사하도록 크기를 자동으로 조정
@next/font
모듈에서 폰트를 가져오고 폰트 인스턴스를 사용해서 웹 사이트의 특정 컴포넌트에 적용하여 사용할 수 있습니다.
import { Poppins } from "@next/font/google";
const poppins = Poppins({ weight: "600", subsets: ["latin"] });
export default function Title() {
return <h1 className={poppins.className}>The Acme Blog</h1>;
}
자체 호스팅 폰트
빌드 시 @next/font
모듈은 fonts.googleapis.com
에서 폰트 스타일시트와 이전에 가져온 @font-face
src
에 정의된 폰트 파일을 모두 다운로드합니다.
이제 외부 요청 없이 폰트를 로컬에서 사용할 수 있습니다.
2022년 1월, 독일 법원은 Google 폰트를 사용하는 것이 GDPR 규정을 위반한다고 판결했습니다.
@next/font 모듈은 웹 폰트를 자체 호스팅할 수 있도록 해서 개인 정보를 개선하는 데 도움이 됩니다.
자동 매칭 대체(Automatic Matching Fallback)
@next/font
모듈은 자신의 도메인에서 폰트를 제공할 수 있을 뿐만 아니라 의도한 웹 폰트와 거의 일치하는 사용자 지정 대체 폰트를 자동으로 제공합니다. 또한 size-adjust
, ascent-override
, descent-override
및 lineheight-override
와 같은 필요한 지표를 계산해서 대체 폰트가 웹 폰트와 거의 일치하도록 합니다.
/* latin */
@font-face {
font-family: '__Poppins_9e171c';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/_next/static/media/d869208648ca5469.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, ...;
}
@font-face {
font-family: '__Poppins_Fallback_9e171c';
src: local("Arial");
ascent-override: 92.83%;
descent-override: 30.94%;
line-gap-override: 8.84%;
size-adjust: 113.11%
}
대체 폰트 값은 빌드 시 자동으로 계산됩니다.
@next/font
모듈을 사용해서 웹사이트에 텍스트를 표시하는 경우 글꼴이 변경되어도 레이아웃 시프트가 거의 또는 전혀 발생하지 않습니다.
Preloading
@next/font
모듈은 또한 subset
이 정의되어 있는 경우 자동으로 폰트를 미리 로드합니다. 이는 성능과 사용자 경험 모두에 도움이 될 수 있습니다.
미리 로드된 폰트는 일반적으로 브라우저가 페이지 렌더링을 시작하기 전에 가능한 한 빨리 다운로드됩니다. 이렇게 하면 페이지가 로드되는 동안 폰트가 잘못 렌더링되거나 변경될 가능성을 방지해서 원활한 경험을 제공합니다.
결론
FOUT 및 FOIT와 같은 문제로 인해 웹 폰트를 사용하는 것이 어려울 수 있습니다. 그러나 preconnect
를 추가하고 size-adjust
, ascent-override
, descent-override
및 lineheight-override
와 같은 폰트 지시어를 사용하는 등 일부 최적화를 통해 웹 폰트의 성능과 모양을 개선할 수 있습니다.
@next/font
모듈은 이러한 최적화를 자동으로 구현하고 자체 도메인에서 폰트를 제공함으로써 웹 폰트 조작을 훨씬 쉽게 만듭니다. 이를 통해 보다 원활한 사용자 경험을 만들고 웹 사이트의 전체적인 성능을 개선해서 Core Web Vitals가 향상됩니다.
'아티클 번역' 카테고리의 다른 글
[번역] Ecma 인터네셔널에서 ECMAScript 2022를 승인했습니다. 새로운 기능은 무엇인가요? (0) | 2022.12.11 |
---|---|
[번역] 자바스크립트 번들러 만들기 (0) | 2022.12.11 |
[번역] 전문가처럼 타입스크립트 infer 사용하기 (0) | 2022.12.11 |
[번역] 문 vs 표현식 (0) | 2022.12.11 |
[번역] JavaScript 패키지 매니저 비교 - npm, Yarn 또는 pnpm? (0) | 2022.12.11 |