본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 19:33

Shopify Web Vitals 데이터 6개월간의 기록: INP를 실제로 변화시킨 요소들

요약

Shopify 스토어의 INP(Interaction to Next Paint) 지표를 380ms에서 140ms로 개선한 6개월간의 실무 사례를 다룹니다. LCP와 달리 응답성을 측정하는 INP의 중요성을 강조하며, 실제 필드 데이터를 기반으로 한 최적화 전략을 공유합니다.

핵심 포인트

  • 무거운 이벤트 핸들러 분리로 110ms 단축
  • 장바구니 드로어 JS 지연 로딩으로 80ms 개선
  • LCP(로딩)와 INP(응답성)는 서로 다른 최적화 영역임
  • 합성 도구보다 실제 필드 데이터(CrUX) 기반의 측정이 중요
  • raxxo.shop에서 6개월 동안 INP가 380ms에서 140ms로 감소

  • 하나의 무거운 이벤트 핸들러 (event handler)를 분리하는 것만으로 110ms 단축

  • 장바구니 드로어 (cart drawer) JS를 지연 로딩 (Lazy-loading) 하여 INP 80ms 개선

  • 서드파티 분석 (third-party analytics) 작업을 지연 (Deferring) 시켰으나 측정 가능한 변화는 없음

raxxo.shop의 INP는 6개월 동안 380ms에서 140ms로 감소했습니다. 세 가지 변화가 거의 모든 성과를 만들어냈습니다. 도움이 될 것이라고 확신했던 두 가지 변화는 아무런 효과가 없었습니다. 여기에는 제 노트북에서 측정된 합성 Lighthouse 점수가 아닌, 실제 필드 데이터 (field data)가 있습니다.

왜 INP가 내가 쫓던 지표를 대체했는가

지난 2년 동안 저는 Largest Contentful Paint (LCP)를 유일하게 중요한 수치로 취급했습니다. LCP가 2초 미만으로 유지될 때까지 이미지를 최적화하고, 폰트를 프리로드 (preload) 하며, 렌더링 차단 CSS (render-blocking CSS)를 다듬었습니다. 그러다 2024년 3월, Interaction to Next Paint (INP)가 Core Web Vital이 되면서 저의 초록색 점수들은 하룻밤 사이에 노란색으로 변했습니다.

INP는 탭이나 클릭 후 페이지가 시각적으로 반응하는 데 걸리는 시간을 측정합니다. 첫 번째 상호작용이 아니라, 전체 방문 기간 중 대략적으로 가장 나쁜 상호작용을 측정합니다. 쇼핑몰이 화면을 빠르게 그려내더라도(paint), 장바구니 버튼을 눌렀을 때 스레드 (thread)가 400ms 동안 멈춘다면 여전히 망가진 것처럼 느껴질 수 있습니다. 이러한 지연은 Lighthouse에서 보이지 않는데, 그 이유는 Lighthouse가 실제 쇼핑객처럼 무언가를 클릭하지 않기 때문입니다.

필드 데이터 (Chrome User Experience Report, 실제 Android 및 iPhone 트래픽)를 기준으로 확인한 저의 시작점은 75 백분위수 (75th percentile) INP가 380ms였습니다. Google의 "좋음 (good)" 임계값은 200ms입니다. 즉, 방문자의 4분의 1이 380ms보다 느린 경험을 하고 있었고, 중간 사양의 휴대폰에서는 중간값 (median) 경험이 느릿느릿했습니다. 데스크톱은 괜찮았습니다. 데스크톱은 항상 괜찮습니다. 아무도 2019년형 Android 휴대폰의 CPU 성능을 가진 데스크톱으로 쇼핑하지 않으니까요.

저를 놀라게 한 점은 이렇습니다. 저의 LCP는 1.9초로 기술적으로는 좋았지만, 실제로 사용해 보면 상점이 느리게 느껴졌다는 것입니다. LCP는 로딩 (loading)을 측정합니다. INP는 응답성 (responsiveness)을 측정합니다. 이들은 해결 방법이 다른 별개의 문제이며, 한 쪽을 위한 해결책이 다른 쪽을 돕는 경우는 드뭅니다.

저는 6개월간의 CrUX 데이터와 함께, 모든 상호작용(interaction)의 지속 시간을 작은 엔드포인트에 기록하는 저만의 실사용자 모니터링 (RUM, Real User Monitoring) 스크립트를 추출했습니다. 합성 도구(Synthetic tools)는 대부분의 사람들이 사용하는 기기보다 더 빠른 하드웨어에서 실행되기 때문에 INP에 대해 거짓 정보를 제공합니다. 유일하게 정직한 수치는 실제 필드(field)에서 나옵니다. 더 넓은 성능 관점의 이야기를 원하신다면, Shopify Theme Performance From 62 to 98 Lighthouse가 로드 시간(load-time) 측면을 다루고 있지만, 그 작업은 INP에는 거의 영향을 미치지 않았습니다. 저는 다른 사고 모델(mental model)로 처음부터 다시 시작해야 했습니다. 바이트(bytes)를 세는 것을 멈추고, 메인 스레드(main thread)가 차단된 밀리초(milliseconds)를 세기 시작하는 것입니다.

메인 스레드가 병목 지점(bottleneck)입니다. 모든 탭(tap)은 이미 실행 중인 JavaScript 뒤에서 줄을 서서 기다립니다. 줄을 해결하면, INP가 해결됩니다.

수치를 실제로 변화시킨 세 가지 변화

첫 번째 변화: 하나의 무거운 이벤트 핸들러(event handler)를 분리했습니다. 제 테마에는 문서(document)에 부착된 단 하나의 클릭 리스너(click listener)가 있었는데, 페이지 어디에서든 탭이 발생할 때마다 긴 if-else 체인을 실행했습니다. 이는 장바구니 담기(add-to-cart), 빠른 보기(quick-view), 메뉴 토글(menu toggle), 스와치 선택(swatch selection) 및 기타 8가지 케이스를 확인했습니다. 한 번의 탭이 그 모든 로직을 실행한다는 것을 의미했습니다.

저는 이를 특정 요소(element)에 부착된 범위가 지정된 리스너(scoped listeners)로 분리했고, 비용이 많이 드는 부분(장바구니 합계 재계산 및 드로어(drawer) 재렌더링)을 requestIdleCallback 뒤로 옮겨 페인트(paint) 전이 아닌 후에 실행되도록 했습니다. INP는 380ms에서 270ms로 떨어졌습니다. 그 단 하나의 변화가 110ms의 가치가 있었습니다. 저는 RUM 데이터를 거의 믿지 못해서 3주 동안 기다렸습니다. 수치는 유지되었습니다.

두 번째 변화: 장바구니 드로어(cart drawer) JavaScript를 지연 로딩(lazy-load)했습니다. 약 34KB의 파싱(parsing) 및 실행이 필요한 드로어 코드는 방문자가 장바구니를 전혀 열지 않더라도 모든 페이지에서 실행되었습니다. 제품 페이지에서 이 JS는 메인 스레드(main thread)를 두고 다른 모든 요소와 경쟁했습니다. 저는 이를 장바구니 트리거와 처음 상호작용할 때만 로드되도록 변경했습니다. 이제 첫 번째 장바구니 열기 동작은 50ms 더 느려졌지만(의도적인 동작이므로 수용 가능합니다), 드로어 로직이 스레드를 미리 점유하지 않게 됨으로써 페이지의 다른 모든 상호작용은 더 빨라졌습니다. INP가 270ms에서 190ms로 이동했습니다. 80ms의 가치가 있었습니다.

세 번째 변화: 90KB의 JavaScript를 주입하고 스크롤 시 레이아웃 스래싱(layout-thrashing) DOM 쓰기를 실행하던 제3자 리뷰 위젯을 교체했습니다. 이를 빌드 타임(build time)에 가져오는 정적 렌더링 HTML로 교체하고, 상호작용이 필요한 "리뷰 작성" 양식은 버튼 뒤로 지연 로딩(lazy-load)되도록 했습니다. 이는 단일 바이트 감소 측면에서는 가장 컸지만, INP 개선 측면에서는 중간 정도의 성과였습니다. 왜냐하면 해당 위젯의 가장 큰 문제는 INP가 부분적으로 포착하는 스크롤 저크(scroll jank)였기 때문입니다. INP는 190ms에서 140ms로 감소했습니다. 50ms의 가치가 있었습니다.

세 가지 변화를 통해 총 240ms를 줄였으며, 140ms는 다음 성능 저하가 발생하기 전까지 여유를 둔 채 200ms "좋음(good)" 기준선 아래에 안정적으로 위치합니다. 이 중 어떤 것도 테마를 재구축할 필요는 없었습니다. 상호작용 중에 메인 스레드를 차단하는 JavaScript가 무엇인지 찾아내어 이를 나중에 실행되도록 옮기거나 제거하기만 하면 되었습니다.

만약 Shopify를 운영 중이고 사용 중인 테마가 인기 있는 유료 테마라면, 먼저 전역 문서 클릭 핸들러(global document click handler)가 있는지 확인하세요. 이런 핸들러는 어디에나 있으며, 여러분이 회복할 수 있는 가장 저렴한 100ms가 될 것입니다.

도움이 될 것이라 확신했지만 효과가 없었던 두 가지 변화

변화 A: 모든 제3자 분석(analytics) 및 트래킹 픽셀(tracking pixels)을 지연(defer)시켰습니다. requestIdleCallback과 강제 지연(hard delay)을 사용하여 분석 도구, 픽셀, 히트맵(heatmap) 도구가 페이지가 상호작용 가능한 상태가 된 후에 로드되도록 옮겼습니다. 이러한 스크립트들은 악명이 높기 때문에 큰 INP 개선을 기대했습니다. 하지만 필드 데이터(field data)는 0ms도 움직이지 않았습니다. 성능 저하도, 개선도 아니었습니다. 통계적 노이즈(statistical noise)일 뿐이었습니다.

왜일까요? 그 스크립트들은 대부분 로드 시점에 한 번 실행된 후에는 조용히 머물러 있기 때문입니다. 이들은 시작 단계에서 LCP(Largest Contentful Paint)와 총 차단 시간(Total Blocking Time)에는 영향을 주지만, INP가 측정하는 상호작용(interaction) 중에는 실행되지 않았습니다. 방문자들의 느린 탭은 세션 시작 8초 후에 발생했으며, 이는 분석(analytics) 작업이 끝난 지 한참 뒤였습니다. 저는 지난 2년 동안 엉뚱한 스크립트를 탓하고 있었습니다. 지연 실행(deferral)은 로드 지표를 약간 개선하는 데 도움이 되었고 그것으로 충분했지만, 저에게 중요한 교훈을 주었습니다. INP 문제는 시작 스크립트가 아니라 상호작용 핸들러(interaction handlers)에 존재한다는 사실입니다.

변경 사항 B: 저는 화면 하단(below-the-fold) 섹션에 content-visibility: auto를 추가했습니다. 이는 화면 밖에 있는 콘텐츠의 렌더링 작업을 건너뛰는 실제 CSS 기능입니다. 이론적으로는 메인 스레드(main thread)의 작업을 줄여 모든 면에서 도움이 되어야 합니다. 하지만 제 현장 데이터(field data)에서 INP는 꿈쩍도 하지 않았습니다. 초기 렌더링에는 약간의 도움을 주었지만(브라우저가 숨겨진 섹션의 레이아웃을 건너뜀), 상호작용은 여전히 동일하게 무거운 JavaScript 핸들러에 부딪혔고, 병목 현상(bottleneck)은 레이아웃이 아니라 바로 그 JavaScript였습니다.

두 번의 실패에서 나타난 공통된 패턴은 다음과 같습니다. 제 문제는 스크립팅(scripting)에 있었는데, 저는 렌더링과 로딩을 최적화하고 있었습니다. INP는 상호작용 중 발생하는 긴 작업(long tasks)에 의해 좌우됩니다. CSS 트릭과 로드 시점의 지연 실행은 잘못된 도구입니다. 그것들은 잘못된 지표를 위한 훌륭한 도구일 뿐입니다.

이것이 함정입니다. 여러분이 '속도'를 위해 읽는 조언들은 대부분 LCP와 First Contentful Paint(FCP)에 관한 조언입니다. 폰트를 프리로드(preload)하고, 이미지를 압축하며, 스크립트를 지연 실행하십시오. 모두 옳은 방법이고 실행할 가치가 있지만, INP에는 거의 쓸모가 없습니다. INP를 해결하려면 Chrome DevTools에서 실제 상호작용을 프로파일링(profile)하고, 긴 작업(long task)을 찾아 이를 분할하거나 크리티컬 경로(critical path) 밖으로 옮겨야 합니다.

저는 에셋 준비의 업스케일링(upscaling) 측면을 별도로 다루었으며, Magnific과 같은 도구들이 이미지 품질을 처리하지만, 이미지 변경이 INP를 변화시킨 적은 단 한 번도 없었습니다. 바이트(Bytes)는 LCP의 이야기입니다. 차단된 스레드의 밀리초(milliseconds)가 INP의 이야기이며, 어떤 스크립트가 차단을 일으키는지 추측할 수는 없습니다. 실제 휴대폰에서 RUM(Real User Monitoring)을 통해 측정해야 합니다. 실패한 두 가지 변경 사항 모두 이론적으로는 영리해 보였지만, 실제 운영 환경(production)에서는 평탄한 그래프를 나타냈습니다.

스스로를 속이지 않고 이를 측정하는 방법

저는 세 가지 데이터 소스만을 신뢰하며 그 외의 모든 것은 무시합니다. 첫 번째는 Search Console의 Core Web Vitals 보고서와 CrUX API를 통한 CrUX 필드 데이터(field data)입니다. 이는 실제 Chrome 트래픽이며, 28일 이동 평균(rolling window), 75번째 백분위수(75th percentile)를 기준으로 합니다. 이것이 Google이 실제로 사용하는 수치입니다. 단점은 28일의 지연(lag)이 있어, 오늘 배포한 변경 사항이 한 달 동안은 명확하게 나타나지 않는다는 점입니다.

두 번째는 저만의 RUM(Real User Monitoring) 스크립트입니다. type: 'event'durationThreshold: 40 설정을 포함한 PerformanceObserver API를 사용하는 약 40줄의 JavaScript로 구성되어 있습니다. 이 스크립트는 40ms를 초과하는 모든 상호작용(interaction)에 대해 해당 요소, 지속 시간(duration), 기기 클래스(device class)를 가벼운 엔드포인트(endpoint)에 기록합니다. 이를 통해 CrUX가 제공할 수 없는 당일 신호(same-day signal)를 얻을 수 있습니다. 제가 이벤트 핸들러(event handler)를 분리했을 때, RUM은 몇 시간 이내에 수치 하락을 보여주었습니다. CrUX는 4주 후에 이를 확인해 주었습니다.

세 번째는 CPU 스로틀링(throttling)을 4배 느리게 설정한 Chrome DevTools의 Performance 패널에서 실제 상호작용(장바구니 열기, 제품 추가, 변형(variant) 변경)을 기록하는 것입니다. 여기서 저는 롱 태스크(long task)를 찾아냅니다. 플레임 그래프(flame graph)는 정확히 어떤 함수가 스레드(thread)를 차단하는지, 그리고 얼마나 오래 차단하는지를 보여줍니다. 저의 세 가지 성공 사례는 모두 여기서 시작되었습니다. 장바구니 버튼을 클릭하고 180ms의 노란색 블록을 확인한 뒤, 이를 확장하여 전체 if-else 체인을 실행하는 글로벌 핸들러(global handler)를 찾아냈습니다.

제가 몇 달 동안 저지른 실수는 Lab 탭에 있는 Lighthouse의 INP 추정치를 신뢰한 것이었습니다. Lighthouse는 스로틀링이 적용되었지만 여전히 성능이 좋은 하드웨어에서 실행되며, 실제 상호작용 패턴을 재현하지 못합니다. 저의 Lab INP는 120ms로 나타났지만, 실제 사용자들은 380ms를 기록하고 있었습니다. Lab 수치는 저를 안주하게 만들었습니다.

실무적인 규칙을 하나 말씀드리자면, 만약 Lab 데이터와 Field 데이터가 2배 이상 차이 난다면, Field 데이터를 신뢰하고 Lab이 시뮬레이션하지 못하는 것이 무엇인지 찾아내야 합니다. 저의 경우, 그것은 열 부하(thermal load) 상태의 중급형 Android CPU였으며, 이는 어떤 합성 테스트(synthetic test)로도 재현할 수 없었습니다.

저는 간단한 스프레드시트를 유지하고 있습니다. 날짜, 배포된 변경 사항, RUM INP 이전, RUM INP 이후, CrUX 확인 날짜. 다섯 개의 열입니다. 이는 제가 증명할 수 없는 승리를 주장하는 것을 막아줍니다. "성능 최적화 (performance optimization)"의 절반은 코드에 대한 영리함이 아니라 측정에 대한 규율입니다. 생산적이라고 느껴지는 화려한 수정 사항은 종종 아무런 효과가 없지만, 거의 건너뛸 뻔했던 지루한 수정 사항이 100ms를 회복해 줍니다.

결론 (Bottom Line)

INP는 현재 대부분의 상점이 조용히 실패하고 있는 지표입니다. LCP가 녹색(양호)일지라도, 쇼핑객의 4분의 1은 휴대폰에서 장바구니가 응답하기를 380ms 동안 기다리고 있을 수 있습니다. 해결책은 이미지 압축이나 폰트 프리로딩 (font preloading)이 아닙니다. 상호작용 (interaction) 중에 메인 스레드 (main thread)를 차단하는 JavaScript를 찾아 이를 분할하거나, 페인트 (paint) 이후로 지연시키거나, 삭제하는 것입니다.

저의 세 가지 승리는 글로벌 이벤트 핸들러 (global event handler) 분할 (110ms), 장바구니 드로어 (cart drawer)의 지연 로딩 (lazy-loading) (80ms), 그리고 무거운 리뷰 위젯 (review widget) 교체 (50ms)였습니다. 저의 두 가지 실패는 분석 도구 (analytics) 지연 및 content-visibility 추가였습니다. 둘 다 생산적이라고 느껴졌습니다. 하지만 둘 다 INP에는 아무런 도움이 되지 않았는데, 왜냐하면 INP는 로딩 (loading) 문제가 아니라 스크립팅 (scripting) 문제이기 때문입니다.

실험실 점수 (lab scores)가 아닌 필드 데이터 (field data)와 작은 RUM 스크립트로 측정하세요. 만약 실험실 수치와 필드 수치가 2배 차이가 난다면, 필드 데이터를 신뢰하십시오.

이러한 실험을 기록하고 AI 어시스턴트와 함께 상점 변경 사항을 배포하는 데 사용하는 전체 시스템을 알고 싶다면, Claude Blueprint에서 프로파일링 (profiling)부터 배포 (deploy)까지 전체 워크플로우를 어떻게 구성하는지 설명합니다. 여러분 자신의 장바구니 버튼에 대한 DevTools 녹화본 하나로 시작해 보세요. 롱 태스크 (long task)는 보통 바로 거기에 있습니다.

이 기사에는 제휴 링크가 포함되어 있습니다. 이 링크를 통해 가입하시면, 귀하에게 추가 비용 부담 없이 저에게 소정의 수수료가 지급될 수 있습니다. (광고)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0