← 목록으로 돌아가기

INP 최적화 실전 가이드: React 렌더링·Long Task·Web Worker로 입력 지연 줄이기

Performance

Frontend INP Optimization Long Tasks React

클릭했는데 화면이 늦게 반응하는 이유

웹 성능을 이야기할 때 오랫동안 LCP와 CLS가 중심이었습니다. 첫 화면이 빨리 뜨고 레이아웃이 흔들리지 않는 것은 여전히 중요합니다. 하지만 사용자가 실제로 서비스를 "빠르다"고 느끼는 순간은 버튼을 눌렀을 때 화면이 즉시 반응하는가에 달려 있습니다. Interaction to Next Paint, 즉 INP는 이 상호작용 지연을 측정하는 지표입니다.

INP가 나쁜 페이지는 로딩이 끝난 뒤에도 답답합니다. 사용자가 필터를 클릭했는데 선택 표시가 늦게 바뀌고, 검색어를 입력했는데 글자가 밀려 나오고, 장바구니 버튼을 눌렀는데 아무 반응이 없다가 한참 뒤에 수량이 바뀝니다. 서버 API가 빠르고 번들 크기가 작아도, 메인 스레드가 긴 작업으로 막혀 있으면 브라우저는 다음 paint를 하지 못합니다.

이 글에서는 React 애플리케이션에서 INP를 악화시키는 long task, 불필요한 렌더링, 큰 리스트 처리, 동기 계산, 이벤트 핸들러 설계를 실무적으로 다룹니다. 단순한 체크리스트가 아니라 "입력 이벤트가 들어온 뒤 다음 화면이 그려지기까지" 어떤 일이 벌어지는지를 기준으로 최적화합니다.


1. INP는 입력부터 다음 Paint까지 본다

INP는 사용자의 상호작용 지연을 대표하는 지표입니다. 클릭, 탭, 키 입력 같은 interaction이 발생한 뒤 브라우저가 다음 화면을 그릴 때까지의 시간을 봅니다. 이 시간에는 input delay, event handler 실행, React state update, render, commit, layout, paint가 모두 포함될 수 있습니다. 즉 "핸들러 함수가 빠르다"만으로 충분하지 않습니다.

브라우저는 메인 스레드에서 많은 일을 합니다. JavaScript 실행, style 계산, layout, paint 준비가 모두 같은 줄에 서 있습니다. 긴 JavaScript 작업이 실행 중이면 사용자의 클릭 이벤트는 큐에서 기다립니다. 이벤트 핸들러가 끝난 뒤에도 React 렌더링과 layout이 오래 걸리면 다음 paint가 늦어집니다.

INP 최적화의 첫 번째 원칙은 사용자 입력 직후 해야 할 일과 나중에 해도 되는 일을 분리하는 것입니다. 클릭 표시, 토글 상태, 입력값 반영처럼 즉시 보여야 하는 작업은 작게 유지합니다. 분석 이벤트 전송, 대량 계산, 필터링, 비싼 상태 동기화는 뒤로 미루거나 나눕니다. 사용자는 전체 작업 완료보다 "내 입력이 먹혔다"는 신호를 먼저 원합니다.


2. Long Task를 쪼갠다

50ms 이상 메인 스레드를 점유하는 작업은 long task로 분류됩니다. 200ms짜리 JSON 파싱, 300ms짜리 리스트 필터링, 500ms짜리 차트 계산이 클릭 이벤트 직후 실행되면 INP는 바로 나빠집니다. 문제는 이런 작업이 코드상으로는 작아 보인다는 점입니다. 배열 map, filter, sort 몇 줄이 데이터 크기가 커지면서 long task가 됩니다.

작업을 쪼개는 가장 단순한 방법은 chunking입니다. 한 번에 50,000개를 처리하지 않고 1,000개씩 나눠 처리하면서 중간에 브라우저에 제어권을 돌려줍니다. setTimeout(0), scheduler.postTask, requestIdleCallback 같은 도구를 상황에 맞게 사용할 수 있습니다. 중요한 것은 브라우저가 입력과 paint를 처리할 틈을 주는 것입니다.

하지만 모든 작업을 idle로 미루면 결과가 너무 늦게 나올 수 있습니다. 우선순위를 나눠야 합니다. 현재 화면에 보이는 30개 항목은 즉시 계산하고, 나머지 항목은 background에서 계산합니다. 사용자가 입력 중인 검색어에 대해서는 이전 작업을 취소하고 최신 입력만 처리합니다. 오래 걸리는 작업은 빨리 끝내는 것보다 중단 가능하게 만드는 것이 더 중요할 때가 많습니다.


3. React 렌더링을 줄이는 법

React에서 INP를 악화시키는 흔한 원인은 입력 하나가 너무 큰 컴포넌트 트리를 다시 렌더링하는 것입니다. 검색창에 글자를 입력할 때 페이지 전체 상태가 바뀌고, 수백 개 카드와 차트가 함께 렌더링되면 키 입력이 밀립니다. State를 어디에 둘지와 컴포넌트 경계가 성능에 직접 영향을 줍니다.

첫 번째 원칙은 state를 사용하는 곳 가까이에 두는 것입니다. 전역 store에 모든 UI 상태를 넣으면 작은 입력도 넓은 구독 범위를 깨웁니다. 검색 input의 draft value는 검색 컴포넌트 안에 두고, 확정된 쿼리만 상위로 올리는 식의 분리가 필요합니다. Zustand나 Redux를 쓴다면 selector와 shallow compare로 구독 범위를 좁혀야 합니다.

두 번째는 expensive child를 분리하는 것입니다. 입력창과 결과 리스트가 같은 컴포넌트에 있으면 입력마다 리스트 렌더링이 따라올 수 있습니다. 리스트는 memoization, virtualization, deferred value를 활용해 입력 반응과 결과 계산을 분리합니다. React의 useDeferredValuestartTransition은 urgent update와 non-urgent update를 나누는 데 유용합니다.

세 번째는 memo를 남용하지 않는 것입니다. React.memo, useMemo, useCallback은 비용을 없애는 마법이 아닙니다. 비교 비용과 메모리 비용이 있습니다. 렌더링이 실제로 비싼 컴포넌트, props 안정성이 중요한 경계, 큰 파생 데이터 계산에 집중해서 써야 합니다. Profiler 없이 모든 곳에 memo를 붙이면 코드만 복잡해집니다.


4. 큰 리스트는 Virtualization이 기본이다

수천 개 DOM 노드를 한 화면에 렌더링하면 interaction은 느려질 수밖에 없습니다. DOM 수가 많으면 React commit뿐 아니라 style 계산과 layout 비용도 커집니다. 리스트, 테이블, 로그 뷰어, 검색 결과처럼 항목 수가 많은 UI는 virtualization을 기본 선택지로 봐야 합니다.

Virtualization은 화면에 보이는 항목과 약간의 buffer만 렌더링합니다. 사용자가 스크롤하면 필요한 항목만 교체합니다. 이 방식은 초기 렌더링과 업데이트 비용을 크게 줄입니다. 다만 동적 높이, sticky header, keyboard navigation, 접근성 처리를 함께 고려해야 합니다. 성능만 보고 무리하게 적용하면 UX가 깨질 수 있습니다.

필터링과 정렬도 주의해야 합니다. 사용자가 체크박스를 누를 때마다 10,000개 항목을 동기 정렬하면 INP가 나빠집니다. 서버 사이드 필터링, 인덱싱된 클라이언트 데이터 구조, web worker 계산, debounce를 조합해야 합니다. 단, debounce는 입력 반응 자체를 늦추는 방식으로 쓰면 안 됩니다. 입력 UI는 즉시 업데이트하고, 비싼 결과 계산만 debounce해야 합니다.


5. Web Worker로 메인 스레드를 비운다

CPU가 많이 드는 작업은 web worker로 옮길 수 있습니다. 대용량 CSV 파싱, 이미지 처리, 암호화, 검색 인덱스 계산, 차트 데이터 집계는 worker 후보입니다. Worker는 별도 스레드에서 실행되므로 메인 스레드의 입력 처리와 paint를 막지 않습니다.

Worker를 쓸 때는 데이터 전달 비용을 고려해야 합니다. 큰 객체를 postMessage로 복사하면 그 자체가 비용입니다. Transferable object를 사용하거나, 필요한 데이터만 압축해 전달해야 합니다. Worker 초기화 비용도 있습니다. 사용자가 버튼을 누른 뒤 worker 번들을 처음 다운로드하면 오히려 느릴 수 있습니다. 중요한 작업은 idle 시간에 worker를 미리 준비하거나 route 단위로 prefetch합니다.

Worker 결과를 React 상태로 반영할 때도 한 번에 거대한 업데이트를 하지 않습니다. Worker가 계산을 빠르게 끝내도, 결과 50,000개를 메인 스레드에서 렌더링하면 다시 막힙니다. Worker는 계산을 분리하는 도구이지 렌더링 비용을 없애지는 않습니다. 결과 적용 단계도 pagination, virtualization, transition으로 나눠야 합니다.


6. 측정: 실사용자 기준으로 본다

개발자 노트북에서 성능이 좋아도 저가형 모바일에서는 INP가 나쁠 수 있습니다. CPU 성능, 메모리, 백그라운드 앱, 배터리 상태가 모두 영향을 줍니다. Lighthouse 한 번으로 판단하지 말고 RUM(Real User Monitoring) 데이터를 봐야 합니다. Interaction target, route, device class, browser별로 INP를 나누면 문제 지점이 보입니다.

Chrome Performance panel은 원인 분석에 좋습니다. 느린 interaction을 녹화하고, input event 이후 어떤 long task가 있었는지 봅니다. React Profiler는 어떤 컴포넌트가 렌더링됐는지 보여줍니다. 둘을 함께 봐야 합니다. 브라우저 timeline은 "메인 스레드가 무엇을 했는가"를 보여주고, React Profiler는 "왜 이 컴포넌트가 렌더링됐는가"를 보여줍니다.

운영 지표에는 p75 INP뿐 아니라 worst interaction 후보를 남기는 것이 좋습니다. 어떤 버튼, 어떤 route, 어떤 컴포넌트 주변에서 느린지 알아야 수정할 수 있습니다. 단, 사용자 입력값이나 개인정보를 interaction name에 넣으면 안 됩니다. 낮은 카디널리티의 route와 element role 수준으로 충분합니다.


7. 배포 전략: 성능 회귀를 릴리즈 게이트로 막는다

INP 개선은 한 번 고치고 끝나는 작업이 아닙니다. 기능이 추가되고 컴포넌트가 커지고 데이터가 늘어나면 다시 나빠집니다. 그래서 성능 회귀를 배포 파이프라인에서 잡는 장치가 필요합니다. 핵심 화면에 대해 synthetic interaction을 만들고, PR마다 주요 클릭과 입력 시나리오의 long task 수, hydration 이후 scripting time, interaction latency를 비교합니다. 절대값은 CI 환경에 따라 흔들릴 수 있으므로 기준 브랜치 대비 증가율을 함께 봅니다.

Bundle 분석도 INP와 연결해서 봐야 합니다. 큰 번들은 초기 로딩뿐 아니라 hydration과 route 전환 후 scripting 비용을 키웁니다. 특히 관리자 페이지에서 쓰는 무거운 차트 라이브러리나 에디터가 일반 사용자 메인 route 번들에 섞이면, 사용하지 않는 기능 때문에 입력 반응이 느려질 수 있습니다. Route 단위 code splitting, interaction 직전 prefetch, lazy import 기준을 정해두면 회귀를 줄일 수 있습니다.

성능 예산은 팀이 합의해야 합니다. 예를 들어 "모바일 p75 INP 200ms 이하", "주요 route의 단일 long task 100ms 초과 금지", "검색 입력 중 동기 필터링 20ms 이하"처럼 판단 가능한 기준이 필요합니다. 기준이 없으면 성능 개선은 항상 기능 개발 뒤로 밀립니다. 반대로 예산이 있으면 새로운 기능을 만들 때도 "이 계산은 worker로 빼야 한다", "이 리스트는 처음부터 virtualization을 써야 한다"는 설계 대화가 가능해집니다.


실무 체크리스트

  • 입력 직후 urgent update와 나중에 해도 되는 non-urgent update가 분리되어 있는가
  • 50ms 이상 long task를 Performance panel에서 확인하고 쪼갰는가
  • 검색, 필터, 정렬 같은 비싼 계산이 입력 핸들러 안에서 동기 실행되지 않는가
  • 큰 리스트와 테이블에 virtualization을 적용했는가
  • 전역 상태 변경이 불필요하게 넓은 컴포넌트 트리를 렌더링하지 않는가
  • startTransition, useDeferredValue를 사용자 입력 반응과 결과 렌더링 분리에 활용했는가
  • CPU heavy 작업을 web worker로 옮기고, worker 초기화와 데이터 전달 비용을 측정했는가
  • RUM에서 route, device class, interaction target별 INP를 보고 있는가

결론: 빠른 화면보다 즉각 반응하는 화면이 기억된다

INP 최적화는 사용자가 서비스를 만지는 순간을 다룹니다. 첫 화면이 아무리 빨라도 클릭과 입력이 늦으면 사용자는 느리다고 느낍니다. React 앱에서는 작은 상태 변경이 큰 렌더링으로 번지고, 짧아 보이는 배열 처리가 데이터 증가와 함께 long task가 되며, 메인 스레드 하나에 너무 많은 일을 맡기기 쉽습니다.

해결 방향은 명확합니다. 입력 직후 할 일을 줄이고, 큰 작업을 나누고, 렌더링 범위를 좁히고, CPU heavy 계산을 worker로 옮기고, 실제 사용자 데이터를 기준으로 다시 측정합니다. INP는 프론트엔드 성능을 사용자 손끝에 가깝게 끌어오는 지표입니다. 그만큼 개선 효과도 사용자가 바로 느낍니다.