최애의 관리자 — LCP, ISR 비용, 보안까지 다시 본 최적화 기록
최애의 관리자
4월 중순 작업을 다시 보니, 커밋 메시지만 보면 전부 “성능 최적화”라고 뭉뚱그릴 수 있다. 그런데 실제로는 종류가 좀 달랐다.
어떤 건 사용자가 첫 화면을 더 빨리 보게 하려는 작업이었고, 어떤 건 Vercel 비용을 덜 쓰려는 작업이었다. 또 어떤 건 Sentry에 찍힌 에러를 하나씩 지우는 운영 작업이었다. 기능을 더 넣는 시기라기보다, 사이트가 커진 뒤에 생기는 피로감을 줄이는 시기였다.
클라이언트 컴포넌트를 줄인다는 말의 실제 의미
이때 서버/클라이언트 컴포넌트 분리를 다시 했다.
초기에는 “클라이언트 컴포넌트가 크니까 줄이자” 정도로 생각했는데, 막상 하다 보면 그렇게 단순하지 않다. 덱, 플래너, 거울 던전 같은 페이지는 필터와 모달이 많다. 전부 서버로 밀 수는 없다. 반대로 전부 클라이언트에 두면 첫 렌더링이 무겁다.
그래서 기준을 다시 세웠다.
초기 화면을 만드는 데이터와 레이아웃은 서버에서 확정한다. 사용자가 누르고 고르는 상태만 클라이언트에 남긴다.
이렇게 말하면 당연해 보이지만, 큰 컴포넌트를 쪼갤 때는 계속 유혹이 생긴다. “이 상태도 여기 있으면 편한데?” 하고 클라이언트에 남겨두면 금방 다시 커진다. 이때 대형 client 컴포넌트 5개를 쪼갠 작업은 그런 유혹을 정리하는 과정이었다.
LCP는 이미지 하나만의 문제가 아니었다
이미지도 계속 손봤다. next/image로 바꾸고, sizes를 넣고, 히어로 이미지를 preload하고, Suspense 스트리밍도 붙였다.
처음에는 LCP가 안 좋은 이유를 “이미지가 크다” 정도로 생각하기 쉽다. 그런데 실제로는 요청 시점, 실제 렌더링 크기, 애니메이션 시작 타이밍, loading boundary가 다 같이 영향을 준다.
예를 들어 40px짜리 썸네일인데 브라우저가 100vw 이미지를 요청하면 그건 그냥 낭비다. 반대로 히어로 이미지는 늦게 요청되면 전체 화면이 늦게 잡힌다. 거기에 GSAP이 첫 렌더링과 겹치면 화면이 버벅이는 것처럼 보인다.
그래서 이때의 이미지 최적화는 단순 교체가 아니라, “어느 이미지를 언제, 어떤 크기로, 어떤 상태에서 보여줄 것인가”를 다시 정리하는 작업이었다.
비용 최적화는 생각보다 빨리 필요해졌다
Vercel ISR write와 Fast Origin Transfer 비용도 손봤다.
팬 사이트라서 처음에는 비용이 크게 문제될 거라고 생각하지 않았다. 그런데 커뮤니티 기능이 들어가면 작은 쓰기가 많아진다. 좋아요, 조회수, 덱 투표, 댓글 같은 것들이 전부 캐시 무효화와 연결될 수 있다.
이때 배운 건 모든 변경이 모든 캐시를 흔들 필요는 없다는 것이다.
덱 투표 하나가 전체 사이트 revalidate로 이어지면 너무 비싸다. 그래서 POST rate limit을 붙이고, on-demand invalidation 범위를 더 좁혔다. 어떤 API가 어떤 경로를 다시 만들게 할지 명확히 해야 비용이 예측 가능하다.
Sentry에 찍힌 에러는 그냥 지나치면 계속 쌓인다
4월에는 Sentry 에러도 꽤 많이 정리했다. DANTE-6/7/8/9 같은 실제 운영 에러를 보고, 이미지 다운로드, hydration mismatch, 잘못된 import, 없는 env에서 빌드가 깨지는 문제를 하나씩 고쳤다.
특히 쓰기 API에 requireActiveUser를 넓게 적용한 건 중요한 작업이었다. 덱, 댓글, 투표, 관리자 기능이 늘어나면 “이 정도는 괜찮겠지”라고 둔 API가 나중에 문제를 만든다. Zod 검증과 active user 체크를 붙이면서 최소한의 경계를 다시 세웠다.
CI도 운영의 일부였다
이 구간에는 CI 관련 커밋도 많다.
Node 22 기준으로 lockfile을 다시 만들고, Dependabot PR에서 dummy env로 빌드가 돌게 하고, Supabase service key가 필요한 구간과 placeholder로도 넘어가야 하는 구간을 나눴다.
Next.js 앱은 빌드 중에 데이터 접근이 섞이는 순간 CI가 까다로워진다. 특히 generateStaticParams나 metadata 쪽에서 Supabase를 만지면, 실제 서비스 키가 없을 때 빌드가 터진다. 그래서 빌드가 필요한 환경과 실제 데이터가 필요한 환경을 분리해야 했다.
정리하면
이 시기의 작업은 “더 멋진 기능”보다는 “서비스가 덜 흔들리게 만드는 일”이었다.
- 클라이언트 번들을 줄이고 서버 렌더링 경계를 다시 잡았다.
- LCP와 이미지 요청 크기를 줄였다.
- ISR write와 Fast Origin Transfer 비용을 줄였다.
- Sentry에 찍힌 실제 오류를 정리했다.
- 쓰기 API의 인증·검증 경계를 넓혔다.
- CI가 환경변수 때문에 불필요하게 깨지지 않게 했다.
블로그에 쓰기엔 덜 화려하지만, 이런 작업을 안 하면 어느 순간 새 기능을 넣을 때마다 어딘가가 같이 무너진다. 4월 중순은 그 무너지는 지점을 미리 줄인 기간이었다.
