김솔비 블로그
기술 블로그

[Part 6] Lighthouse 모바일 63점을 98점으로 올렸다

5분 읽기시리즈 7/10

TL;DR

  • Lighthouse 모바일 점수를 홈 63 → 98, 모든 프롬프트 63 → 99, 공지사항 67 → 100으로 올렸다.
  • 원인 대부분은 UX를 위해 넣은 것이었다. 공통 로딩 화면, 투명에서 시작하는 애니메이션, 스크롤 등장 효과.
  • 로딩 화면은 성능만 해친 게 아니었다. 없는 페이지가 404가 아니라 200으로 나갔다.

결과부터

페이지전후
홈6398
모든 프롬프트6399
TOP 106494
상세9598
공지사항67100

Lighthouse 모바일 기준이다. 아래는 점수를 끌어내린 다섯 가지와 고친 방법이다.

① 공통 로딩 화면이 첫 화면을 늦췄다

문제: 모든 페이지에 공통 loading.tsx를 두었다. 데이터를 기다리는 동안 뼈대 화면을 먼저 보여 주려는 의도였다.

원인: loading.tsx가 있으면 Next.js는 로딩 화면을 먼저 보내고 본문을 스트리밍으로 나중에 보낸다. 본문이 늦게 오니, 첫 화면의 주요 내용이 그려지는 시점(LCP)이 5초 가까이 늦어졌다.

해결: 공통 로딩 화면을 없애고, 실제로 기다림이 있는 목록 화면에만 뒀다.

② 투명에서 시작하는 애니메이션

문제: 첫 화면의 큰 제목이 opacity: 0에서 시작해 떠오르는 애니메이션이었다.

원인: 브라우저는 완전히 투명한 요소를 LCP로 세지 않는다. 그래서 LCP가 애니메이션이 끝나는 시점에 잡혔다.

해결: 투명에서 나타나는 대신 보이는 채로 살짝 올라오게 바꿨다. 움직임의 느낌은 남기고, 처음부터 보이게 했다.

③ 스크롤 등장 효과가 첫 줄까지 기다렸다

문제: 카드가 스크롤에 맞춰 나타나는 효과가, 화면에 처음부터 보이는 첫 줄 카드까지 JS가 실행되길 기다렸다.

해결: 첫 줄은 즉시 보이게 했다. 등장 효과는 스크롤해서 새로 보이는 카드에만 쓴다.

④ 쿼리를 읽는 페이지는 매번 서버에서 그렸다

문제: 목록과 공지사항 페이지는 ?kind=, ?sort= 같은 쿼리를 읽는다. 쿼리를 읽는 페이지는 미리 만들어 둘 수 없어서 요청마다 서버에서 그렸다.

선택지:

  1. 필터를 전부 브라우저에서 처리한다. 첫 화면에 JS가 더 필요하다.
  2. 필터 없는 주소만 미리 만든 정적 사본을 준다.

대부분의 방문은 필터 없는 주소로 들어온다. 그래서 2번을 골랐다. Next.js 16의 proxy(예전 middleware)에서 쿼리가 없으면 미리 만든 사본으로 rewrite한다. 주소창의 주소는 그대로다.

ts
// src/proxy.ts export async function proxy(request: NextRequest) { const { pathname, search } = request.nextUrl; if (!pathname.startsWith("/admin")) { if (search) return NextResponse.next(); // 정렬·검색이 있으면 평소대로 const cached = pathname === "/prompts" ? "/browse-cache/prompts" : /^\/c\/[^/]+$/.test(pathname) ? `/browse-cache${pathname}` : null; return cached ? NextResponse.rewrite(new URL(cached, request.url)) : NextResponse.next(); } // ... 관리자 경로는 로그인 확인 }

공지사항은 필터를 쿼리 대신 경로로 바꿨다(/notice/kind/update). 경로는 미리 만들 수 있다.

⑤ TOP 10 1위 이미지가 lazy였다

문제: TOP 10 페이지에서 가장 크게 보이는 1위 이미지가 loading="lazy"였다. 첫 화면에 보이는데도 늦게 불러왔다.

해결: 1위 이미지에만 priority를 줬다.

덤: 로딩 화면이 404를 200으로 바꿨다

성능을 보다가 더 큰 문제를 찾았다. 없는 페이지가 404가 아니라 200으로 응답하고 있었다(화면은 404 내용에 noindex).

원인: loading.tsx로 스트리밍을 시작하면 응답 상태 코드가 먼저 200으로 확정된다. 그 뒤에 본문을 그리다가 "없는 글"임을 알아도 이미 보낸 상태 코드는 바꿀 수 없다.

해결: 404가 날 수 있는 상세 화면에는 로딩 화면을 두지 않는다.

구글은 이런 페이지를 Soft 404로 처리한다. 사람 눈에는 404 화면이지만 검색 엔진에는 "정상 페이지인데 내용이 없음"으로 보인다. 이 버그는 화면 테스트(8편)가 잡았다.

배운 점

  • 다섯 가지 원인 중 넷이 UX를 위해 넣은 것이었다. 로딩 화면, 등장 애니메이션, 스크롤 효과. 넣을 때 측정부터 했어야 했다.
  • 로딩 화면은 SEO를 깰 수 있다. 스트리밍은 상태 코드를 먼저 확정한다.
  • 필터 있는 페이지를 전부 빠르게 만들 필요는 없다. 대부분이 들어오는 주소 하나를 빠르게 하는 게 효과가 컸다.

프롬피드 개발기

프롬피드: promfeed.solfany.com