웹 성능 측정 — curl·sips·Python 만으로 TTFB·페이지 무게·이미지 audit 다 잡기
결론 먼저: Lighthouse·WebPageTest 띄우지 않아도 페이지 속도·무게·이미지 비대를 30 줄 스크립트로 정량 측정 가능. CI 에 박아두면 회귀 자동 잡힘.
왜 직접 측정해야 하나
브라우저 직감으로 "느린 거 같아…" 라고 끝나면 fix 가 우선순위 안 잡힘. 다음 세 가지를 숫자로 잡아야 함:
- TTFB (Time To First Byte) — 서버가 응답 시작까지. 인프라·CDN·SSR 지표.
- 페이지 총 무게 — HTML + JS + CSS + 이미지 합산. LCP·INP 의 원료.
- 이미지 가중 — 거의 모든 느린 사이트의 진짜 범인. 카드 200×200 표시인데 원본 1024×1024 다운로드 식.
Lighthouse 는 종합 지표 좋지만 로컬 환경 ↔ 네트워크 가변성 때문에 매번 다른 점수. curl + sips 기반 측정은 결정론적·ms 단위 정확·cron·CI 자동화 가능. Lighthouse 는 PR 마지막 점검용, 이쪽은 일상 audit.
도구 — 다 macOS·Linux 기본 또는 표준
| 도구 | 설치 | 용도 |
|---|---|---|
curl |
macOS·Linux 기본 | TTFB·total·전송 속도·페이지 size |
sips |
macOS 기본 (없으면 identify from ImageMagick) |
이미지 해상도·bytes |
python3 |
macOS·Linux 기본 | HTML 파싱·이미지 src 추출·집계 |
별도 설치 없음.
Part 1 — curl -w 로 TTFB·total·size
핵심: -w (write-out) format string
curl -w 는 요청 끝나면 timing·response 정보를 templated string 으로 출력. 7 개 변수가 핵심:
curl -w "
DNS: %{time_namelookup}s
Connect: %{time_connect}s
TLS: %{time_appconnect}s
TTFB: %{time_starttransfer}s
Total: %{time_total}s
Size: %{size_download} bytes
Speed: %{speed_download} bytes/s
" -o /tmp/out.html -s "https://taystudios.com/blog/ko/"
-o /tmp/out.html 으로 body 를 파일에 저장, -s 로 progress bar 끔.
변수 의미
| 변수 | 무엇을 측정 |
|---|---|
time_namelookup |
DNS 해석 끝까지 |
time_connect |
TCP 핸드셰이크 끝까지 (namelookup 포함) |
time_appconnect |
TLS 핸드셰이크 끝까지 (HTTPS only) |
time_pretransfer |
요청 보내기 직전 (협상 완료) |
time_starttransfer |
TTFB — 응답 첫 바이트 받은 시점 |
time_total |
응답 body 완전히 받은 시점 |
size_download |
받은 body 바이트 (응답 본문 size) |
speed_download |
평균 다운로드 속도 (bytes/s) |
각 값은 누적이라 단계별 시간 = 다음 단계 - 이전 단계:
DNS: time_namelookup
TCP: time_connect - time_namelookup
TLS: time_appconnect - time_connect
서버: time_starttransfer - time_appconnect ← 실제 서버 처리 + 응답 시작
전송: time_total - time_starttransfer ← body 전송
실측 예시 (taystudios.com/blog/ko/)
DNS: 0.014286s ← 14ms DNS
Connect: 0.048982s ← 35ms TCP
TLS: 0.089744s ← 41ms TLS
TTFB: 0.337152s ← 247ms 서버 응답 (GitHub Pages CDN)
Total: 0.337994s ← 0.8ms body 전송
Size: 21702 bytes ← HTML 22KB
해석: TTFB 247ms 가 시간의 73% — CDN edge 응답. 개선 여지는 같은 edge 에서 caching warm-up. HTML 자체 22KB body 는 0.8ms — 무시 가능.
캐시 우회 — 매번 fresh 측정
CDN·브라우저 캐시 때문에 두 번째 요청은 빨라짐. baseline 측정엔 매번 cache miss 강제:
# Cache busting: URL 에 timestamp 쿼리
curl -s "https://taystudios.com/blog/ko/?ts=$(date +%s)"
# 또는 Cache-Control 헤더로 명시
curl -s -H "Cache-Control: no-cache" "https://taystudios.com/blog/ko/"
# 둘 다 — 가장 안전
curl -s -H "Cache-Control: no-cache" "https://taystudios.com/blog/ko/?ts=$(date +%s)"
?ts= 같은 dummy query 는 CDN 캐시 키를 바꿔 새 응답 강제. Cache-Control 헤더는 상위 proxy 에 신호.
Part 2 — sips 로 이미지 해상도·bytes
sips 는 macOS 기본 image 도구. 1 줄로 해상도 query:
sips -g pixelWidth -g pixelHeight cover.jpg
# 출력:
# /path/cover.jpg
# pixelWidth: 1024
# pixelHeight: 1024
bytes 는 stat:
stat -f %z cover.jpg # macOS
stat -c %s cover.jpg # Linux
한 폴더 전체 audit (Python)
import subprocess
from pathlib import Path
for f in sorted(Path('blog/assets/posts').rglob('cover.*')):
sz = f.stat().st_size
out = subprocess.check_output(
['sips', '-g', 'pixelWidth', '-g', 'pixelHeight', str(f)],
stderr=subprocess.DEVNULL
).decode()
dims = [l.split(':')[1].strip() for l in out.split('\n') if 'pixel' in l]
w, h = dims[0], dims[1]
flag = '🔴' if sz > 200_000 else '🟡' if sz > 50_000 else '✓'
print(f'{sz/1024:>7.1f}KB {w}x{h} {flag} {f}')
실측 (이 블로그 audit 결과 일부)
416.5KB 1332x2088 🔴 aws-summit-2024/cover.jpg ← og:image 비례 X
375.0KB 1024x1024 🔴 hypothesis-testing/cover.jpg
375.0KB 1024x1024 🔴 uncertainty-variance/cover.jpg
375.0KB 1024x1024 🔴 t-test/cover.jpg
4.5KB 200x200 ✓ career-ktds-newgrad-2021/cover.jpg ← 회사 로고
4.1KB 225x225 ✓ kaist-grad-mech-written-set1/cover.png
진단: 통계 시리즈 7편 모두 1024×1024 / 375KB — 카드 썸네일은 200×200 만 표시되는데 25배 큰 원본 다운로드. 즉시 fix 대상.
Part 3 — 페이지별 이미지 가중 (HTML 파싱 + 집계)
curl -s URL 로 받은 HTML 에서 <img src> 다 뽑아 총합:
import re
from pathlib import Path
# 빌드된 HTML 에서 페이지별 cover 총합
for page in ['blog/ko/index.html', 'blog/ko/category/data/index.html']:
h = open(page).read()
imgs = re.findall(r'src="([^"]*cover\.[a-z]+)"', h)
total = 0
for src in imgs:
f = Path('blog') / src
if f.exists():
total += f.stat().st_size
print(f'{page}: {len(imgs)} covers · {total/1024:.1f} KB')
실측 (현 블로그)
| 페이지 | covers | total |
|---|---|---|
/blog/ko/ (홈 첫 페이지 10글) |
10 | 1524 KB |
/blog/ko/category/data/ (통계 7편) |
7 | 2625 KB 🔴 |
/blog/ko/category/reviews/career/ (회사 로고 6글) |
6 | 250 KB ✓ |
통계 카테고리만 따로 2.6 MB. 모바일 4G 환경 (~50Mbps) 에서 0.4 초 추가 다운로드 = LCP 직격.
💡 HTML 자체는 빠른데 (TTFB 247ms) 이미지 때문에 체감 느림 같은 케이스를 1 분 안에 진단할 수 있는 게 이 방법의 핵심 가치.
Part 4 — 전체 페이지·자원 합산 (live URL 기준)
빌드된 HTML 이 없는 환경 (live URL audit) 에선 curl 로 받아서 자원 URL 추출 → 각자 curl 로 size 측정:
#!/bin/bash
URL="https://taystudios.com/blog/ko/"
TMPDIR=$(mktemp -d)
# 1. HTML 받기
curl -s "$URL?ts=$(date +%s)" -o "$TMPDIR/page.html"
HTML_SIZE=$(stat -f %z "$TMPDIR/page.html")
# 2. 모든 이미지·JS·CSS URL 추출 (절대화)
python3 -c "
import re, sys
from urllib.parse import urljoin
base = '$URL'
html = open('$TMPDIR/page.html').read()
patterns = [
r'<img[^>]+src=[\"\']([^\"\']+)[\"\']',
r'<script[^>]+src=[\"\']([^\"\']+)[\"\']',
r'<link[^>]+href=[\"\']([^\"\']+)[\"\'][^>]+rel=[\"\']stylesheet[\"\']',
]
for p in patterns:
for m in re.findall(p, html):
if m.startswith(('http','//')):
print(m if not m.startswith('//') else 'https:'+m)
else:
print(urljoin(base, m))
" > "$TMPDIR/urls.txt"
# 3. 각 자원 size 측정
TOTAL=$HTML_SIZE
while read u; do
sz=$(curl -o /dev/null -s -w "%{size_download}" "$u")
echo "$sz $u"
TOTAL=$((TOTAL + sz))
done < "$TMPDIR/urls.txt"
echo
echo "HTML: $((HTML_SIZE / 1024)) KB"
echo "Total: $((TOTAL / 1024)) KB ($(echo "scale=2; $TOTAL/1048576" | bc) MB)"
CI 환경에서 매 PR 마다 돌리면 페이지 무게 회귀를 정량 잡음.
Part 5 — CWV 와 연결
| 측정값 | CWV 메트릭 | 임계 |
|---|---|---|
| TTFB | (FCP 의 일부) | 200ms 이하 = good, 600ms+ = poor |
| 페이지 무게 | LCP (Largest Contentful Paint) | 2.5s 이하 = good (4G/모바일 기준) |
| 이미지 가중 | LCP·INP 둘 다 | LCP 의 70% 가 이미지인 사이트가 흔함 |
대략적 환산 (3G slow 시뮬): - HTML 50KB + 이미지 500KB = ~3s LCP (good) - HTML 50KB + 이미지 2MB = ~8s LCP (poor)
이미지 무게가 LCP 의 1차 변수. 이게 이 글의 audit 방법론이 강조하는 이유.
Part 6 — Lighthouse 와 비교
| curl + sips audit | Lighthouse | |
|---|---|---|
| 측정 종류 | 정량 (bytes, ms) | 종합 점수 (0-100) |
| 환경 | 결정론적 (네트워크만 가변) | 가변 (CPU·throttling 시뮬레이션) |
| 속도 | < 5 초 | 30~60 초/run |
| CI 적합 | ✓ 자동화 쉬움 | △ headless Chrome 필요 |
| 발견 가능 | 무거운 자원·TTFB | + JS execution·CLS·a11y·SEO 점수 |
병행 사용: - 일상 audit·CI 회귀 잡기 → curl + sips - 정기 큰 진단 (월/분기) → Lighthouse + PageSpeed Insights
CI 자동화 — bash 한 줄로
# .github/workflows/perf-check.yml 안에 추가
- name: Cover image size guard
run: |
fail=0
for f in blog/assets/posts/*/cover.*; do
sz=$(stat -c %s "$f")
if [ "$sz" -gt 100000 ]; then
echo "::error::$f is $((sz/1024))KB (>100KB) — optimize"
fail=1
fi
done
exit $fail
PR 에서 cover 가 100KB 넘으면 CI 빨강. 직접 손 안 대도 회귀 차단.
정리
| 단계 | 도구 | 1 줄 |
|---|---|---|
| TTFB·페이지 시간 | curl -w |
curl -w "%{time_starttransfer}s\n" -o /dev/null -s URL |
| 이미지 audit | sips + find + stat |
find . -name cover.\* -exec sips -g pixelWidth -g pixelHeight {} + |
| 페이지별 가중 | Python re.findall |
HTML → <img src> 모음 → 각 file size sum |
| CI guard | bash + stat -c %s |
PR 마다 자동 size threshold 검증 |
진정한 효과: 매 배포 전·후 bash perf-baseline.sh 한 번 돌리면 회귀·개선 정량 비교. 느낌이 아니라 숫자로 토론 가능.
📝 이 글의 실제 측정 데이터는 본 블로그 (
taystudios.com/blog/) 의 cover 이미지 audit 결과입니다. 결과적으로 통계 시리즈 7편 cover 가 1024×1024 PNG (각 375KB) 인 게 발견됐고, WebP 800×800 로 일괄 변환 → 페이지당 평균 1MB+ 절감 예정.
댓글