보안 점검과 PageSpeed 성능 개선
오늘의 목표와 맥락
운영 중인 블로그 서비스에 대해 두 가지를 진행했다. 하나는 백엔드 전반의 보안 점검이고, 다른 하나는 PageSpeed 측정에서 지적된 성능 문제(캐시 수명 미설정 1,020KiB, 렌더링 차단 CSS 2,250ms, 미사용 JS 259KiB, llms.txt 형식 위반)의 해결이다.
측정 결과를 뜯어 보니 원인은 배포 구조에 있었다. Nginx가 프론트 빌드 산출물을 정적 서빙하는데 /assets 경로에 캐시 헤더가 전혀 없었고, Ubuntu 기본 Nginx는 HTML만 gzip 압축하므로 JS와 CSS가 원본 크기 그대로 전송되고 있었다. llms.txt는 저장소에 아예 없어서 SPA fallback으로 index.html이 대신 응답되어 "형식 위반"으로 잡힌 것이었다.
구현한 내용
성능 개선
- Nginx 성능·보안 스니펫 도입 — 해시 파일명 자산은 1년 immutable 캐시, gzip을 JS/CSS/SVG/JSON까지 확장, 보안 헤더(nosniff, X-Frame-Options, HSTS, Referrer-Policy) 추가. certbot이 관리하는 사이트 설정 본문은 건드리지 않고
include한 줄만 삽입하는 구조로 만들어 SSL 설정과 충돌하지 않게 했고, 이후 스니펫 변경은 배포 스크립트가 자동 동기화한다. - 라우트 지연 로딩 — 관리자 화면 전체와 마크다운 렌더러(marked, highlight.js)를 lazy 청크로 분리해 초기 번들을 620KB에서 326KB로 줄였다.
- 폰트 CSS 비동기 분리 — 빌드된 CSS 195KB 중 88%가 한글 서브셋 @font-face 선언 248개였다. 이 선언을 동적 import 로 분리해 렌더링 차단 CSS를 gzip 기준 57KiB에서 5.4KiB로 줄였다.
- llms.txt 표준 형식 작성 — H1 제목과 주요 페이지 링크를 갖춘 형식으로 신규 작성했다.
보안 강화 (TDD, Red → Green 순서)
- OAuth 콜백 토큰을 URL 프래그먼트로 전달 — 기존에는
?token=쿼리스트링으로 리다이렉트해서 JWT가 access log와 브라우저 히스토리에 평문으로 남았다. 프래그먼트는 서버로 전송되지 않으므로 로그 유출 경로가 사라진다. 프론트 콜백 페이지는 구버전 호환 폴백을 유지했다. - state 쿠키 secure 플래그 — HTTPS 환경에서만 붙도록 설정 기반으로 분기해, 로컬 http 개발 흐름은 깨지지 않게 했다.
- SECRET_KEY 필수화 — 공개된 기본값 폴백을 제거하고 32자 미만이면 기동 자체를 거부하도록 했다. CI처럼 .env가 없는 환경은 테스트 픽스처가 앱 임포트 전에 테스트용 키를 주입한다.
설계와 판단
보안 점검에서 인젝션, IDOR, 관리자 권한 체크, 저장형 XSS(서버 nh3와 프론트 DOMPurify 이중 방어)는 문제 없음을 확인했고, 실제 조치가 필요한 항목만 골라 우선순위를 매겼다. 가장 시급한 것은 토큰의 쿼리스트링 노출과 SECRET_KEY 폴백 두 가지로 판단해 이번에 처리했다.
폰트는 unicode-range 서브셋 방식이라 로딩 자체는 정상이었다. 문제는 폰트 파일이 아니라 선언 CSS의 크기였고, font-display: swap 덕분에 선언을 비동기로 빼도 폴백 글꼴로 즉시 렌더되므로 체감 저하 없이 차단만 제거할 수 있다고 판단했다.
네트워크 종속 트리(HTML → JS → API 체인)는 CSR SPA의 구조적 특성이라 SSR 도입 없이는 근본 해결이 어렵고, 현재 규모 대비 비용이 커서 이번 범위에서 제외했다.
문제와 해결
PR 기반 흐름을 쓰려 했으나 gh CLI가 저장소 소유자와 다른 계정으로 로그인되어 있어 비공개 저장소에 PR을 만들 수 없었다. CI와 동일한 검사(pytest, ruff, eslint, 빌드)를 로컬에서 전부 통과시킨 뒤 로컬 스쿼시 머지로 main에 반영하는 방식으로 대응했고, main 푸시 시점에 CI가 자동 실행되므로 검증 공백은 없다.
작업 트리에 있던 별도 진행 중 변경(테스트 헬퍼의 cli_port 옵션)은 stash로 분리해 보안 커밋과 섞이지 않게 했다가 작업 후 복원했다.
검증한 내용
- 백엔드 pytest 91개 통과(신규 테스트 5개 포함), ruff 경고 0
- 프론트 eslint 경고 0, 빌드 성공 및 청크 분리 확인
- 재측정에서 전송량 감소 확인: CSS 192→57KiB, JS 606→103KiB (이후 폰트 분리로 렌더링 차단 CSS 5.4KiB까지 축소)