[Deep Dive] INP(Interaction to Next Paint) 최적화: 메인 스레드 병목 현상 해결 전략



웹 성능의 핵심 지표가 FID(First Input Delay)에서 INP(Interaction to Next Paint)로 전환됨에 따라, 사용자가 페이지와 상호작용했을 때 얼마나 빠르게 시각적 피드_백을 제공하느냐가 서비스의 품질을 결정짓는 중요한 요소가 되었습니다.

INP 점수가 높다는 것은 사용자의 클릭, 탭, 키보드 입력에 대해 브라우저가 즉각적으로 반응하지 못하고 '버벅임'을 느끼고 있다는 신호입니다. 이러한 현상의 근본 원인은 대부분 메인 스레드의 점유(Main Thread Blocking)에 있습니다. 본 가이드에서는 INP를 개선하기 위한 세 가지 핵심 전략과 모니터링 방법을 심도 있게 다룹니다.




1. 전략 1: Long Task 분할 (Yielding to the Main Thread)



브라우저의 메인 스레드는 자바스크립트 실행, 스타일 계산, 레이아웃, 페인팅을 모두 처리합니다. 만약 하나의 자바스크립트 함수가 50ms 이상 실행된다면, 이는 'Long Task'로 간주되며 이 기간 동안 사용자의 입력은 큐(Queue)에서 대기하게 되어 INP 점수를 악화시프니니다.

💡 핵심 개념: Yielding (양보)

연속적인 작업을 하나의 거대한 함수로 실행하는 대신, 중간중간 브라우저에 제어권을 양보하여 입력 이벤트를 처리할 틈을 만들어야 합니다.

🛠️ 구현 방법



A. scheduler.yield() 또는 setTimeout 활용

가장 효과적인 방법은 작업을 작은 단위로 쪼개어 브라우저가 중간에 렌더링 작업을 수행할 수 있도록 하는 것입니다.

// ❌ 나쁜 예: 거대한 루프가 메인 스레드를 점유
function heavyTask() {
  for (let i = 0; i < 100000000; i++) {
    // 복잡한 계산 작업...
  }
  console.log("작업 완료");
}

// ✅ 좋은 예: 작업을 쪼개어 브레이크 타임을 제공
async function optimizedTask() {
  const chunk = 1000000;
  for (let i = 0; i < 100000000; i += chunk) {
    // 복잡한 계산 작업 수행
    doWork(i, i + chunk);

    // 브라우저에게 렌더링 및 이벤트 처리 시간을 할당 (마이크로태스크 큐 활용)
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  console.log("작업 완료");
}


B. Web Workers 활용

계산 집약적인 작업(예: 데이터 파싱, 이미지 처리)은 메인 스레드가 아닌 별도의 Web Worker 스레드에서 실행하여 메인 스레드의 UI 반응성을 완전히 보장해야 합니다.




2. 전략 2: Layout Thrashing 방지 (Reflow 최적화)



자바스크립트로 DOM을 조작할 때, '읽기(Read)''쓰기(Write)' 작업이 불규칙하게 섞이면 브라우저는 매번 레이아웃을 다시 계산해야 하는 'Layout Thrashing' 상태에 빠집니다.

❌ 나쁜 예: Read와 Write의 반복

const elements = document.querySelectorAll('.item');

elements.forEach(el => {
  // 1. Read: 요소의 너비를 읽음 (Layout 발생 가능성)
  const width = el.offsetWidth; 
  
  // 2. Write: 너비를 변경 (Layout 발생)
  el.style.width = `${width + 10}px`; 
  
  // 다음 루프에서 다시 Read를 수행할 때, 브래우저는 이전 Write로 인한 
  // 변경사항을 반영하기 위해 강제로 레이아웃을 재계산(Forced Reflow)해야 함
});


✅ 좋은 예: Batching (일괄 처리)

모든 읽기 작업을 먼저 수행한 뒤, 모든 쓰기 작업을 나중에 수행하도록 구조를 변경합니다.

const elements = document.querySelectorAll('.item');
const updates = [];

// 1. 모든 Read 작업을 먼저 수행 (Batch Read)
elements.forEach(el => {
  updates.push({ el, width: el.offsetWidth });
});

// 2. 모든 Write 작업을 나중에 수행 (Batch Write)
updates.forEach(({ el, width }) => {
  el.style.width = `${width + 10}px`;
});





3. 전략 3: 이벤트 핸들러 최적화 (Debounce & Throttle)



스크롤, 리사이즈, 마우스 이동과 같은 고빈도 이벤트는 짧은 시간 동안 수백 번 호출될 수 있습니다. 이를 적절히 제어하지 않으면 메인 스레드에 과부하가 걸립니다.

  • Debounce: 이벤트가 연속해서 발생할 때, 마지막 이벤트가 끝나고 일정 시간이 지난 후에만 함수를 실행합니다. (예: 검색창 자동완성)
  • Throttle: 이벤트가 일정 주기마다 한 번씩만 실행되도록 제한합니다. (예: 무한 스크림, 스크롤 애니메이션)


// Throttle 예시: 스크롤 이벤트 최적화
function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }
}

window.addEventListener('scroll', throttle(() => {
  console.log('스롤 이벤트 처리 중 (최대 100ms에 한 번)');
}, 100));





4. 종합 모니터링 및 진단 방법



최적화가 잘 이루어지고 있는지 확인하기 위해서는 정량적인 데이터가 필요합니다.

  1. Chrome DevTools - Performance Tab:
* Record를 눌러 사용자 상호작용(클릭 등)을 수행합니다. * 'Long Task' 표시(빨간색 삼각형)를 찾아 어떤 함수가 메인 스레드를 점유했는지 확인합니다. * 'Layout Thrashing' 경고가 뜨는지 확인합니다.
  1. Web Vitals Library:
* 사용자 환경에서 실제 Interaction to Next Paint (INP) 지표를 수집하여 분석합니다.
  1. Lighthouse:
* 정기적인 오딧(Audit)을 통해 성능 점수와 개선 권장 사항을 확인합니다.

결론적으로, INP를 개선하기 위해서는 메인 스레드를 점유하는 '긴 작업(Long Task)'을 분할하고, 브라우저가 화면을 다시 그릴(Paint) 수 있는 여유 시간을 확보하는 것이 핵심입니다.