웹 페이지 INP(Interaction to Next Paint) 지표 개선 및 메인 스레드 블로킹 해소법 II
1. 소개 및 개요
1. 소개 및 개요
1.1 INP(Interaction to Next Paint)의 정의 및 핵심 메커니즘
웹 성능 측정 지표의 패러다임이 단순한 로딩 속도(LCP)에서 사용자 경험의 연속성(Responsiveness)으로 이동함에 따라, INP(Interaction to Next Paint)는 현대 웹 프론엔드 엔지니어링에서 가장 정밀하게 관리해야 할 핵심 지표로 자리 잡았습니다.
INP는 사용자가 페이지 내에서 발생하는 모든 상호작용(클릭, 탭, 키보드 입력 등)에 대해 브라우저가 얼마나 신속하게 시각적인 피드백을 제공하는지를 측정합니다. 기존의 FID(First Input Delay)가 페이지 로드 초기 단계의 첫 번째 상호작용만을 측정했던 것과 달리, INP는 페이지 전체 생명주기 동안 발생하는 모든 상호작용 중 가장 지연 시간이 긴(Longest Latency) 사례를 추적하여 측정합니다.
INP의 지연 시간(Latency)은 단순히 이벤트 핸들러의 실행 시간만을 의미하지 않으며, 다음의 세 가지 단계가 결합된 총합으로 계산됩니다:
- 입력 지연(Input Delay): 사용자의 입력이 발생한 시점부터 브라우저가 해당 이벤트를 처리하기 위해 이벤트 핸들로 제어권을 넘기기 전까지의 대기 시간입니다. 주로 메인 스레드가 이전의 'Long Task'를 처리하느라 바빠서 입력 큐(Input Queue)에 대기하는 시간입니다.
- 처리 시간(Processing Time): 이벤트 핸들러 내부에서 실행되는 JavaScript 로직의 실행 시간입니다. 데이터 가공, 상태 업데이트, DOM 구조 변경 등의 연산이 이 단계에 포함됩니다.
- 표시 지연(Presentation Delay): 이벤트 핸들러 실행이 완료된 후, 브라우저가 변경된 DOM을 기반으로 스타일을 계산(Recalculate Style)하고 레이아웃을 생성(Layout)하여 실제 화면에 픽셀을 그려내는(Paint) 과정에서 발생하는 지연 시간입니다.
1.2 INP 개선의 필요성: 사용자 경험과 비즈니스 임팩트
INP 수치가 높다는 것은 사용자가 버튼을 클릭하거나 메뉴를 열었을 때, 화면에 즉각적인 변화가 나타나지 않고 '프리징(Freezing)' 또는 '버벅임(Jank)'을 경험하고 있음을 의미합니다. 이는 다음과 같은 치명적인 결과를 초상합니다.
- 사용자 이탈률(Churn Rate) 증가: 시각적 피드백이 100ms~200ms를 초과하기 시작하면 사용자는 인터페이스가 응답하지 않는다고 인지하며, 이는 곧 서비스에 대한 신뢰도 하락과 직결됩니다.
- SEO 및 검색 엔진 순위 영향: Google은 Core Web Vitals의 핵심 지표로 INP를 채택했습니다. 따라서 높은 INP 수치는 검색 엔진 결과 페이지(SERP)에서의 노출 순위 하락을 초래할 수 있습니다.
- 인터랙션 신뢰도 저하: 복잡한 대시보드나 폼(Form) 중심의 애플리케이션에서 입력 지연은 사용자에게 불안정한 서비스라는 인상을 심어줍니다.
1.3 본 가이드의 목적 및 범위
본 가이드는 단순히 "코드를 최적화하라"는 추상적인 권고를 넘어, 메인 스레드의 차단(Main Thread Blocking)을 최소화하기 위한 구조적 접근법을 다룹니다.
특히, 기존에 해결하기 어려웠던 대규모 데이터 렌더링 상황에서의 Task 분할 전략(Task Splitting), Web Workers를 활용한 연산 오프로딩, 그리고 브라우저 렌더링 파이프라인 최적화를 통해 INP 수치를 실질적으로 낮출 수 있는 엔지니어링 방법론을 제시합니다.
이후 섹션에서는 각 지표별(Input Delay, Processing Time, Presentation Delay) 구체적인 병목 지점 진단법과 이를 해결하기 위한 실전 코드 패턴을 상세히 다룰 예정입니다.
2. 핵심 구현 및 설정 방법
2. 핵심 구현 및 설정 방법
INP(Interaction to Next Paint) 지표를 개선하기 위한 핵심은 사용자의 입력이 발생했을 때 메인 스레드가 점유되는 시간을 최소화하고, 브라우저가 다음 프레임을 즉시 렌더기(Renderer)에 전달할 수 있도록 '작업의 단위'를 재구성하는 것입니다. 본 섹션에서는 메인 스레드 블로킹을 해소하기 위한 구체적인 구현 전략과 코드 패턴을 다룹니다.
2.1. 긴 작업(Long Task)의 분할: scheduler.yield() 및 비동기 패턴 활용
INP 저하의 가장 큰 원인은 50ms를 초과하는 'Long Task'입니다. 하나의 함수가 메인 스레드를 독점하면 브라우저는 사용자의 다음 입력을 처리할 수 없습니다. 이를 해결하기 위해 거대한 작업을 작은 단위로 쪼개어 브라우저에 '틈(Yield)'을 주어야 합니다.
1) scheduler.yield()를 이용한 작업 중단점 생성
최신 브라우저에서 지원되는 Prioritized Task Scheduling API를 사용하면, 실행 중인 자바스크립트 함수 중간에 제어권을 브라우저에게 양보하여 렌더링 작업을 수행할 수 있도록 유도할 수 있습니다.// 개선 전: 단일 함수가 메인 스레드를 장시간 점유
async function processLargeData(items) {
const result = [];
for (const item of items) {
// 복잡한 연산이 수행됨 (Long Task 발생)
const processed = heavyComputation(item);
result.push(processed);
}
return result;
}
// 개선 후: scheduler.yield()를 통한 작업 분할
async function processLargeDataOptimized(items) {
const result = [];
for (let i = 0; i < items.length; i++) {
const processed = heavyComputation(items[i]);
result.push(processed);
// 100개 단위로 작업을 끊고 브라우저에 렌더링 기회를 부여
if (i % 100 === 0 && 'scheduler' in window) {
await scheduler.yield();
}
}
return result;
}
2) setTimeout 및 requestIdleCallback을 이용한 후순위 작업 이월
scheduler.yield()를 사용할 수 없는 환경이라면, 중요도가 낮은 로직을 setTimeout(callback, 0)이나 requestIdleCallback을 통해 다음 이벤트 루프 턴으로 미루어 입력 처리(Input Latency)를 우선순위에 둡니다.2.2. Web Worker를 활용한 연산 오프로딩(Offloading)
UI 업데이트와 직접적인 관련이 없는 데이터 파싱, 정렬, 복잡한 수학적 계산은 메인 스레드가 아닌 별도의 워커 스레드에서 처리해야 합니다.
구현 단계:
- Worker 스크립트 분리: 연산 로직만 담긴 별도의
.js파일을 생성합니다. - Message Passing 구성:
postMessage를 통해 데이터를 전달하고,onmessage로 결과를 수신합니다.
// worker.js (Worker Thread)
self.onmessage = function(e) {
const heavyData = e.data;
// 복잡한 연산 수행
const result = performComplexCalculation(heavyData);
self.postMessage(result);
};
// main.js (Main Thread)
const worker = new Worker('worker.js');
function handleDataProcessing(data) {
worker.postMessage(data); // 데이터 전달
worker.onmessage = function(e) {
// 연산 완료 후 UI 업데이트 (Main Thread에서 수행)
updateUI(e.data);
};
}
2.3. 레이아웃 스래싱(Layout Thrashing) 방지 및 DOM 접근 최적화
DOM의 값을 읽는 작업(Read)과 쓰는 작업(Write)이 교차로 발생하면 브래우저는 매번 레이아웃을 재계산해야 하며, 이는 심각한 프레임 드랍을 유발합니다.
[나쁜 사례: Read/Write 교차]
// 루프 내부에서 읽기와 쓰기가 반복되어 레이아웃 스래싱 발생
elements.forEach(el => {
const width = el.offsetWidth; // Read (Layout Trigger)
el.style.height = `${width * 2}px`; // Write (Invalidate Layout)
});
[좋은 사례: Batching (일괄 처리)]
// 1. 모든 값을 먼저 읽기 (Read Phase)
const widths = elements.map(el => el.offsetWidth);
// 2. 모든 값을 나중에 한꺼번에 적용 (Write Phase)
requestAnimationFrame(() => {
elements.forEach((el, index) => {
el.style.height = `${widths[index] * 2}px`;
});
});
2.4. 요약 및 체크리스트
| 구분 | 최적화 전략 | 기대 효과 |
|---|---|---|
| Task 분할 | scheduler.yield() 또는 setTimeout 활용 |
메인 스레드 점유 시간 단축 및 입력 반응성 향상 |
| 병렬 처리 | Web Workers 도입 | 무거운 연산이 UI 렌더링을 방해하는 현상 방지 |
| DOM 관리 | Read/Write 작업의 일괄 처리 (Batching) | 레이아웃 재계산(Reflow) 횟수 최소화 |
| 이벤트 제어 | Debounce / Throttle 적용 | 빈번한 이벤트(Scroll, Resize)로 인한 오버헤드 감소 |
3. 모니터링 및 트러블슈팅
3. 모니터링 및 트러블슈팅
INP(Interaction to Next Paint) 개선은 단발성 작업이 아닌 지속적인 최적화 프로세스입니다. 지표가 개선되었는지 확인하고, 새로운 기능 추가 시 발생하는 성능 저하를 방지하기 위해서는 정교한 모니터 만과 체계적인 트러블슈팅 체계가 필수적입니다.
3.1. RUM(Real User Monitoring) 기반의 실시간 지표 수집
실제 사용자가 경험하는 INP는 개발 환경(Lab Data)과 차이가 클 수 있습니다. 따라서 반드시 실제 운영 환경의 데이터를 수집하는 RUM 전략을 구축해야 합니다.
web-vitals 라이브 library를 활용하여 사용자 환경의 인터랙션 지표를 수집하고, 이를 분석 플랫폼(Sentry, Datadog, Google Analytics 등)으로 전송하는 구조를 권장합니다.[실무 적용 코드 예시: web-vitals 지표 수집 및 로그 전송]
import { onINP } from 'web-vitals';
// 지표 데이터를 분석 서버 또는 로깅 서비스로 전송하는 함수
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value, // INP 값 (ms)
entries: metric.entries,
attribution: metric.attribution, // 어떤 요소에서 발생했는지에 대한 컨텍text
url: window.location.href,
timestamp: new Date().toISOString(),
});
// Beacon API를 사용하여 페이지 종료 시에도 데이터 유실 없이 전송
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/v1/analytics/web-vitals', body);
} else {
fetch('/api/v1/analytics/web-vitals', { method: 'POST', body, keepalive: true });
}
}
// INP 지표 모니터링 설정
onINP(sendToAnalytics);
모니터링 핵심 체크포인트:
- Attribution 분석:
metric.attribution을 통해 지표 상승을 유발한 구체적인 DOM 요소(Element)와 해당 요소의 속성을 추적하십시오. - Threshold Alerting: INP 값이 200ms(Good)를 초과하여 500ms(Needs Improvement)에 도달할 경우, 개발 팀에 즉시 알림(Slack/PagerDuty)이 가도록 임계값을 설정하십시오.
- P90/P99 지표 관리: 평균값(Average)에 매몰되지 말고, 상위 10%(P90) 및 상위 1%(P99) 사용자의 경험을 기준으로 성능을 평가하십시오.
3.2. Chrome DevTools를 활용한 심층 병목 분석
RUM을 통해 특정 페이지나 인터랙션에서 INP 상승이 감지되었다면, 개발자 도구를 사용하여 메인 스레드의 병목 지점을 정밀 타격해야 합니다.
- Performance Tab 활용:
- Rendering 탭 활용:
3.3. 상황별 트러블슈팅 가이드
| 문제 상황 | 주요 원인 | 해결 전략 |
|---|---|---|
| JS 실행 시간이 긴 경우 (Long Task) | 복잡한 데이터 연산, 거대한 JSON 파싱, 과도한 이벤트 리스너 | Web Worker를 통한 연산 분리, requestIdleCallback을 활용한 낮은 우선순위 작업 처리, 코드 스플리팅 |
| Layout/Reflow 발생 | 인터랙션 직후 DOM 구조 변경, 스타일 속성(width, height 등) 변경 | transform, opacity 등 Composite 전용 속성 사용, contain: layout 속성 적용, 레이아웃 계산 최소화 |
| 이벤트 핸들러 지연 | 과도한 이벤트 리스너 등록, 동기식 API 호출 | Debounce 또는 Throttle 적용, 비동기(Async/Await) 패턴으로 흐름 제어, passive: true 옵션 활용 |
| Third-party Script 간섭 | 광고, 분석 스크립트, 위젯 등의 메인 스레드 점유 | defer 또는 async 속성 사용, Partytown과 같은 라이브러리를 이용한 Web Worker로의 스크립트 격리 |
결론적으로, INP 최적화는 단순히 코드를 줄이는 것이 아니라, "사용자의 입력에 대한 응답성을 메인 스레드에서 어떻게 보장할 것인가"에 초점을 맞추어야 합니다. 주기적인 RUM 데이터 모니터링과 정기적인 성능 회귀 테스트(Regression Test)를 통해 최적화된 상태를 유지하십시오.
결론
본 가이드에 기술된 단계별 설정을 참고하여 시스템 최적화를 완료하시기 바랍니다. 추가로 생기는 예외 상황이나 질문은 댓글로 남겨주시면 성심껏 조치해 드리겠습니다.
댓글 0
가장 먼저 유용한 의견을 남겨보세요!
전문적인 지식 교류에 참여하시려면 HOWTODOIT 회원이 되어주세요.
로그인 후 참여하기