[Part 6] Lighthouse 모바일 63점을 98점으로 올렸다
TL;DR
- Lighthouse 모바일 점수를 홈 63 → 98, 모든 프롬프트 63 → 99, 공지사항 67 → 100으로 올렸다.
- 원인 대부분은 UX를 위해 넣은 것이었다. 공통 로딩 화면, 투명에서 시작하는 애니메이션, 스크롤 등장 효과.
- 로딩 화면은 성능만 해친 게 아니었다. 없는 페이지가 404가 아니라 200으로 나갔다.
결과부터
| 페이지 | 전 | 후 |
|---|---|---|
| 홈 | 63 | 98 |
| 모든 프롬프트 | 63 | 99 |
| TOP 10 | 64 | 94 |
| 상세 | 95 | 98 |
| 공지사항 | 67 | 100 |
Lighthouse 모바일 기준이다. 아래는 점수를 끌어내린 다섯 가지와 고친 방법이다.
① 공통 로딩 화면이 첫 화면을 늦췄다
문제: 모든 페이지에 공통 loading.tsx를 두었다. 데이터를 기다리는 동안 뼈대 화면을 먼저 보여 주려는 의도였다.
원인: loading.tsx가 있으면 Next.js는 로딩 화면을 먼저 보내고 본문을 스트리밍으로 나중에 보낸다. 본문이 늦게 오니, 첫 화면의 주요 내용이 그려지는 시점(LCP)이 5초 가까이 늦어졌다.
해결: 공통 로딩 화면을 없애고, 실제로 기다림이 있는 목록 화면에만 뒀다.
② 투명에서 시작하는 애니메이션
문제: 첫 화면의 큰 제목이 opacity: 0에서 시작해 떠오르는 애니메이션이었다.
원인: 브라우저는 완전히 투명한 요소를 LCP로 세지 않는다. 그래서 LCP가 애니메이션이 끝나는 시점에 잡혔다.
해결: 투명에서 나타나는 대신 보이는 채로 살짝 올라오게 바꿨다. 움직임의 느낌은 남기고, 처음부터 보이게 했다.
③ 스크롤 등장 효과가 첫 줄까지 기다렸다
문제: 카드가 스크롤에 맞춰 나타나는 효과가, 화면에 처음부터 보이는 첫 줄 카드까지 JS가 실행되길 기다렸다.
해결: 첫 줄은 즉시 보이게 했다. 등장 효과는 스크롤해서 새로 보이는 카드에만 쓴다.
④ 쿼리를 읽는 페이지는 매번 서버에서 그렸다
문제: 목록과 공지사항 페이지는 ?kind=, ?sort= 같은 쿼리를 읽는다. 쿼리를 읽는 페이지는 미리 만들어 둘 수 없어서 요청마다 서버에서 그렸다.
선택지:
- 필터를 전부 브라우저에서 처리한다. 첫 화면에 JS가 더 필요하다.
- 필터 없는 주소만 미리 만든 정적 사본을 준다.
대부분의 방문은 필터 없는 주소로 들어온다. 그래서 2번을 골랐다. Next.js 16의 proxy(예전 middleware)에서 쿼리가 없으면 미리 만든 사본으로 rewrite한다. 주소창의 주소는 그대로다.
// 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를 깰 수 있다. 스트리밍은 상태 코드를 먼저 확정한다.
- 필터 있는 페이지를 전부 빠르게 만들 필요는 없다. 대부분이 들어오는 주소 하나를 빠르게 하는 게 효과가 컸다.
프롬피드 개발기
- 개요. SNS에서 유행하는 AI 프롬프트, 빈칸만 채워 바로 쓰게 만들었다
- 1편. 방문자가 늘어도 DB 조회는 그대로다
- 2편. AI 없이 유행 프롬프트 자동 수집기 만들었다
- 3편. 유튜브 API 무료 한도, 10,000번이 아니었다
- 4편. 복사해도 숫자가 안 올라가던 이유
- 5편. 이미지가 깨져 보였던 이유 3가지
- 6편. Lighthouse 모바일 63점을 98점으로 올렸다 (지금 읽는 글)
- 7편. 개인정보 없이 매주 알림 보내기 (웹 푸시)
- 8편. 테스트가 깨지면 배포가 안 되게 막았다
- 9편. 프롬피드 회고: 잘한 것과 아쉬운 것
프롬피드: promfeed.solfany.com