← Blog
LighthouseNext.jsWeb PerformanceAccessibility

내 포트폴리오 사이트 Lighthouse 감사 — bfcache, 대비, 보안 헤더 고친 기록

·7분 읽기

증상 — Lighthouse 감사에서 나온 낮은 점수

모바일 환경(Slow 4G, Lighthouse 13.4)에서 내 포트폴리오 홈(aidevdesign.com)을 감사했더니 Performance 63, Accessibility 94, Best Practices 81, SEO 100이 나왔다. 세부적으로는 이미지·캐시 관련 절감 기회(약 1.4MB), back/forward cache 진입 실패(3가지 이유), 색상 대비 실패, main 랜드마크 누락, 보안 헤더(CSP·HSTS 등) 부재가 지적됐다.

숫자 하나만 보고 넘어가지 않고, 각 항목의 원인을 코드에서 직접 추적했다.

원인 1 — back/forward cache가 막혀 있던 이유

Lighthouse의 bf-cache 감사가 'MainResourceHasCacheControlNoStore'를 실패 이유로 지목했다. 즉 홈(`/`) 응답 자체에 Cache-Control: no-store가 붙어 있었다는 뜻이다.

원인은 내가 최근에 관리자 페이지에서 프로젝트를 추가·수정할 수 있도록 홈을 `export const dynamic = "force-dynamic"`으로 바꾼 것이었다. Next.js는 완전히 동적인 라우트에 기본적으로 no-store를 붙이는데, 이게 뒤로가기/앞으로가기 캐시를 막아버린 것이다.

해결은 `force-dynamic` 대신 `export const revalidate = 60`(ISR)으로 바꾸는 것이었다. 60초 단위로 캐시된 HTML을 서빙하면서도, 관리자 화면에서 실제로 프로젝트를 수정하면 `revalidatePath("/")`로 즉시 갱신되도록 이미 연결되어 있어서 체감 지연 없이 캐시 가능한 응답으로 되돌릴 수 있었다.

원인 2 — 색상 대비와 main 랜드마크

Lighthouse가 지목한 실패 요소를 열어보니 브랜드 강조색(#ff5a2c, 흰 배경 대비 3.1:1)과 45% 투명도 회색 텍스트(2.8:1) 두 가지가 WCAG AA 기준(4.5:1)에 못 미쳤다.

강조색은 #c44318로, 회색은 투명도 45%→65%로 조정해 각각 5:1, 5.2:1로 올렸다. 브랜드 톤은 유지하면서 실제로 읽을 수 있는 대비로 맞춘 것이다.

`<main>` 랜드마크가 없다는 지적도 있었다 — 히어로부터 방명록까지 전체 콘텐츠 영역이 `<header>`/`<footer>`만 있고 `<main>`으로 감싸여 있지 않았다. 감싸는 것으로 해결했다.

시도했다가 되돌린 것 — 코드 스플리팅

unused JavaScript(약 180KB) 절감을 위해 방명록 섹션을 next/dynamic으로 분리해봤다. 그런데 이 사이트는 스크롤 reveal 애니메이션을 IntersectionObserver로 구현하면서, 마운트 시점에 한 번만 `[data-reveal]` 요소를 전부 수집하는 구조였다.

동적으로 늦게 로드되는 방명록 섹션의 reveal 요소는 이 최초 수집 시점에 아직 DOM에 없어서 관찰 대상에서 빠졌고, 그 결과 hydration mismatch 경고가 발생했다. 번들 크기를 줄이려다 새 버그를 만드는 셈이라 이 변경은 되돌렸다.

unused JS의 상당 부분은 실제로는 GTM·GA 스크립트(약 30만 바이트)에서 나온 것이었는데, 이건 분석 도구를 유지하기로 한 이상 내가 손댈 수 있는 영역이 아니었다.

보안 헤더 추가

CSP, X-Frame-Options, Referrer-Policy, Cross-Origin-Opener-Policy, Permissions-Policy를 next.config.ts의 headers()에 추가했다. CSP는 이 사이트가 실제로 쓰는 도메인(GTM, GA, Cloudflare Turnstile, jsdelivr 폰트 CDN)만 허용하도록 구성했다.

적용 직후 로컬 개발 서버에서 콘솔 에러가 하나 떴다 — React 개발 모드가 디버깅용으로 eval()을 쓰는데 CSP의 script-src가 이를 막은 것이다. 프로덕션 빌드에서는 필요 없는 동작이라, 기존에 `_next/static` 캐시 규칙에도 쓰던 것과 같은 패턴으로 `NODE_ENV === "production"`일 때만 보안 헤더를 적용하도록 조건을 걸어 해결했다.

결과 — 같은 환경에서 직접 재측정

라이브 사이트는 네트워크 조건에 따라 점수가 크게 흔들려서, 수정 전/후 코드를 같은 로컬 프로덕션 빌드·같은 기기로 각각 측정해 비교했다.

Accessibility 94 → 100, back/forward cache 실패(3건) → 통과, 색상 대비 실패 → 통과, main 랜드마크 없음 → 통과. 전부 코드로 직접 확인한 결과다.

Performance 점수 자체는 이 로컬 비교에서 뚜렷한 개선으로 나타나지 않았다(오히려 TBT 변동으로 소폭 낮게 나온 회차도 있었다) — 측정 노이즈로 보이지만, 단정하지 않고 그대로 기록한다. 라이브 사이트에서 지적된 1.4MB 캐시 절감(히어로 영상)은 이미 next.config.ts에 캐시 규칙이 존재하는데도 실제 응답에는 반영되지 않고 있어서, Cloudflare Workers Assets 바인딩이 정적 파일 요청에 대해 next.config.ts의 headers()를 우회하는 구조 때문일 가능성이 높다고 보고 있다 — 배포 후 별도로 검증이 필요한 부분으로 남겨둔다.

Best Practices 점수(라이브 81점)를 낮추는 'Uses deprecated APIs' 경고는 확인해보니 Cloudflare의 자체 봇 관리 스크립트(cdn-cgi/challenge-platform)에서 나오는 것이었다 — 내 애플리케이션 코드가 아니라 Cloudflare가 주입하는 스크립트라 애플리케이션 레벨에서 고칠 수 없는 항목이다.