[AI 활용법 8] 성능 예산 — 숫자가 없으면 최적화는 정치다
Core Web Vitals와 ROI 우선순위로 AI 코드의 무게 잡기
시리즈 8편. “더 빠르게 해 줘”는 끝이 없다. 숫자를 박아 두면 끝이 보인다.
왜 성능 예산인가
LLM은 의외로 무거운 라이브러리를 잘 추천한다. 작은 문제 에 큰 도구 를 끌어오는 경향이 있다 — 학습 데이터에 그런 패턴이 흔하기 때문이다.
이걸 막으려면 사전 합의된 숫자 가 필요하다. “느려진 것 같은데”는 의견, “LCP 2.5s 넘었어”는 사실.
권장 기본 예산 (웹)
| 메트릭 | 기준 | 의미 |
|---|---|---|
| LCP | ≤ 2.5s | 가장 큰 콘텐츠가 보이는 시점 |
| INP | ≤ 200ms | 사용자 인터랙션 응답 |
| CLS | ≤ 0.1 | 시각적 안정성 |
| TTFB | ≤ 800ms | 첫 바이트 |
| 초기 JS 번들 | ≤ 200KB (gzip) | 첫 페인트 차단 자원 |
| 이미지 첫 화면 | ≤ 200KB | 위와 동일 |
이 숫자는 Google Web Vitals와 PageSpeed의 권장치를 따른다. 절대값 이라기보다 합의된 최저선.
Backend 예산
| 메트릭 | 기준 |
|---|---|
| p50 응답 | ≤ 100ms |
| p95 응답 | ≤ 300ms |
| p99 응답 | ≤ 1s |
| DB 쿼리/요청 | ≤ 5 |
p99까지 보지 않으면 꼬리(tail) 의 문제를 놓친다. 평균은 거짓말을 잘 한다.
ROI 우선순위 — 어디부터 손보는가
최적화는 체감 효과 / 작업 비용 의 비율이 큰 순서로 한다.
1. 네트워크 — 캐싱, gzip, CDN, prefetch, dedupe
2. 렌더링 — 차단 리소스 제거, lazy load, defer
3. 알고리즘 — 자료구조 선택, 인덱스, N+1 제거
4. 마이크로 — 인스트럭션 단위 최적화 (마지막)
LLM에게 아무 최적화 시키면 4번부터 손대기 쉽다. 명시적으로 위에서부터 라고 알려 주자.
측정 없이 최적화하지 않는다
가장 흔한 실수: 체감 으로 “빨라졌다”고 끝내는 것. 다음 셋 중 하나는 반드시 거친다.
- Lighthouse (CI에 자동화 가능)
- Chrome Performance 패널 (수동, 깊은 분석)
performance.mark/performance.measure(코드에 직접)
performance.mark("a");
heavyWork();
performance.mark("b");
performance.measure("heavy", "a", "b");
console.table(performance.getEntriesByName("heavy"));
before/after 숫자를 PR 본문 에 박는 것을 규칙으로 만들면, 성능 PR이 말장난이 안 된다.
Lightweight-first
“일단 만들고 나중에 최적화”가 아니라 “처음부터 가벼운 구조를 기본값으로”.
- 작은 유틸 하나를 위해 거대 라이브러리 추가 금지
- 표준 라이브러리·플랫폼 API를 우선 고려
- 의존성 추가는 별도 커밋 + 이유 명시
LLM에게 “moment.js 써서 시간 포맷 해 줘”라고 시키지 말고, “Intl.DateTimeFormat 가능한가?” 부터 물어보자. 99% 가능하다.
N+1 — 가장 흔한 백엔드 함정
ORM과 LLM의 조합이 만드는 단골 사고:
# N+1
for user in User.objects.all():
print(user.posts.count()) # 매 user마다 쿼리 1개
# fix
for user in User.objects.annotate(c=Count("posts")):
print(user.c) # 1쿼리
LLM에게 “단일 쿼리 또는 prefetch_related/select_related 명시” 를 요구하면 자동으로 안 잡히는 경우가 많다.
가상 스크롤 — 큰 리스트의 디폴트
100개 이상의 행을 한 번에 렌더링한다면 가상 스크롤(virtualized list)을 디폴트로 한다. React에서는 react-window, Vue에서는 vue-virtual-scroller. 직접 구현도 어렵지 않다 — 뷰포트에 들어오는 것만 DOM에 둔다 가 핵심.
폰트와 이미지 — 잊기 쉬운 둘
- 폰트:
font-display: swap, 서브셋,<link rel="preload" as="font"> - 이미지:
width/height명시 (CLS 방지),loading="lazy",srcset/sizes(해상도별)
이 둘만 챙겨도 Core Web Vitals 점수가 크게 움직인다.
캐싱 — 무효화 전략부터
캐시는 넣는 것 보다 빼는 것 이 어렵다. 시작 전에 정한다:
- 무효화 키 — mtime, ETag, version, hash
- 스코프 — per-user / global / region
- 만료(TTL) — 짧을수록 안전, 길수록 빠름
LazyClaude의 mtime 캐시(이 시리즈 [LazyClaude 심층 1편] 참고)도 무효화 키 가 명확해서 잘 작동한 사례.
한 줄 요약
숫자가 없는 최적화는 정치다. 예산 을 박고, 측정 으로 합의하라.
이전 편: AI 활용법 7 — 모듈화 임계값
시리즈 진행 (8편 / 진행 중)
- 정직성
- 컨텍스트 관리
- 자율모드
- 가드레일
- Git 워크플로우
- ADR
- 모듈화 임계값
- 성능 예산