성능 예산을 CI 게이트로 만들기

출시 후 최적화는 대부분 실패합니다. LCP·INP·CLS 목표를 착수 시점에 숫자로 정하고 빌드에서 막는 방법.

성능 측정 결과가 표시된 CI 파이프라인 화면

"출시하고 나서 최적화하죠"라는 말은 대부분 최적화하지 않겠다는 뜻입니다. 출시 후에는 기능 요구가 밀려들고, 성능 작업은 눈에 보이는 성과가 없어서 항상 다음 스프린트로 밀립니다.

숫자를 먼저 정한다

착수 시점에 목표를 숫자로 씁니다. 저희가 기본으로 쓰는 값은 이렇습니다.

  • LCP 2.0초 이하 (모바일, 4G 시뮬레이션)
  • INP 200ms 이하
  • CLS 0.05 이하
  • 초기 JS 번들 120KB 이하 (gzip)
  • 히어로 이미지 180KB 이하

이 값들은 협상 대상입니다. 다만 협상은 착수 시점에 하고, 그 뒤로는 고정합니다. 개발 중에 목표를 낮추기 시작하면 목표가 없는 것과 같습니다.

번들 크기는 빌드에서 막는다

가장 쉽게 자동화되는 항목입니다. 빌드 산출물의 gzip 크기를 계산해 임계값을 넘으면 실패시킵니다. 여기서 중요한 것은 전체 크기가 아니라 초기 로드에 필요한 크기를 재는 것입니다. 지연 로드되는 청크까지 합산하면 숫자가 무의미해집니다.

가장 흔한 위반

실무에서 번들 예산을 깨는 원인은 거의 정해져 있습니다.

  • 날짜 라이브러리 전체 임포트 (로케일 데이터가 통째로 따라옵니다)
  • 애니메이션 라이브러리를 초기 번들에 넣는 것
  • 아이콘 세트 전체 임포트
  • 차트 라이브러리를 첫 화면에서 로드

애니메이션 라이브러리는 특히 자주 걸립니다. 스크롤 애니메이션 때문에 GSAP 같은 라이브러리를 넣는 경우가 많은데, 대부분의 진입 애니메이션은 CSS animation-timeline: view() 로 처리할 수 있습니다. 라이브러리가 정말 필요한 구간이 한두 곳이라면 그 구간에서만 동적 임포트하면 됩니다.

LCP 는 측정 환경을 고정한다

실측값은 환경에 따라 흔들리므로 CI 에서는 조건을 고정합니다. 같은 컨테이너 이미지, 같은 CPU 스로틀링 배수, 같은 네트워크 프로파일에서 3회 측정해 중앙값을 씁니다. 1회 측정으로 게이트를 걸면 재실행만 반복하게 됩니다.

실패를 어떻게 다룰 것인가

처음부터 빌드를 차단하면 팀이 게이트 자체를 우회하기 시작합니다. 2주 정도는 경고로 운영하며 현재 값을 파악하고, 그 뒤에 차단으로 전환합니다. 이미 예산을 초과한 상태라면 현재 값을 상한으로 잡고 더 나빠지지 않게 막는 것부터 시작합니다.

LCP 요소는 절대 숨기지 않는다

마지막으로 자주 보는 실수 하나. 진입 애니메이션을 위해 히어로 제목을 opacity: 0 으로 두고 스크립트나 옵저버로 나타내는 구현입니다. 이렇게 하면 LCP 요소가 스크립트 실행 이후에야 그려지므로 LCP 가 통째로 밀립니다.

애니메이션이 필요하면 주변 요소를 움직이세요. 제목 자체는 첫 페인트에서 완전한 불투명도로 그려져야 합니다.