웹사이트에 접속했을 때, 처음에는 기본 폰트로 텍스트가 보이다가 갑자기 예쁜 웹 폰트로 바뀌면서 줄 바꿈이 변하거나 문단이 아래로 툭 떨어지는 현상을 겪어보신 적이 있나요?

사용자를 당황하게 만드는 이 현상을 FOUT(Flash of Unstyled Text)라고 하며, 이는 구글의 핵심 성능 지표 중 하나인 CLS(Cumulative Layout Shift) 점수를 크게 갉아먹는 주범입니다. 오늘은 웹 폰트 로딩 최적화를 통해 시각적 안정성을 확보하는 실무 전략을 깊이 있게 알아보겠습니다.


1. CLS의 적, 웹 폰트 로딩의 이해

웹 폰트는 용량이 크기 때문에 브라우저가 HTML과 CSS를 파싱하는 속도보다 늦게 다운로드되는 경우가 많습니다. 이때 브라우저는 두 가지 방식 중 하나를 선택합니다.

  1. FOIT (Flash of Invisible Text): 폰트가 다운로드될 때까지 텍스트를 아예 보여주지 않음. (사용자는 빈 화면을 보게 됨)
  2. FOUT (Flash of Unstyled Text): 시스템 기본 폰트로 먼저 보여준 뒤, 웹 폰트가 로드되면 교체함. (이 과정에서 레이아웃이 어긋남)

구글은 사용자 경험을 위해 텍스트가 아예 안 보이는 것보다 레이아웃이 조금 흔들리더라도 빨리 보이는 것을 권장하지만, 이 '흔들림'을 최소화하는 것이 프론트엔드 개발자의 실력입니다.


2. font-display 속성으로 로딩 제어하기

가장 쉽고 필수적인 해결책은 CSS의 @font-face에서 font-display 속성을 사용하는 것입니다.

CSS
 
@font-face {
  font-family: 'Pretendard';
  src: url('/fonts/Pretendard.woff2') format('woff2');
  font-display: swap; /* 핵심 설정 */
}

2.1 주요 옵션 차이점

  • block: 폰트가 로드될 때까지 텍스트를 숨깁니다(최대 3초).
  • swap (권장): 시스템 폰트로 즉시 텍스트를 보여주고, 웹 폰트 로드가 완료되면 교체합니다. LCP 점수 유지에 유리합니다.
  • fallback: 아주 짧은 시간(약 0.1초) 동안 텍스트를 숨긴 후 로드되지 않으면 기본 폰트를 보여줍니다.
  • optional: 네트워크 상태에 따라 웹 폰트를 쓸지 말지 브라우저가 결정합니다. 성능이 최우선일 때 사용합니다.

3. 가상 폰트(Fallback Font) 최적화: 사이즈 매칭

font-display: swap을 사용하더라도 기본 폰트와 웹 폰트의 글자 크기, 자간, 높이가 다르면 여전히 레이아웃 시프트가 발생합니다. 이를 해결하기 위해 최근에는 가상 폰트(Size Adjust) 기법을 사용합니다.

3.1 size-adjust와 ascent-override

웹 폰트와 최대한 비슷한 부피를 가지도록 시스템 폰트의 수치를 강제로 조정하는 방식입니다.

CSS
 
/* 기본 폰트(Arial)를 웹 폰트와 크기가 비슷하게 조정 */
@font-face {
  font-family: 'Pretendard-Fallback';
  src: local('Arial');
  size-adjust: 95%;        /* 전체 크기 조정 */
  ascent-override: 90%;    /* 글자 윗부분 높이 조정 */
  descent-override: 10%;   /* 글자 아랫부분 높이 조정 */
}

body {
  font-family: 'Pretendard', 'Pretendard-Fallback', sans-serif;
}

이렇게 하면 폰트가 교체될 때 텍스트의 부피 변화가 거의 없어 CLS 점수를 0에 가깝게 유지할 수 있습니다. 최근에는 Capsize 같은 도구를 통해 이 수치를 정교하게 계산할 수 있습니다.


4. Next.js를 사용한다면? next/font 활용

Next.js (v13+) 환경에서 개발 중이라면 위 복잡한 과정이 대폭 자동화됩니다. next/font는 빌드 시점에 폰트를 최적화하고 가상 폰트를 자동으로 생성해 줍니다.

JavaScript
 
// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true, // 가상 폰트 자동 생성 옵션
});

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

next/font를 사용하면 폰트 파일을 구글 서버가 아닌 우리 서버(Self-hosting)에서 직접 제공하므로 개인정보 보호와 성능 면에서 모두 유리합니다.


5. 실무 체크리스트: 웹 폰트 100% 활용법

  1. 포맷 확인: 가장 압축률이 좋은 woff2 포맷을 우선적으로 사용하세요.
  2. Subset 활용: 한글 폰트는 용량이 매우 큽니다. 자주 쓰는 2,350자만 추린 '서브셋(Subset)' 폰트를 사용해 용량을 80% 이상 줄이세요.
  3. Preload: 중요한 폰트는 <link rel="preload">를 통해 미리 로드하세요.
  4. HTML
     
    <link rel="preload" href="/fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
    
  5. 로컬 폰트 활용: 사용자의 기기에 이미 설치된 폰트가 있다면 그것을 우선 사용하게 설정하세요. (src: local('Pretendard'), url(...))

웹사이트의 첫인상을 결정짓는 가장 중요한 요소 중 하나는 '로딩 속도'입니다. 구글은 이를 정량적으로 측정하기 위해 Core Web Vitals라는 지표를 도입했으며, 그중에서도 가장 큰 비중을 차지하는 것이 바로 LCP(Largest Contentful Paint)입니다.

LCP는 페이지에서 가장 큰 콘텐츠(보통 히어로 이미지나 큰 텍스트 블록)가 화면에 렌더링 되는 시간을 의미합니다. 놀랍게도 대부분의 웹사이트에서 LCP 점수를 갉아먹는 주범은 바로 '최적화되지 않은 이미지'입니다. 오늘은 Next.js가 제공하는 강력한 도구인 next/image를 활용하여 LCP 점수를 획기적으로 개선하는 전략을 심층 분석해 보겠습니다.


1. 왜 일반 <img> 태그보다 next/image인가?

전통적인 <img> 태그는 단순히 이미지를 불러올 뿐, 성능 최적화에 대한 어떠한 책임도 지지 않습니다. 반면 Next.js의 Image 컴포넌트는 다음과 같은 작업을 자동으로 수행합니다.

  1. 이미지 포맷 최적화: 브라우저가 지원한다면 WebP나 AVIF 같이 용량이 훨씬 작은 최신 포맷으로 자동 변환합니다.
  2. 리사이징 (Resizing): 디바이스 크기에 맞춰 최적화된 크기의 이미지를 생성하여 전송합니다. 4K 이미지를 모바일 사용자에게 전송하는 낭비를 막아줍니다.
  3. Lazy Loading: 화면에 보이지 않는 이미지는 로딩을 뒤로 미뤄 초기 로딩 속도를 높입니다.
  4. Placeholder 제공: 이미지가 로드되기 전 저해상도 이미지나 블러(Blur) 효과를 보여주어 사용자 경험을 개선합니다.

2. LCP 개선의 핵심: priority 속성

LCP 점수를 높이는 가장 쉽고 강력한 방법은 '가장 중요한 이미지'를 먼저 로드하는 것입니다.

보통 페이지 상단의 히어로 이미지나 배너가 LCP 요소가 됩니다. next/image는 기본적으로 레이지 로딩(Lazy Loading)이 적용되는데, LCP 요소에 레이지 로딩이 걸려 있으면 오히려 렌더링이 늦어져 LCP 점수가 나빠집니다.

JavaScript
 
// LCP 대상이 되는 히어로 이미지 예시
import Image from 'next/image';

export default function Hero() {
  return (
    <section>
      <Image
        src="/hero-banner.png"
        alt="메인 배너 이미지"
        width={1200}
        height={600}
        priority // 이 속성이 LCP 개선의 핵심입니다!
        className="object-cover"
      />
    </section>
  );
}

priority 속성을 추가하면 Next.js는 해당 이미지를 높은 우선순위(Fetch Priority)로 처리하고 프리로드(Preload) 태그를 삽입합니다. 이는 브라우저가 HTML을 파싱 할 때 해당 이미지를 가장 먼저 다운로드하도록 유도하여 LCP 시간을 단축시킵니다.


3. 레이아웃 시프트 방지와 sizes 속성 활용

이미지 로딩 중 화면이 덜컥거리는 현상인 CLS(Cumulative Layout Shift) 역시 성능 점수에 큰 영향을 줍니다. 이를 방지하려면 이미지의 비율을 미리 브라우저에 알려줘야 합니다.

3.1 정적 이미지와 고정 비율

width와 height를 명시하면 Next.js는 이미지 비율에 맞춰 공간을 미리 확보합니다. 만약 반응형 이미지를 구현해야 한다면 sizes 속성을 반드시 사용해야 합니다.

3.2 sizes 속성 최적화

sizes는 브라우저에게 "이 이미지는 뷰포트 크기에 따라 이 정도 너비를 차지할 거야"라고 미리 알려주는 힌트입니다.

JavaScript
 
<Image
  src="/product.jpg"
  alt="상품 이미지"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

위 설정은 모바일에서는 화면 꽉 차게(100vw), 태블릿에서는 절반(50vw), 데스크탑에서는 1/3(33vw) 크기의 이미지만 요청하도록 만듭니다. 불필요하게 큰 이미지를 다운로드하지 않게 되어 로딩 속도가 비약적으로 향상됩니다.


4. 이미지 포맷과 퀄리티 조정 (next.config.js)

더 극단적인 최적화를 원한다면 설정 파일을 통해 이미지 포맷을 제어할 수 있습니다. WebP보다 압축률이 높은 AVIF를 지원하도록 설정해 보세요.

JavaScript
 
// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

또한, 기본 퀄리티(75)를 프로젝트 성격에 맞춰 조정하여 용량과 화질 사이의 균형을 맞출 수 있습니다.


5. 실무 트러블슈팅: 외부 도메인 이미지 처리

많은 실무 프로젝트(예: CMS, 쇼핑몰 가비아 이미지 서버 등)에서 외부 URL의 이미지를 가져옵니다. 이때 next/image를 쓰려면 반드시 허용된 도메인 설정을 거쳐야 합니다.

JavaScript
 
// next.config.js
images: {
  remotePatterns: [
    {
      protocol: 'https',
      hostname: 'your-cdn-server.com',
      pathname: '/images/**',
    },
  ],
},

이 설정을 통해 외부 서버의 이미지도 Next.js 서버가 대신 최적화(Optimization API)하여 사용자에게 전달할 수 있게 됩니다.

+ Recent posts