JK.dev

블로그 성능 최적화 가이드: 성능 점수 100점 달성기

블로그 성능 최적화 가이드: 성능 점수 100점 달성기

이 블로그는 정적 사이트 빌더인 Astro 프레임워크 기반 Minimalistic Astro Blog 테마를 바탕으로 커스터마이즈하여 운영하고 있습니다. 기본 테마만으로도 가볍고 단순한 구조를 제공하지만, 실제 운영 과정에서 이미지·CSS·스크립트 등 여러 요소가 Web Vital 퍼포먼스 점수에 영향을 주는 문제가 드러났습니다.

이번 글에서는 이러한 문제들을 어떻게 확인했고, 어떤 방식으로 최적화했으며, 적용 후 어떤 결과가 있었는지를 항목별로 정리해 소개합니다.

0. Lighthouse 품질 측정 도구

Lighthouse는 구글에서 제공하는 웹페이지 품질 측정 오픈소스 도구입니다.

Lighthouse는 다음 4가지 영역을 측정합니다.

  • Performance
  • Accessibility
  • Best Practices
  • SEO

이번에는 아티클에선 Performance 영역을 중심으로 개선 작업을 진행했습니다.

0.1 Performance 성능 지표

사용자가 접속했을 때 얼마나 빨리 볼 수 있고, 매끄럽게 상호작용하며, 레이아웃이 안정적으로 유지되는지 포괄하는 지표입니다. Lighthouse V.10 기준 5가지 항목을 측정합니다.

  • FCP(First Contentful Paint): 첫 번째 텍스트나 이미지가 그려지는 시점
  • LCP(Largest Contentful Paint): 가장 큰 콘텐츠 요소가 화면에 표시되는 시점
  • CLS(Cumulative Layout Shift): 페이지 로드 중 예기치 않은 레이아웃 이동의 정도
  • SI(Speed Index) : 콘텐츠가 시각적으로 표시되는 전체 속도의 평균
  • TBT(Total Blocking Time): 메인 스레드가 바쁘게 막혀서 입력을 처리하지 못한 시간
Lighthouse 버전별 성능 지표 변화

V.8에선 추가로 아래 지표를 확인했습니다.

  • TTI(Time to Interactive): 상호작용 가능하기까지 걸린 시간 하지만 TTI는 네트워크 환경과 긴 작업에 지나치게 민감하고 다른 대안 지표들이 있어 버전 10부턴Performance 점수 반영 기준에선 사라졌습니다. (*TTI의 가중치 10%는 CLS로 이전되었습니다.)

0.2 PageSpeed Insights

크롬 개발자 도구(Performance 탭), Lighthouse CLI, Web Page Test 등 다양한 방식으로 실행할 수 있습니다. 이중 PageSpeed Insights는 실험실 데이터와 실제 사용자 데이터를 함께 제공합니다.

실험실 데이터

  • 통제된 환경(특정 기기·네트워크)에서 시뮬레이션해 측정합니다.
  • 재현성이 높아 성능 디버깅과 개선 효과 검증에 적합하지만, 실제 사용자 환경과는 차이가 있을 수 있습니다.
PageSpeed Insights 실험실 데이터

실제 사용자 데이터

  • Chrome 사용자 환경 보고서(CrUX) 데이터 세트를 기반으로 합니다.
  • 실제 이용 상황이 반영된다는 장점이 있으나, 충분한 트래픽이 필요하고 문제의 구체적 원인 파악에는 한계가 있습니다.

0.3 분석 개요

측정 기준은 PageSpeed Insights에서 제공하는 실험실 데이터(Lab) 를 기반으로 했으며, 2025년 9월 8일 커밋 시점을 기준으로 삼았습니다.

(*실제 사용자 환경 데이터는 충분한 지표가 수집되지 않아 실험실 데이터 기반으로만 측정되었습니다.)

PageSpeed Insights Performance 측정 결과

측정 결과 Performance 점수는 75점으로, 전반적으로 보통 수준에 해당합니다.

세부 지표를 보면 LCP(4.5s), FCP(3.3s), Speed Index(5.0s) 가 다소 느리게 나타나 초기 렌더링 지연이 주요 원인으로 파악되었습니다. 반면 TBT(0ms) 와 CLS(0ms)는 매우 양호합니다. 이는 정적 사이트 특성상 자바스크립트 실행 부담이 적고, 레이아웃 구조가 단순하여 시각적 안정성이 확보된 결과로 해석할 수 있습니다.

PageSpeed Insights 분석 결과

Insights 패널에서는 특히 렌더링 차단 리소스(최대 3.3초 절감 가능), 이미지 전송 최적화(264KiB 절감 가능), LCP 리소스 요청 지연 등이 핵심 개선 항목으로 제시되었습니다.

0.4 분석 결과

이처럼 측정 결과는 초기 렌더링 지연(LCP, FCP, Speed Index)이 핵심 과제로 드러났습니다. 따라서 이후 과정에서는 렌더링 차단 리소스, 이미지 전달 방식, 폰트 및 CSS 최적화, 불필요한 패키지 제거와 같은 구체적인 개선 항목들을 단계적으로 살펴봅니다.

개선1: 불필요한 패키지 정리

앞서 말했듯, 이 블로그는 Minimalistic Astro Blog 테마를 활용해 구축되었습니다. 기존 테마에서 제공하는 기능과 스타일을 어느정도 유지하고 있지만, 일부 사용하지 않는 패키지가 프로젝트에 포함되어 있습니다.

패키지는 설치만 되어 있어도 빌드 속도, 번들 크기에 영향을 줍니다. 특히 정적 사이트처럼 가벼운 구조를 가진 프로젝트에서는, 사용하지 않는 패키지가 쌓일수록 성능상의 장점을 희석시키는 결과를 낳을 수 있습니다.

1.1 개선 방법

depcheck는 쓰이지 않는 패키지를 찾아내는 도구입니다.

아래의 간단한 명령어를 통해 확인할 수 있습니다.(pnpm dlx는 일회성으로 패키지를 실행하는 방법입니다.)

pnpm dlx depcheck
depcheck CLI 실행 결과

depcheck는 정적 분석을 기반으로 하기 때문에 몇 가지 한계가 있습니다.

  • 동적 require() 구문
  • 조건부 import
  • 빌드 시 자동으로 삽입되는 모듈 (예: Babel plugin)

이러한 경우는 정확히 감지하지 못할 수 있으므로, 결과를 확인한 후 수동 검토를 거쳐 삭제 여부를 결정해야 합니다.

1.2 적용 결과

프로젝트에서 제거한 패키지

  • react 생태계 라이브러리 정적 블로그 특성상 앞으로도 React 라이브러리를 사용할 계획이 없으므로 제거했습니다.
  • 그 외 불필요한 패키지
    • framer-motion
    • sanitize-html
    • @astrojs/db

이들은 코드 상에서 활용되지 않고 있어 함께 정리했습니다.

제거한 패키지 목록

개선2: 정적 리소스 압축

정적 사이트는 HTML, CSS, JS, 이미지와 같은 리소스를 직접 내려주기 때문에, 파일 크기 자체가 곧 로딩 속도에 직결됩니다. 특히 모바일 환경이나 네트워크가 불안정한 상황에서 더욱 체감 속도에 영향을 줄 수 있습니다.

2.1 개선 방법

Astro 자체 빌드 파이프라인과 배포 플랫폼 Vercel 덕분에 HTML, CSS, JS 같은 텍스트 리소스는 기본으로 압축이 적용됩니다.

하지만 public/ 폴더에 있는 정적 자산(특히 이미지 파일)은 원본 그대로 배포하고 있습니다.

SEO 이미지 네트워크 요청
public/ 폴더는 왜 필요한가요?

public/ 폴더는 빌드 시 경로나 파일명이 변하지 않고 그대로 배포되기 때문에 브라우저·크롤러가 고정 경로를 기대하는 리소스(예: favicon.ico, site.webmanifest, robots.txt, /.well-known/*, OG/Twitter/RSS 대표 이미지)를 안정적으로 제공하는 데 필수입니다.

정적 자산이 늘어날수록 수동 압축 대신 빌드 단계에서 자동 최적화하는 것이 효율적이기 때문에, 이를 위해 astro-compress 플러그인을 사용하면, 빌드 시점에 이미지까지 함께 압축할 수 있습니다.

astro-compress 플러그인

Astro에서는 프로젝트에 필요한 기능을 빌드/실행 단계에 쉽게 끼워 넣는 확장 플러그인을 제공합니다.

그 중, astro-compress 플러그인은 빌드 단계에 실행되어 정적 리소스(HTML, CSS, JS, SVG, IMG 등)를 자동으로 압축합니다.

astro.config.mjsintegrations 속성에 추가하여 간단하게 적용이 가능합니다.

astro-compress 플러그인 설정 예시

2.2 적용 결과

빌드 단계에서 기록된 로그입니다.

빌드 로그 1

텍스트 리소스는 이미 압축이 적용되기 때문에 추가적인 효과는 미미했습니다.

빌드 로그 2

반면, 이미지 리소스는 최대 61%까지 용량 절감이 확인되었습니다.

개선3: CSS 최적화

정적 리소스를 압축해 용량을 줄이는 것만으로도 성능에 도움이 되지만, CSS는 단순한 정적 자산 이상의 특성을 가지고 있습니다.

브라우저는 CSS 스타일 계산이 끝날 때까지 화면 그리기를 멈추기 때문에, CSS 파일 하나가 곧바로 렌더링 차단 요소가 됩니다. 그 결과 초기 화면 표시 속도, 특히 FCP(First Contentful Paint) 가 직접적으로 지연될 수 있습니다.

아래는 DevTool Performance 측정 결과입니다.

css 파일이 렌더링을 차단하여 FCP가 약 2000ms로 늦게 나타납니다.

CSS 렌더링 차단 예시

3.1 개선 방법

렌더링 차단을 줄이기 위해서는 최초 화면 렌더링에 꼭 필요한 최소한의 스타일만 먼저 전달해야 합니다.

이를 크리티컬 CSS라고 하며, HTML 문서의 <style> 태그에 직접 인라인합니다. 나머지 스타일은 별도 CSS 파일로 분리해 서버에서 다운로드하여 로드하게 됩니다.

3.1.1 @playform/inline 플러그인 적용

프로젝트에는 많은 CSS 코드가 있고, 구조를 일일이 분석하기엔 시간이 많이 소요됩니다. 따라서 직접 css를 분리, 추출하기보단 @playform/inline 플러그인을 도입했습니다.

@playform/inline 플러그인은 빌드 시점에 자동으로 크리티컬 CSS를 추출하여 HTML에 인라인으로 삽입하고 나머지는 외부 CSS 파일로 분리하여 지연 로드하게 됩니다.

설정은 간단하게 Astro 설정 파일(astro.config.mjs)의 integrations 항목에 @playform/inline 플러그인을 추가하여 빌드 단계에서 최적화가 적용됩니다.

@playform/inline 플러그인 설정 예시

3.2 적용 결과

빌드 후 각 CSS 파일의 약 27~30% 가 HTML에 인라인으로 포함되었습니다.

빌드 결과 크리티컬 CSS 포함 비율 HTML에 인라인된 크리티컬 CSS 예시

나머지 스타일은 외부 CSS 파일로 분리되어 지연 로드되며, 브라우저는 초기 화면 렌더링에 필요한 최소한의 스타일만 먼저 처리하게 되었습니다.

그 결과 렌더링 차단 요소가 제거되었고, DevTools Performance 측정 기준 FCP(First Contentful Paint)가 약 2000ms → 1000ms로 절반 수준 개선되었습니다.

CSS 렌더링 차단 제거 예시

개선4: 폰트 최적화

폰트를 별도 최적화 없이 사용하면 렌더링 차단으로 인한 초기 로딩 지연, 불필요한 대용량 전송, 그리고 FOIT/FOUT 같은 텍스트 표시 문제가 발생할 수 있습니다.

4.1 개선 방법

폰트 최적화는 어디서 가져오는지 그리고 어떻게 제공하느냐에 따라서도 성능 차이가 큽니다.

4.2 폰트 호스팅 방식

프로젝트에서는 Pretendard 폰트를 외부 CDN을 통해 불러왔습니다.

하지만 외부 도메인 리소스를 사용할 경우 DNS Lookup과 TLS Handshake 같은 네트워크 오버헤드가 발생하여 페이지 초기 로딩 성능에 불리할 수 있습니다.

특히 폰트는 보통 CSS를 통해 로드되기 때문에 브라우저는 다음 단계를 거칩니다.

  1. 폰트 CSS 다운로드
  2. CSS 내부의 @font-face 규칙 해석
  3. 실제 폰트 파일 다운로드
폰트 렌더링 차단 예시

이 과정에서 CSS는 렌더링 차단 리소스로 동작하므로, 외부 CDN 의존 시 네트워크 지연이 곧바로 초기 페인트(FCP/LCP) 지연으로 이어지게 됩니다. 이를 개선하기 위해 프로젝트에서는 폰트 파일과 CSS를 직접 호스팅하는 방식으로 전환하여, 외부 네트워크 의존성을 줄이고 초기 로딩 속도를 개선할 수 있습니다.

4.3 폰트 제공 방식

폰트 제공 방식은 대표적으로 3가지로 구분됩니다.

방식포함 글리프굵기(weight) 처리
기본 폰트모든 글자weight별 여러 파일
다이나믹 서브셋실제 사용 글자weight별 여러 파일
가변 다이나믹 서브셋실제 사용 글자가변 축으로 여러 weight를 한 face가 커버

기본 폰트

불필요한 글리프를 포함한 큰 폰트 파일을 weight마다 내려받습니다.

기본 폰트 제공 방식

그로 인해:

  • 초기 화면에 사용되지 않는 문자까지 다운로드되어 전송량 증가 및 첫 페인트 지연(FCP/LCP) 위험이 커집니다.
  • 각 weight마다 파일 용량이 크기 때문에, 렌더링 차단 효과가 두드러집니다.

다이나믹 서브셋 + 가변 다이나믹 서브셋

이 방식은 실제로 사용되는 글리프만 개별 파일로 분리해 제공하는 방법입니다.

다음은 가변 다이나믹 서브셋 css 파일 일부 예시입니다:

/* pretendardvariable-dynamic-subset.css */
/* unicode range별 font-face 규칙이 수십 개 나열된 css 파일 */

@font-face {
  font-family: 'Pretendard Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 45 920;
  src: url(./woff2-dynamic-subset/PretendardVariable.subset.0.woff2) format('woff2-variations');
  unicode-range:
    U+f9ca-fa0b, U+ff03-ff05, U+ff07, U+ff0a-ff0b, U+ff0d-ff19, U+ff1b, U+ff1d, U+ff20-ff5b, U+ff5d,
    U+ffe0-ffe3, U+ffe5-ffe6;
}

@font-face {
  ...
}

@font-face {
  ...
}

여기서 unicode-range에 해당하는 문자가 페이지에 존재할 경우, 브라우저가 필요한 폰트 파일만 자동으로 요청합니다.

즉, 전체 폰트 파일을 내려받는 대신 실제 사용되는 일부 글리프만 요청하기 때문에 전송량이 줄어들고, 초기 로딩 속도와 시각적 안정성이 개선됩니다.

4.4 적용 결과

font 개선 후 URL

프로젝트에서는 외부 CDN 대신 직접 호스팅 방식을 적용하여 폰트 파일을 제공합니다.

위 화면에서 확인할 수 있듯이, vercel.app 도메인에서 woff2 폰트 파일들이 내려오고 있습니다.

폰트 TCP 연결 시간

폰트가 서비스 도메인과 동일한 출처에서 제공되기 때문에 별도의 DNS Lookup이나 TCP Handshake 같은 초기 네트워크 비용은 발생하지 않습니다. 덕분에 요청–응답 구간만 거쳐 빠르게 파일이 내려와, 외부 CDN 대비 초기 로딩 지연을 줄일 수 있습니다.

폰트 최적화 후 네트워크 요청

폰트는 가변 다이나믹 서브셋 방식으로 제공되어, 하나의 대용량 파일이 아니라 여러 개의 작은 파일(20~40KB 수준)로 분리되어 내려옵니다.

각 파일은 보통 20 ~ 40KB 크기로 나뉘어 있으며, 다운로드 시간은 평균 60 ~ 90ms로 짧게 유지되어 전체적으로 빠른 응답성을 보입니다.

개선5: 메인 이미지 최적화

LCP는 사용자가 페이지에서 가장 큰 핵심 콘텐츠가 보이기까지 걸리는 시간을 측정하는 Performance 지표의 핵심 요소입니다. 이 수치가 높으면 사용자는 화면이 느리게 로딩된다고 체감하며, 이탈률 증가와 전환율 하락으로 이어질 수 있습니다.

사이트 lcp 이미지

블로그 메인 페이지의 경우, 상단 포스트 대표 이미지가 LCP 요소로 작동합니다.

LCP 이미지 최적화 전

그런데 현재 이 이미지는 실제 표시 크기보다 훨씬 큰 해상도로 제공되고 있으며, 포맷 또한 jpeg라 파일 용량이 불필요하게 커 성능 저하를 유발합니다.

5.1 개선 방법

이미지 최적화는 크게 세 가지 단계로 나눌 수 있습니다.

  1. 포맷 최적화
    • webp, AVIF 등 차세대 포맷 사용
  2. 반응형 최적화
    • srcset, sizes 속성으로 뷰포트에 맞는 이미지 제공
  3. 로딩 최적화
    • loading 속성으로 로드 시점 제어 (lazy / eager)
    • fetchpriority 속성으로 다운로드 우선순위 힌트 제공
    • <link rel="preload" />로 LCP 이미지를 미리 다운로드
  • 이미지 속성 설명 sizes: 뷰포트별 이미지 크기 srcset: 뷰포트별 이미지 파일 후보 목록 loading: 이미지를 언제 로딩할지 결정 fetchpriority : 브라우저에게 이미지 다운로드 우선순위의 힌트를 전달

5.2 Astro와 이미지 최적화

Astro는 <Image> 컴포넌트를 통해 빌드 과정에서 이미지를 자동 최적화하고, 해시가 포함된 URL을 생성하여 효율적인 캐싱과 관리가 가능합니다.

Astro 이미지 해시 예시

다만, 프리로드 적용 시에는 주의가 필요합니다. 프리로드 대상은 반드시 실제 <img> 태그가 요청하는 최종 URL과 정확히 일치해야 의미가 있으며, 그렇지 않을 경우 브라우저가 동일 이미지를 두 번 다운로드하는 중복 요청이 발생해 오히려 성능에 악영향을 줄 수 있습니다.

// src가 일치해야 한다.
<link href="/_astro/hero.BNsLR17y_1kK0ax.webp" rel="preload" />
<img src="/_astro/hero.BNsLR17y_1kK0ax.webp" />

이 문제를 해결하기 위해 <link> 태그는 imagesrcset·imagesizes 속성을 지원합니다.

이는 <img>srcset·sizes와 대응하며, 브라우저가 같은 규칙으로 이미지를 선택할 수 있게 합니다.

<link
  rel="preload"
  as="image"
  imagesrcset="/_astro/hero.480w.BNsLR17y.webp 480w,
               /_astro/hero.800w.BNsLR17y.webp 800w,
               /_astro/hero.1200w.BNsLR17y.webp 1200w"
  imagesizes="(max-width: 768px) 100vw, 768px"
/>

따라서 <img><link rel="preload">가 동일한 이미지 후보 집합을 공유해야만 브라우저가 중복 다운로드 없이 올바른 리소스를 선행 요청할 수 있습니다.

5.3 프리로드와 <img>의 후보 집합 맞추기

이 문제는 Astro의 getImage() API를 활용하여 해결할 수 있습니다. getImage()는 최적화된 이미지 파일과 함께 srcset, sizes 등의 속성을 자동으로 생성해 주기 때문에, 이를 기반으로 <img><link rel="preload">가 동일한 후보 집합을 공유하도록 유틸리티 함수를 구성했습니다.

---
import { getImage } from 'astro:assets';
import hero from "/assets/hero.png";

// getImage 실행 → 최적화된 이미지 데이터 생성
const img = await getImage({
  src: hero,
  widths: [480, 800, 1200],
  formats: ['webp'],
  sizes: "(max-width: 768px) 100vw, 768px"
});
---

<html>
  <head>
    <link
      rel="preload"
      as="image"
      href={img.src}
      imagesrcset={img.srcSet.attribute}
      imagesizes={img.attributes.sizes}
    />
  </head>
  <body>
    <img
      src={img.src}
      srcset={img.srcSet.attribute}
      sizes={img.attributes.sizes}
    />
  </body>
</html>

이 방식으로 프리로드와 실제 <img> 요청 간 불일치를 방지할 수 있으며, 불필요한 중복 다운로드 없이 성능 최적화를 안정적으로 적용할 수 있습니다.

5.4 적용 결과

최적화 전

  • LCP: 4.97s
  • 이미지 크기: 134kB
LCP 이미지 최적화 전 SEO LCP 이미지 최적화 전

최적화 후

  • LCP: 1.26s
  • 이미지 크기: 데스크탑 기준 12.7kB, 모바일 기준 7.6kB
LCP 이미지 최적화 후 SEO LCP 이미지 최적화 후 - 데스크탑 SEO LCP 이미지 최적화 후 - 모바일

최적화 과정에서는 다음과 같은 개선이 적용되었습니다.

  • 이미지 포맷 변환: jpegWebP로 교체하여 용량을 크게 줄임
  • 적응형 크기 제공: srcset·sizes를 활용해 디바이스 해상도별로 적절한 크기의 이미지를 전달
  • 프리로드(preload) 적용: <link rel="preload">를 통해 핵심 LCP 이미지를 우선적으로 요청

이러한 최적화를 통해 LCP 요소 렌더링 속도가 크게 개선되며 사용자 체감 성능도 눈에 띄게 향상되었습니다.

정리하며

최적화 후 PageSpeed Insights Performance 점수

최종적으로 Performance 75 → 100, LCP 4.5s → 1.0s까지 끌어올렸습니다. 정적 사이트 빌더 Astro 기반 블로그에서도, 초기 렌더 경로를 정교하게 다듬으면 체감 성능을 크게 개선할 수 있음을 확인했습니다.

주요 개선 요약

  • 불필요한 패키지 제거 → 빌드 속도·번들 크기 개선
  • astro-compress로 정적 자산 자동 최적화 → 네트워크 전송량 감소
  • @playform/inline으로 크리티컬 CSS 인라인 → FCP 2s → 1s
  • 폰트는 동일 출처 + 가변 다이내믹 서브셋 → 초기 지연·FOIT/FOUT 완화
  • Astro getImage() + <link rel="preload"> → 중복 없이 LCP 선행 로드
공유:
Last updated on