dataLayer 이벤트 스케줄링으로 INP 최적화하기

GTM 이벤트를 브라우저가 페인트한 뒤로 미뤄 INP 점수를 개선하는 방법

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-09-28

TL;DR: dataLayer.push() 지연으로 INP 개선하기

이벤트 핸들러 내부에서 dataLayer.push()가 동기적으로 실행되면, 브라우저가 시각적 피드백을 페인트하기 전에 GTM이 태그를 먼저 처리합니다. 이 과정에서 main thread가 차단되고 Interaction to Next Paint (INP) 수치가 치솟습니다. 해결책: requestAnimationFrame을 사용해 브라우저가 다음 프레임을 페인트한 뒤 dataLayer.push()가 실행되도록 예약하세요. 이 패턴을 적용하면 대부분의 사이트에서 INP를 20~100ms 줄일 수 있으며, 기준 미달이던 점수가 통과 수준으로 개선되기도 합니다. 데이터 수집이 50~250ms 지연되지만, 애널리틱스나 마케팅 트래킹에는 아무런 지장이 없습니다.

dataLayer.push()가 INP를 악화시키는 이유

한 고객사 프로젝트에서는 사용자 인터랙션 이후 dataLayer.push()가 실행되는 시점을 재조정해 Interaction to Next Paint (INP)를 100ms 단축했습니다. 해결책은 트래킹보다 렌더링을 우선 처리하는 JavaScript 드롭인 대체 코드입니다.

최종 검토: Arjen Karel (2026년 3월)

동기식 dataLayer.push()의 문제점

Google Tag Manager를 다뤄봤다면 dataLayer.push()는 익숙할 것입니다. Data Layer로 이벤트를 전송하는 표준 방식이며, 대부분의 사이트 기능에 깊숙이 연동되어 있습니다. 문제는 이 코드가 언제 실행되는지에 대해 아무도 의문을 품지 않는다는 점입니다.

이벤트 핸들러(버튼 클릭, 폼 제출, 메뉴 토글 등) 내부에서 dataLayer.push()를 직접 호출하면 동기적으로 실행됩니다. 해당 이벤트에 반응하도록 설정된 GTM 태그 역시 즉시 실행되어, 브라우저가 레이아웃을 갱신하기 전에 main thread를 차단합니다. 방문자가 버튼을 클릭해도 GTM이 백그라운드에서 트래킹 스크립트를 처리하는 동안 화면에는 아무런 변화가 없습니다. 이 지연 시간이 바로 presentation delay이며, INP 점수를 갉아먹는 주원인입니다.

아래 성능 트레이스는 주요 뉴스 웹사이트에서 사용자 인터랙션 이후 발생한 GTM 동작을 보여줍니다. GTM 작업을 처리하는 데 약 90ms가 걸려 전체 INP가 263ms까지 늘어났습니다. 이 인터랙션은 Core Web Vitals 기준을 통과하지 못합니다.

datalayer push inp

해결책: 먼저 페인트하고 그다음에 push하기

이 해결책은 web.dev의 INP 최적화 가이드와 일치합니다. 중요하지 않은 코드를 실행하기 전에 브라우저가 먼저 페인트할 수 있도록 main thread에 yield하는 방식입니다. 이벤트 핸들러 내부에서 트래킹을 동기 실행하는 대신 다음과 같이 처리합니다.

  1. 시각적 업데이트 코드를 즉시 실행합니다.
  2. 브라우저가 먼저 페인트하게 합니다.
  3. 그다음에 dataLayer로 push합니다.

동일한 인터랙션에 이 패턴을 적용한 결과입니다. 달라진 점은 브라우저가 다음 프레임을 페인트한 뒤 dataLayer.push()가 실행된다는 것뿐입니다.

datalayer push inp yeilded

이전에는 기준 미달이던 인터랙션이 이제 여유 있게 기준을 통과합니다. 모든 데이터는 여전히 dataLayer에 정상 전달됩니다. 차이점은 브라우저가 사용자의 동작에 먼저 반응한 뒤에야 GTM 스크립트가 실행된다는 점입니다.

awaitPaint 헬퍼 함수

이 헬퍼 함수는 requestAnimationFrame을 사용해 브라우저가 페인트를 마친 뒤 콜백이 실행되도록 예약합니다. 중첩된 setTimeout은 함수가 페인트 직전이 아니라 반드시 페인트 완료 후에 실행되도록 보장합니다. 이는 인터랙션 이후 중요하지 않은 작업을 뒤로 미룰 때 web.dev에서 권장하는 패턴입니다.

async function awaitPaint(fn) {
    await new Promise((resolve) => {
        // Fallback: ensures we don't hang if RAF never fires (e.g. background tabs)
        setTimeout(resolve, 200);

        // Schedule after the next paint
        requestAnimationFrame(() => {
            setTimeout(resolve, 50);
        });
    });

    if (typeof fn === 'function') {
        fn();
    }
}

헬퍼 함수 사용법

dataLayer.push() 호출을 awaitPaint로 감싸 페인트 이후로 실행을 미루세요.

function pushToDataLayer(event, data) {
    window.dataLayer = window.dataLayer || [];

    awaitPaint(() => {
        window.dataLayer.push({
            event,
            ...data,
            timestamp: new Date().toISOString()
        });
    });
}

// Usage
document.querySelector('.buy-button').addEventListener('click', () => {
    // Visual feedback happens immediately
    showLoadingSpinner();

    // Tracking fires after paint
    pushToDataLayer('purchase_click', { productId: '123' });
});

빠른 테스트를 위한 전역 오버라이드

모든 dataLayer.push() 호출을 일일이 리팩터링하지 않고 사이트 전체에서 이 패턴을 테스트하려면, push 함수를 전역에서 오버라이드하면 됩니다. 이 스크립트를 GTM 컨테이너 스니펫 바로 뒤인 <head>에 넣으세요.

<script type="module">
    window.dataLayer = window.dataLayer || [];

    async function awaitPaint(fn) {
        await new Promise((resolve) => {
            // Fallback: ensures we don't hang if RAF never fires (e.g. background tabs)
            setTimeout(resolve, 200);

            // Schedule after the next paint
            requestAnimationFrame(() => {
                setTimeout(resolve, 50);
            });
        });

        if (typeof fn === 'function') {
            fn();
        }
    }

    if (window.dataLayer && typeof window.dataLayer.push === 'function') {
        const originalPush = window.dataLayer.push.bind(window.dataLayer);

        window.dataLayer.push = function (...args) {
            awaitPaint(() => {
                originalPush(...args);
            });
        };
    }
</script>

이렇게 하면 페이지의 모든 dataLayer.push()가 오버라이드됩니다. 기존 GTM 이벤트 트리거, 하드코딩된 트래킹 호출, 플러그인 연동 모두 별도의 코드 수정 없이 자동으로 성능 개선 효과를 얻습니다.

INP가 개선되는 이유

INP는 사용자 인터랙션 발생 시점부터 브라우저가 시각적 반응을 페인트할 때까지 걸리는 시간을 측정합니다. 이벤트 핸들러 내부에서 GTM 이벤트 처리를 동기적으로 실행하면 main thread가 차단되어 렌더링이 가로막힙니다. 2024 Web Almanac에 따르면 presentation delay는 중앙값 기준 INP 악화의 가장 큰 요인이며, 트래킹 스크립트가 그 주된 원인 중 하나로 지목되었습니다.

수치가 이를 증명합니다. 2025 Web Almanac에 따르면 모바일 페이지 전체의 INP 통과율은 77%인 반면, 방문자 수 상위 1,000개 사이트의 통과율은 63%에 불과합니다. 상위권 사이트일수록 서드파티 스크립트를 더 많이 로드하기 때문입니다. 이탈리아 최대 중고거래 플랫폼 Subito에서는 GTM으로 로드되던 TikTok 트래킹 스크립트 단 하나를 비활성화한 것만으로 INP가 208ms에서 약 170ms로 줄었습니다. 스크립트 하나로 38ms를 단축한 것입니다.

dataLayer.push()를 페인트 이후로 미루면 모든 GTM 처리 작업이 인터랙션의 critical path에서 빠집니다. 사용자는 시각적 피드백을 즉시 받게 됩니다. GTM 역시 여전히 실행되며, 단지 50~250ms 늦어질 뿐입니다.

고정 지연 시간, Idle Callback, scheduler.yield()를 쓰지 않는 이유

  • 고정된 setTimeout(delay): 지연 시간을 하드코딩(예: setTimeout(..., 100))하는 것은 렌더링 완료 시점을 어림짐작하는 것에 불과합니다. 지연 시간이 너무 길면 트래킹이 불필요하게 늦어지고, 너무 짧으면 여전히 페인트를 차단합니다.
  • requestIdleCallback: 브라우저가 유휴 상태일 때 작업을 예약하지만, 특정 인터랙션 직후 신속하게 실행된다는 보장이 없습니다. 콜백이 훨씬 늦게 실행되거나 사용자가 다른 페이지로 이동할 때까지 전혀 실행되지 않을 수도 있습니다.
  • scheduler.yield(): main thread로 yield하기 위한 최신 API입니다. 후속 작업을 큐 맨 뒤로 보내는 setTimeout과 달리 작업 우선순위를 보존합니다. 하지만 scheduler.yield()는 코드가 실행되기 전에 페인트가 일어났는지를 보장하지 않습니다. 이 특정한 용도에는 렌더링 라이프사이클에 직접 연결된 requestAnimationFrame이 더 나은 도구입니다. Safari에서는 scheduler.yield()가 아직 지원되지 않는다는 점도 유의해야 합니다.

requestAnimationFrame + setTimeout 조합은 바로 이 시나리오에 특화된 방식입니다. requestAnimationFrame은 브라우저가 페인트하기 직전에 실행됩니다. 중첩된 setTimeout은 해당 페인트가 완료된 뒤에 실행되는 새 태스크를 생성합니다. 이 둘이 함께 작동하여 지연된 코드가 시각적 업데이트 이후에 실행되도록 보장하며, 이는 정확히 INP가 측정하는 대상과 일치합니다.

트레이드오프

  • 데이터 수집의 미세한 지연: 동기식 push에 비해 이벤트가 GTM에 50~250ms 늦게 도달합니다. 애널리틱스와 마케팅 트래킹 측면에서 이는 큰 문제가 되지 않습니다.
  • 빠른 이탈 시 데이터 유실: 방문자가 이벤트를 발생시킨 직후 지연 시간 내에 페이지를 벗어나면 해당 이벤트가 전송되지 않을 수 있습니다. 리다이렉트나 페이지 언로드 직전에 발생하는 중요한 이벤트라면 dataLayer.push() 대신 navigator.sendBeacon()을 사용하세요. Beacon API에 대한 자세한 내용은 애널리틱스 스크립트를 제한해야 하는 이유를 참고하세요.

대부분의 트래킹에서 INP 개선 효과는 미세한 지연으로 인한 단점을 훨씬 상회합니다. 이벤트 로깅에 밀리초 단위 정밀도가 필요한 경우(실시간 입찰, 금융 대시보드 등)에는 적합하지 않지만, 그 외의 모든 상황에서는 확실한 개선책입니다.

오버라이드를 적용한 뒤 Real User Monitoring으로 개선 효과를 검증하세요. lab data에서도 변화가 나타나겠지만, Core Web Vitals 점수에 결정적인 영향을 미치는 것은 실제 네트워크 환경에서 실제 사용자가 발생시킨 field data입니다.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

보고서 말고 코드를 씁니다.

1~2 스프린트 동안 팀에 합류해서 모니터링까지 세팅해둡니다. 제가 빠진 뒤에도 지표가 계속 초록으로 유지되도록.

연락 주세요
dataLayer 이벤트 스케줄링으로 INP 최적화하기 Core Web Vitals dataLayer 이벤트 스케줄링으로 INP 최적화하기