노트

스텁 큐

Stub Queue

프론트엔드#js#performance · 연결된 개념 7개

쉽게 말하면

스텁 큐는 가게 주인이 오기 전에 문 앞 아르바이트생이 주문을 쪽지에 받아 적어 두는 방식이에요. 주인이 늦게 와도 쌓인 쪽지를 순서대로 처리하니까, 손님은 기다리지 않고 주문을 넣을 수 있어요.

비유의 한계 쪽지는 받아 적기만 해서 '재고 몇 개 남았어요?' 같은 질문엔 바로 답할 수 없어요. 또 쪽지는 페이지 메모리에만 있어서, 본체가 오기 전에 사용자가 떠나면 그대로 사라져요.

서드파티 SDK 본체가 아직 로드되지 않았을 때, 작은 스텁(stub)이 들어온 호출을 배열에 쌓아 두고 본체가 도착하면 쌓인 호출을 순서대로 처리하는 방식. Google Tag Manager(GTM)의 dataLayer와 gtag.js의 gtag()가 이 구조이고, Meta Pixel의 fbq() 설치 코드도 본체 전 호출을 배열에 쌓는다. 쌓아 두는 배열은 먼저 들어온 것부터 꺼내는 큐로 쓰인다.

GTM 설치 스니펫

<script>
  window.dataLayer = window.dataLayer || [];
</script>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');</script>
  • 스니펫이 하는 일은 셋이다. dataLayer 배열을 만들고(w[l]=w[l]||[]), 시작 메시지(gtm.start, event: 'gtm.js')를 넣고, gtm.js를 async로 삽입한다
  • 본체가 오기 전 dataLayer는 그냥 배열이다. dataLayer.push()는 배열에 항목을 붙일 뿐이다
  • 컨테이너가 로드되면 GTM은 쌓여 있던 메시지를 처리하기 시작한다. 들어온 순서대로(FIFO, First-In First-Out) 한 번에 하나씩 처리하고, 메시지가 이벤트면 조건이 맞은 태그를 실행한 뒤 다음 메시지로 넘어간다
  • 본체가 로드된 뒤의 dataLayer.push()·gtag() 호출도 큐에 들어가 앞서 대기 중인 메시지 다음에 처리된다
  • || [] 덕분에 앱 코드와 스니펫 중 어느 쪽이 먼저 실행돼도 같은 배열을 쓴다. 반대로 dataLayer = [...]처럼 다시 대입하면 앞서 쌓인 값이 사라지므로 공식 문서도 덮어쓰지 말라고 한다

gtag와 fbq

gtag.js도 같은 dataLayer를 큐로 쓴다. gtag()는 받은 인자를 그대로 넣는 한 줄짜리 함수다.

window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'TAG_ID');

Meta Pixel 기본 코드는 분기를 스텁 안에 둔다. fbq를 부르면 callMethod가 있을 때는 그쪽으로 넘기고, 없으면 fbq.queue에 인자를 쌓는다. 스니펫은 fbq.queue = []로 큐를 만들고 fbevents.js를 async로 삽입한다. callMethod를 정의하는 코드는 스니펫에 없다.

n = f.fbq = function () {
  n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments)
}

로드를 늦춰도 되는 이유

스텁은 몇 줄짜리라 일찍 실행해도 부담이 적다. 무거운 본체는 하이드레이션 뒤나 브라우저가 한가할 때로 미뤄도, 그사이 호출은 큐에 남았다가 나중에 처리된다. 그래서 스텁을 먼저 두면 본체 로드를 늦춰 첫 화면·하이드레이션과의 메인 스레드 경합(롱 태스크)을 줄일 수 있다(INP(Interaction to Next Paint), TBT(Total Blocking Time)).

Next.js next/script(2026 기준)의 strategy로 예를 들면 다음과 같다.

  • afterInteractive(기본값): 하이드레이션이 일부 또는 전부 끝난 뒤 로드한다. 공식 문서가 태그 매니저·분석 도구의 후보로 든다
  • lazyOnload: 페이지의 리소스를 다 받은 뒤 브라우저 유휴 시간에 로드한다. 채팅 위젯처럼 급하지 않은 스크립트용이다

한계

  • 큐는 페이지 메모리에만 있다. 공식 문서도 dataLayer 변수는 방문자가 그 페이지에 머무는 동안만 유지된다고 한다. 본체가 오기 전에 사용자가 떠나면 쌓인 이벤트는 보내지지 않고 사라진다. 본체를 늦출수록 이 구간이 길어진다
  • 스텁은 명령을 받아 적기만 한다. 반환값으로 무언가를 읽어 오는 API는 스텁 단계에서 답할 수 없다. gtag.js에서 값을 읽는 gtag('get', target, field_name, callback)도 결과를 반환값이 아니라 콜백으로 준다
  • 모든 SDK에 스텁이 있지는 않다. 스텁 없이 전역 객체를 바로 부르는 SDK는 로드를 늦추면 호출이 에러를 내거나 유실된다. 늦추기 전에 그 SDK의 설치 코드가 본체 전 호출을 큐에 쌓는지 확인한다

이탈 직전에 꼭 남겨야 하는 데이터라면 navigator.sendBeacon()을 검토할 수 있다. MDN은 분석 데이터를 페이지 이탈이나 다음 이동을 늦추지 않고 보내는 용도로 이 메서드를 소개하고, unload 대신 visibilitychange에서 보내라고 권한다(unload는 bfcache(뒤로·앞으로 가기 캐시)와도 맞지 않는다). 아직 보내지 않은 데이터를 합쳐 약 64KiB까지라, 그보다 크면 fetch()를 검토한다.

출처: Google Tag Manager — The data layer: Installation · How data layer information is processed · Persist data layer variables · Troubleshooting · Google tag — gtag.js API reference: get · Meta Pixel — Get Started: Base Code · Next.js — Script Component: strategy · MDN — Navigator: sendBeacon()

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 스택

    나중에 넣은 것을 먼저 꺼내는(Last In, First Out) 자료구조. 접시를 쌓듯 맨 위에서만 넣고(push) 꺼낸다(pop). 넣기·꺼내기 모두 O(1)이다.

  • 운영 Node 프로세스의 메모리 누수 찾기

    살아 있는 Node 서버에 inspector를 붙이고, 힙 스냅샷 두 장을 비교해 무엇이 쌓이는지, retainer로 누가 붙잡고 있는지 찾는 절차.

  • 이벤트 루프

    자바스크립트를 실행하는 하나의 스레드가 할 일을 큐에서 하나씩 꺼내 처리하는 반복 구조. JS 엔진 자체에는 타이머도 네트워크도 없고, 브라우저(또는 Node.js)가 이 루프를 돌리면서 콜백을 엔진에 넘겨준다.

  • 버전 스큐와 정적 자산 오프로드

    버전 스큐(Version Skew)는 새 버전을 배포한 뒤에도 브라우저에 떠 있는 이전 빌드의 클라이언트가 새 서버와 통신하면서 생기는 불일치다. 대표 증상은 이전 HTML이 참조하던 JS 청크를 늦게 불러오려다 404가 나는 ChunkLoadError다.

  • 마이크로태스크

    현재 실행 중인 코드가 끝난 직후, 다음 태스크로 넘어가기 전에 몰아서 처리되는 작은 작업. 이벤트 루프는 태스크 하나를 끝낼 때마다 마이크로태스크 큐가 빌 때까지 전부 실행한다.

보기 옵션