서드파티 스크립트 초기화가 늦어질 때 어떻게 해결할까요?

서비스의 유저 행동 지표는 성과 측정과 의사결정의 근거가 됩니다. 새로운 기능을 런칭하거나, 가설을 검증하는 실험에서도 트래킹 이벤트에 대한 의존도는 매우 높습니다.
그런데 어느 날, 유입 지표의 10~20%가량이 조용히 비어 있는 것을 발견했습니다. 서버가 죽은 것도, 코드가 터진 것도 아니었습니다. 원인은 의외로 단순했습니다 — 이벤트를 보내야 할 바로 그 순간에, 분석 스크립트가 아직 초기화되지 않았던 것입니다.
초기화가 늦는 서드파티 스크립트 때문에 새어나가는 이벤트를, 어떻게 붙잡을 수 있을까요?
이 글에서 다루는 내용
Google Analytics(GA), Meta Pixel, Sentry, Hotjar 같은 서드파티 스크립트는 이제 거의 모든 서비스에 들어갑니다. 문제는 이 스크립트들이 언제 준비되는지를 우리가 온전히 통제하기 어렵다는 데 있습니다. 준비되기 전에 발생한 이벤트는 그대로 유실됩니다.
이 글에서는 먼저 이벤트가 유실되는 메커니즘과 스크립트가 브라우저에서 로드·실행되는 우선순위를 정리한 뒤, 유실을 줄이기 위해 제가 검토하고 적용했던 네 가지 대안을 살펴봅니다. 네 대안은 경쟁 관계가 아니라 겹쳐 쌓는 방어막에 가깝습니다.
- 대안 1) FIFO 이벤트 큐 — 유실을 붙잡는 안전망
- 대안 2) 청크 분리 & 브라우저 캐싱 — 초기화를 앞당기기
- 대안 3)
next/scriptstrategy — 실행 순서를 명시하기 - 대안 4) Web Worker — 메인 스레드에서 덜어내기
문제 — 왜 이벤트가 유실되는가
현대 웹 서비스는 사용자 행동 분석, 마케팅 성과 추적, 성능 모니터링을 위해 여러 서드파티 스크립트를 동시에 싣습니다. 그런데 이 스크립트들은 대부분 비동기로 다운로드된 뒤, 자바스크립트가 실행되는 시점에야 인스턴스를 초기화합니다. 즉 "스크립트 태그가 존재한다"와 "이벤트를 받을 준비가 됐다" 사이에는 시간 간격이 있습니다.
문제가 되는 건 이 간격 안에서 이벤트가 발생할 때입니다. 대표적으로,
- 첫 화면(above the fold)에서 즉시 발생하는 이벤트 — 페이지뷰(PV), 히어로 영역 노출, 첫 클릭. 사용자는 스크립트의 초기화를 기다려주지 않습니다.
- SSR/하이드레이션 프레임워크 — Next.js처럼 서버에서 HTML을 먼저 그리는 경우, 화면은 이미 보이지만 클라이언트 측 분석 인스턴스는 하이드레이션 이후에야 초기화됩니다. "보이는 시점"과 "추적 가능한 시점"의 격차가 더 벌어집니다.
- 유저 식별 지연 — 이벤트에 유저 식별자를 붙여야 하는데, 그 식별자를 별도 API로 fetching하는 동안 발생한 이벤트는 익명으로 새거나 유실됩니다.
이 경합(race)을 시퀀스로 그려보면 이렇습니다.
이런 유실은 브라우저 콘솔에 에러를 남기지 않기 때문에 조용히 새어나갑니다. 그래서 대개는 지표가 이상하다는 신고나, 서버 접근 로그(예: PV)와 클라이언트 이벤트 수의 괴리를 통해 뒤늦게 발견됩니다. 유입 지표의 10~20% 격차가 바로 이 간격에서 나왔습니다.
해결의 방향은 두 갈래입니다. (1) 초기화되기 전에 발생한 이벤트를 잃지 않도록 붙잡거나(큐), (2) 초기화 자체를 최대한 앞당겨 간격을 좁히는 것(로드 전략). 아래에서 순서대로 살펴봅니다.
스크립트 로드 방식에 따른 로드/실행 우선순위 (Chromium 브라우저 기준)
간격을 좁히려면 먼저 스크립트가 어떻게 로드·실행되는지를 알아야 합니다. 웹 페이지에서 스크립트를 처리하는 방식은 크게 세 가지로 나뉩니다.
- 기본 로드 방식: HTML 파싱 중단, 로드 후 즉시 실행
- 비동기 로드(Async): HTML 파싱 병행, 로드 후 즉시 실행, 실행 순서 비보장
- 지연 로드(Defer): HTML 파싱 병행, 파싱 완료 후 선언 순서대로 실행
여기서 헷갈리기 쉬운 점은 로드(다운로드) 우선순위와 실행 우선순위가 별개로 움직인다는 것입니다. 예를 들어 async 스크립트는 낮은 우선순위로 다운로드되지만, 도착하는 즉시 파서를 멈추고 실행됩니다. 선언 위치(<head> / <body>)와 옵션 조합에 따라 두 우선순위가 어떻게 갈리는지 정리하면 다음과 같습니다.
| 스크립트 로드 방식 | 로드 우선순위 (네트워크 / 블링크) | 실행 우선순위 |
|---|---|---|
<head> 태그 안의 <script> | Medium / High | VeryHigh — 스크립트 파싱 블락 |
<link rel="preload"> + <script async>를 조합한 방식 또는 <script type="module" async> | Medium / High | High — 파서의 작동에 영향을 줌 |
<script async> | Lowest / Low | High — 파서의 작동에 영향을 줌 |
<script defer> | Lowest / Low | VeryLow — <body> 태그 끝의 <script> 실행 후 동작 |
<body> 태그 안에서 마지막에 위치한 <script> | Medium / High | Low — 파서의 동작이 끝난 후 실행 |
<body> 태그 안에서 마지막에 위치한 <script defer> | Lowest / Low — end of the queue | VeryLow — <body> 태그 끝의 <script> 실행 후 동작 |
<link rel="prefetch"> + <script> | Idle / Lowest | 스크립트를 어떻게 소비(consume)하는지에 따라 다름 |
참조: JavaScript Loading Priorities in Chrome
몇 가지 실무 포인트
type="module"스크립트는 기본이defer처럼 동작합니다. 모듈은 파싱을 막지 않고, 파싱 완료 후 선언 순서대로 실행됩니다.async를 붙이면 도착 즉시 실행으로 바뀝니다.fetchpriority속성(fetchpriority="high")으로 같은 로드 방식 안에서도 다운로드 우선순위를 끌어올릴 수 있습니다. 분석 스크립트처럼 "빨리 준비되어야 하지만 렌더를 막으면 안 되는" 리소스에 유용합니다.preload는 양날의 검입니다.as를 정확히 지정하지 않거나 실제로 곧 쓰이지 않으면 이중 다운로드나 낭비가 발생합니다. 정말 임계 경로(critical path)에 있는 스크립트에만 씁니다.- 서드파티 스크립트에
integrity(SRI)를 걸 때는 주의해야 합니다. 원본이 예고 없이 갱신되면 해시가 어긋나 로드가 실패합니다.
Recap
스크립트는 어디에(head/body) 선언했는지와 어떤 옵션(async/defer/preload/module/fetchpriority)을 붙였는지에 따라 로드 우선순위와 실행 우선순위가 따로 움직입니다. 분석 스크립트처럼 "빨리 준비되어야 하지만 첫 화면 레이아웃을 막으면 안 되는" 성격이라면 preload + async나 type="module" async 조합이 좋은 출발점입니다. 이 표를 기준선으로 삼아, 아래의 네 가지 대안을 살펴보겠습니다.
대안 1) FIFO 이벤트 큐
로드 전략을 아무리 다듬어도 초기화 간격을 0으로 만들 수는 없습니다. 그래서 가장 먼저 깔아두는 방어막은 그 간격 동안 발생한 이벤트를 잃지 않는 안전망입니다.
핵심 아이디어는 단순합니다. 이벤트를 보낼 준비가 되지 않았다면(인스턴스 미초기화 또는 유저 식별자 미설정), 전역 컨텍스트의 FIFO 대기열에 페이로드를 쌓아두었다가, 준비가 끝난 순간 순서대로 flush합니다.
트래킹 진입점에서 준비 여부를 플래그로 판단하고, 조건을 충족하지 못하면 원래 보내려던 이벤트를 대기열에 적재합니다.
if (!인스턴스초기화여부 || !유저식별자세팅여부) {
global.eventQueue.enqueue(new TrackEvent(/* ... */));
return;
}
이벤트 큐라는 것이 특별한 자료구조는 아닙니다. 프로젝트에서 쓰는 표준 이벤트 객체 컨벤션에 맞춘 페이로드를, 순서대로 차곡차곡 쌓아둔 배열(또는 셋)일 뿐입니다. 초기화가 끝나 이벤트 발송이 가능해지면, 트래킹 메소드에서 대기열을 먼저 비우고(flush) 실제 요청을 처리합니다.
trackEvent(payload) {
if (!this.isReady) {
global.eventQueue.enqueue(payload);
return;
}
if (global.eventQueue.size >= 1) {
this.flush(); // 대기 이벤트를 발생 순서대로 먼저 전송
}
this.dispatch(payload);
}
여기까지가 기본형입니다. 실전에서 이 큐를 신뢰할 수 있게 만들려면 몇 가지를 더 챙겨야 합니다.
- 순서 보존: FIFO를 지켜 발생 순서대로 flush합니다. 세션 시작 → PV → 첫 클릭의 순서가 뒤바뀌면 퍼널 분석이 왜곡됩니다.
- 큐 상한(bounded queue): 초기화가 끝내 실패하는 경우를 대비해 최대 크기를 둡니다. 상한을 넘겼을 때 오래된 항목부터 버릴지, 새 항목을 버릴지는 지표 성격(초기 퍼널이 중요한지, 최신 상태가 중요한지)에 따라 정합니다.
- flush 실패와 재시도: flush 도중 네트워크 오류가 나면 해당 이벤트는 큐에 남겨 재시도합니다. flush는 멱등(idempotent)하게 설계해 중복 전송을 피합니다(이벤트에 클라이언트 생성 ID 부여).
- 이탈 시점 방어: 사용자가 곧 페이지를 떠날 수 있으므로,
pagehide/visibilitychange시점에 남은 큐를navigator.sendBeacon()으로 밀어 넣으면 언로드 중 유실을 줄일 수 있습니다.
Recap
이벤트 큐는 초기화 지연 자체를 없애는 것이 아니라, 지연되는 동안 발생한 이벤트를 잃지 않도록 붙잡아두는 안전망입니다. 인스턴스와 유저 식별자 준비 여부를 플래그로 판단해 조건을 충족하지 못하면 전역 큐에 적재하고, 준비가 끝나면 발생 순서대로 flush합니다. 여기에 큐 상한·멱등 재시도·sendBeacon 이탈 방어를 더하면, 구현 비용이 낮으면서도 가장 넓은 범위의 유실을 막아줍니다.
대안 2) 청크 분리 & 브라우저 캐싱
큐가 유실을 사후에 붙잡는다면, 더 근본적인 개선은 이벤트 핸들링 스크립트를 최대한 빨리 로드해 초기화 간격 자체를 좁히는 것입니다.
이 방법은 서드파티 스크립트를 npm 라이브러리 번들로 소비하는 경우를 가정합니다. @amplitude/analytics-browser를 예시로 합니다.
import * as amplitude from "@amplitude/analytics-browser";
export class AmplitudeAnalytics {
apiKey: string;
isInitialized = false;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
initialize() {
if (!this.isInitialized) {
amplitude.init(this.apiKey, { defaultTracking: true });
this.isInitialized = true;
}
}
setUser(id: string) {
this.initialize();
amplitude.setUserId(id);
}
trackEvent(name: string, data?: Record<string, unknown>) {
this.initialize();
amplitude.track(name, data);
}
}
문제는 이 코드가 애플리케이션 메인 번들 안에 섞여 있으면, 거대한 앱 번들이 다 내려오고 파싱·실행된 뒤에야 분석 인스턴스가 준비된다는 점입니다. 그래서 공통으로 쓰이는 트래킹 스니펫과 패키지를 빌드 타임에 별도의 청크로 분리하고, 고정된 파일명으로 내보내 캐싱과 병렬 로드가 가능하도록 합니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
// ...
webpack(config, options) {
if (!options.isServer) {
config.optimization.splitChunks.cacheGroups.assetsChunk = {
chunks: "all",
enforce: true,
minChunks: 1,
name: "amplitude",
filename: "static/chunks/amplitude.bundle.js",
priority: 10,
test: /\/app\/Amplitude*/,
reuseExistingChunk: true,
};
config.optimization.splitChunks.cacheGroups.common = {
name: "amplitude-package",
filename: "static/chunks/amplitude-analytics-browser.bundle.js",
priority: 1,
minChunks: 1,
test: /[\\/]@amplitude\/*[\\/]/,
reuseExistingChunk: true,
};
}
return config;
},
async headers() {
return [
{
source: "/_next/static/chunks/amplitude:etc*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
];
},
};
export default nextConfig;
이 설정에는 두 가지 의도가 담겨 있습니다.
-
청크 분리 — 트래킹 스니펫(
amplitude)과 패키지(amplitude-analytics-browser)를 앱 번들과 떼어내, 앱 코드가 다 내려오기를 기다리지 않고 병렬로 로드·실행되게 합니다. -
고정 파일명 +
immutable캐싱 — 파일명을 고정하면 URL이 안정적이어서Cache-Control: max-age=31536000, immutable로 장기 캐싱할 수 있습니다. 재방문 사용자는 네트워크 왕복 없이 캐시에서 즉시 스크립트를 얻어 초기화 간격이 사실상 사라집니다.다만 이 조합에는 분명한 트레이드오프가 있습니다. 콘텐츠 해시가 파일명에 박히지 않기 때문에, 나중에 번들 내용이 바뀌어도
immutable캐시를 물고 있는 재방문자에게는 옛 스크립트가 계속 서빙됩니다(브라우저가 재검증조차 하지 않습니다). 그래서 이 방식은 자주 바뀌지 않는 서드파티 번들에 한정해 쓰고, 갱신이 필요할 때는 파일명 뒤에 버전 쿼리/서픽스를 붙이는 식으로 명시적인 캐시 무효화 수단을 함께 마련해야 합니다.

amplitude-analytics-browser.bundle.js와 amplitude.bundle.js 두 파일 모두 지정된 이름을 가진 별도의 청크로 분리되어 배포됩니다.

amplitude와 관련된 모듈만 따로 번들링되어 async 방식으로 스크립트 로드가 이루어지는 것을, build artifacts의 HTML 파일에서 확인할 수 있습니다.

다만 주의할 점이 있습니다. 스크립트 로드의 priority를 별도로 지정하지 않으면 청크는 async 방식으로 로드되기 때문에, 다운로드는 병렬로 이뤄지되 실행 순서까지 보장하지는 않습니다. 초기화의 선후 관계가 중요하다면(예: 유저 식별 → 이벤트 전송), 다음의 로드 전략으로 순서를 명시해야 합니다.
Recap
청크 분리는 이벤트 핸들링 스크립트를 애플리케이션 번들에서 떼어내, 고정된 파일명으로 캐싱하고 병렬 로드해 초기화 시점 자체를 앞당기는 접근입니다. immutable 캐싱까지 얹으면 재방문 시 간격이 사실상 사라지지만, 그 대가로 고정 파일명은 갱신 시 캐시 무효화를 스스로 챙겨야 합니다. 또한 priority를 명시하지 않으면 async로 로드되어 실행 순서는 보장되지 않으므로, 순서가 중요하면 다음 전략과 함께 다뤄야 합니다.
대안 3) next/script strategy
Next.js를 사용한다면, next/script의 strategy 프로퍼티로 스크립트의 로드·실행 시점을 프레임워크 차원에서 조정할 수 있습니다. 로드 순서를 HTML 구조에 직접 손대지 않고도 선언적으로 제어할 수 있다는 것이 장점입니다.
strategy | 실행 시점 | 쓰임새 |
|---|---|---|
beforeInteractive | 하이드레이션 이전, 초기 HTML에 주입되어 최우선 | 봇 탐지·동의 관리 등 상호작용 전 필요한 스크립트 |
afterInteractive | 하이드레이션 직후 (기본값) | 대부분의 분석·태그 매니저 |
lazyOnload | 브라우저 유휴 시점 | 채팅 위젯 등 후순위 스크립트 |
worker (실험적) | 웹 워커에서 실행 (Partytown 기반) | 메인 스레드에서 덜어낼 무거운 스크립트 |
저는 트래킹 클라이언트를 최대한 빨리 준비시키기 위해 beforeInteractive를 사용했습니다.
worker 전략은 아직 실험적 기능이라 next.config.js의 experimental.nextScriptWorkers를 켜야 활성화되며, 내부적으로 Partytown을 사용합니다(뒤의 대안 4와 연결됩니다).
beforeInteractive 전략의 동작
beforeInteractive 옵션의 스크립트는 서버에서 초기 HTML에 주입되며, 다른 Next.js 모듈(번들)보다 먼저 다운로드되고 페이지 하이드레이션이 발생하기 전에 실행됩니다. 모든 prior 코드보다 앞서 실행되므로, 상호작용 전에 준비되어야 하는 스크립트에 적합합니다. (단, 이 전략은 루트 레이아웃/문서에서만 유효합니다.)
로드 전략이 서빙된 HTML에서 어떻게 나타나는지 살펴보겠습니다.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<GlobalProvider>{children}</GlobalProvider>
<Script
id="afterInteractive-body"
strategy="afterInteractive"
src="http://127.0.0.1:5500/src/after.js"
/>
<Script
id="beforeInteractive-body"
strategy="beforeInteractive"
src="http://127.0.0.1:5500/src/index.js"
/>
</body>
</html>
);
}
호스트(3000포트)에서 afterInteractive와 beforeInteractive 각각의 옵션으로 5500포트 개발 서버에 특정 js를 요청합니다.

HTML의 head를 보면, Script로 불러온 두 스크립트 모두 preload로 우선순위를 높여 다운로드하는 것을 볼 수 있습니다.

스크립트가 선언된 body는 어떻게 되어 있을까요? 먼저 beforeInteractive를 사용한 스크립트는 소스 코드에서 가장 하단에 위치했음에도, 그 어떤 스크립트보다 앞서 배치됩니다.
흥미로운 점은 큐가 나뉜다는 것입니다. 다른 애플리케이션 번들 소스는 하이드레이션 태스크를 위한 큐로 보이는 self.__next_f에 enqueue되는 반면, beforeInteractive 스크립트는 파싱·평가 우선순위가 더 높아 보이는 __next_s라는 별도 큐에 push됩니다. 프레임워크가 이 스크립트를 "번들보다 먼저 처리해야 할 것"으로 분류한다는 신호입니다.
이렇게 next/script를 사용하면 명시적으로 더 높은 우선순위를 부여할 수 있고, beforeInteractive로 번들 스크립트 실행 전에 트래킹 클라이언트를 가장 먼저 준비시킬 수 있습니다. 라이브러리를 사용하더라도 애플리케이션 빌드 타임에 실행 우선순위를 명시적으로 지정할 수 있다는 점에서, strategy는 번들 스크립트와 서드파티 초기화의 순서를 다루는 가장 간편한 도구였습니다.
Recap
next/script의 strategy는 프레임워크 차원에서 로드·실행 순서에 개입할 수 있는 스위치입니다. beforeInteractive로 선언한 스크립트는 preload로 우선순위가 오르고 __next_s 큐를 통해 하이드레이션보다 앞서 실행되므로, "번들보다 먼저 준비되어야 하는" 초기화 스크립트에 적합합니다. 청크 분리가 로드를 앞당긴다면, 이 전략은 실행 순서를 붙잡아 줍니다.
대안 4) Web Worker
지금까지의 대안이 "언제 준비되는가"를 다뤘다면, 웹 워커는 다른 축의 문제 — 서드파티 스크립트가 메인 스레드를 잡아먹는 것 — 를 겨냥합니다. 웹 워커(Web Worker)는 브라우저 메인 스레드와 분리된 별도 스레드에서 병렬로 연산을 수행하므로, 메인 스레드가 서드파티 스크립트에 의해 블로킹되지 않고 본연의 서비스를 제공할 수 있습니다.
또 다른 장점은, 셰어드 워커(shared worker)의 경우 서로 다른 윈도우·탭·아이프레임 등 다른 스크립트 컨텍스트(realm)이더라도 같은 오리진이면 활성화된 포트를 통해 서로 통신할 수 있다는 점입니다. 부모 컨텍스트(parent page)에 종속된 하위 브라우저 컨텍스트 역시, 같은 오리진이면 spawn된 subworker를 사용할 수 있습니다.
라이브러리 구현체로는 Builder.io의 Partytown(@builder.io/partytown)이 있습니다. Partytown은 서드파티 스크립트를 웹 워커에서 실행시키되, 워커에서는 원래 접근할 수 없는 DOM/window API 호출을 동기 XHR과 서비스 워커로 프록시해 메인 스레드로 중계하는 방식으로 동작합니다.

메인 컨셉은, 우선순위 높은 연산만 메인 스레드가 맡고, 부차적인 서드파티 스크립트 관련 연산은 웹 워커에 일임하는 것입니다. 성능 부하를 덜어 메인 비즈니스 로직의 퍼포먼스를 최적화하는 것이 라이브러리의 목적입니다.

CPU-intensive한 연산이나 초기 XHR 부하가 큰 작업 등, 브라우저 성능에 영향을 줄 만한 작업을 대신 수행하기에 좋습니다.
Caveat
서비스 워커(Service Worker)는 사실상 모든 최신 브라우저에서 지원되는 성숙한 API이며, 네트워크 요청을 인터셉트한다는 특성상 폴리필(polyfill)로 대체하는 것도 원리적으로 불가능합니다. 오히려 주의할 대상은 셰어드 워커입니다. Safari에서 한동안 지원이 제거되었다가 16.0 버전에서야 다시 지원되었기 때문에, 기능 감지(feature detection) 후 폴백을 마련해야 합니다.
Partytown 제작자 또한 솔직하게 트레이드오프를 명시합니다. 프록시 계층을 거치는 만큼, 아래 영역에서는 제약이나 주의가 따릅니다.
- Intercepted Network Requests
- Throttled DOM Operations
- UI Intensive Third-Party Scripts
- Third-Party Scripts Without CORS Headers
- Events Cannot Prevent Default
- Cross-Origin iFrame Cookies And Storage
- Service Worker
즉 DOM을 무겁게 조작하거나(UI 위젯), 이벤트의 기본 동작을 막아야 하는 스크립트에는 적합하지 않습니다. 순수하게 백그라운드에서 이벤트를 수집·전송하는 분석 스크립트가 가장 잘 맞습니다. 자세한 정보는 Partytown 공식 웹사이트에서 확인해보세요.
Recap
웹 워커는 이벤트를 붙잡거나 순서를 조정하는 대신, 서드파티 스크립트의 연산을 메인 스레드 밖으로 옮겨 초기화와 실행이 서비스 성능을 갉아먹지 않게 하는 접근입니다. Partytown 같은 구현체가 이를 손쉽게 해주지만, DOM 조작 제약·기본 이벤트 차단 불가 등 트레이드오프가 뚜렷하므로 스크립트 성격을 따져 도입해야 합니다.
결론
서드파티 스크립트는 우리가 작성한 코드가 아니기에, "언제 준비되는가"는 끝내 우리 통제 밖의 변수로 남습니다. 프레임워크가 제공하는 라이프사이클 훅에 기대더라도, 그 스크립트가 초기화되는 정확한 시점까지 보장해 주지는 못합니다.
그래서 저는 하나의 완벽한 해법을 찾기보다, 여러 겹의 방어막을 겹쳐 두는 쪽을 택했습니다. 각 대안은 문제의 다른 결을 맡습니다.
큐로 유실을 붙잡는 안전망을 깔고(대안 1), 청크 분리와 캐싱으로 초기화 자체를 앞당기며(대안 2), next/script 전략으로 실행 순서를 명시하고(대안 3), 부하가 큰 스크립트는 웹 워커로 덜어냅니다(대안 4).
"알아서 잘 되겠지"라고 넘기는 것만큼 위험한 태도는 없다고 생각합니다.
완성도 높은 소프트웨어는 예측하기 쉽고, 예상한 대로 동작하며, 결과를 신뢰할 수 있어야 합니다. 조용히 새어나가던 10~20%의 지표는, 결국 스크립트가 준비되는 시점을 우리가 얼마나 정확히 다루느냐의 문제였습니다.
References
로드 우선순위 · 스크립트 전략
- JavaScript Loading Priorities in Chrome — Addy Osmani
- Optimizing Scripts (
next/scriptstrategy) — Next.js 공식 문서 <script>: async, defer — MDN- Resource Hints: preload / prefetch — web.dev
이벤트 전송 · 이탈 방어
웹 워커
