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

사용자 행동 지표는 기능의 성과를 측정하고 가설을 검증하는 근거가 됩니다.
어느 날 유입 지표의 10~20%가 기록되지 않는 문제를 발견했습니다. 이벤트가 발생한 시점에 분석 스크립트의 초기화가 끝나지 않은 것이 원인이었습니다.
서드파티 스크립트의 초기화가 늦어도 이벤트를 잃지 않으려면 어떻게 해야 할까요?
이 글에서 다루는 내용
Google Analytics(GA), Meta Pixel, Sentry, Hotjar 같은 서드파티 스크립트는 이제 거의 모든 서비스에 들어갑니다. 문제는 이 스크립트들이 언제 준비되는지를 우리가 온전히 통제하기 어렵다는 데 있습니다. SDK나 호출부에 대기열이 없다면 준비 전에 발생한 이벤트가 유실될 수 있습니다.
이 글에서는 이벤트가 유실되는 과정과 스크립트의 로드·실행 우선순위를 정리한 뒤, 제가 검토하고 적용한 대안을 살펴봅니다. 각 대안은 서로 다른 단계의 문제를 다루므로 함께 적용할 수 있습니다.
- 대안 1) FIFO 이벤트 큐: 초기 이벤트 보관하기
- 대안 2) 청크 분리 & 브라우저 캐싱: 초기화를 앞당기기
- 대안 3)
next/scriptstrategy: 실행 순서를 명시하기 - 대안 4) Web Worker: 메인 스레드에서 덜어내기
문제: 왜 이벤트가 유실되는가
현대 웹 서비스는 사용자 행동 분석, 마케팅 성과 추적, 성능 모니터링을 위해 여러 서드파티 스크립트를 함께 불러옵니다. 그런데 이 스크립트들은 대부분 비동기로 다운로드된 뒤, 자바스크립트가 실행되는 시점에야 인스턴스를 초기화합니다. 즉 "스크립트 태그가 존재한다"와 "이벤트를 받을 준비가 됐다" 사이에는 시간 간격이 있습니다.
문제는 이 간격 안에서 이벤트가 발생할 때 생깁니다. 대표적인 경우는 다음과 같습니다.
- 첫 화면(above the fold)에서 즉시 발생하는 이벤트: 페이지뷰(PV), 히어로 영역 노출, 첫 클릭. 사용자는 스크립트의 초기화를 기다려주지 않습니다.
- SSR/하이드레이션 프레임워크: Next.js처럼 서버에서 HTML을 먼저 그리는 경우, 화면은 이미 보이지만 클라이언트 측 분석 인스턴스는 하이드레이션 이후에야 초기화됩니다. "보이는 시점"과 "추적 가능한 시점"의 격차가 더 벌어집니다.
- 유저 식별 지연: 이벤트에 유저 식별자를 붙여야 하는데, 그 식별자를 별도 API로 가져오는 동안 발생한 이벤트는 익명으로 새거나 유실됩니다.
이 경합(race)을 시퀀스로 그려보면 이렇습니다.
준비되지 않은 호출을 무시하는 구현에서는 콘솔 오류도 남지 않습니다. 서버 접근 로그의 페이지뷰와 클라이언트 이벤트 수를 비교하거나 지표 이상을 조사할 때 비로소 발견되기도 합니다. 이 사례의 10~20% 격차도 초기화 전 호출에서 발생했습니다.
해결의 방향은 두 갈래입니다. (1) 초기화되기 전에 발생한 이벤트를 잃지 않도록 붙잡거나(큐), (2) 초기화 자체를 최대한 앞당겨 간격을 좁히는 것(로드 전략). 아래에서 순서대로 살펴봅니다.
스크립트 로드 방식에 따른 로드/실행 우선순위 (Chromium 브라우저 기준)
간격을 좁히려면 먼저 스크립트가 어떻게 로드·실행되는지를 알아야 합니다. HTML 파서가 만나는 외부 classic 스크립트의 대표적인 처리 방식은 다음과 같습니다.
- 기본 로드 방식: HTML 파싱 중단, 로드 후 즉시 실행
- 비동기 로드(Async): HTML 파싱 병행, 로드 후 즉시 실행, 실행 순서 비보장
- 지연 로드(Defer): HTML 파싱 병행, 파싱 완료 후 선언 순서대로 실행
여기서 헷갈리기 쉬운 점은 로드(다운로드) 우선순위와 실행 우선순위가 별개로 움직인다는 것입니다. 예를 들어 async 스크립트는 다운로드가 끝나면 실행될 수 있으며, 실행 중에는 메인 스레드의 HTML 파싱도 진행되지 않습니다. 다운로드 우선순위의 구체적인 값은 브라우저 구현과 힌트에 따라 달라집니다. 선언 위치(<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)하는지에 따라 다름 |
위 우선순위 이름은 아래 자료가 작성된 당시의 Chromium 구현을 정리한 것으로, 웹 표준의 고정된 등급은 아닙니다.
참조: JavaScript Loading Priorities in Chrome
몇 가지 실무 포인트
type="module"스크립트는 기본이defer처럼 동작합니다. 모듈은 파싱을 막지 않고, 파싱 완료 후 선언 순서대로 실행됩니다.async를 붙이면 도착 즉시 실행으로 바뀝니다.fetchpriority속성(fetchpriority="high")으로 같은 로드 방식 안에서도 다운로드 우선순위를 끌어올릴 수 있습니다. 분석 스크립트처럼 "빨리 준비되어야 하지만 렌더를 막으면 안 되는" 리소스에 유용합니다.preload는 필요한 리소스에만 적용해야 합니다.as를 정확히 지정하지 않거나 실제로 곧 쓰이지 않으면 이중 다운로드나 낭비가 발생할 수 있습니다.- 서드파티 스크립트에
integrity(SRI)를 걸 때는 주의해야 합니다. 원본이 예고 없이 갱신되면 해시가 어긋나 로드가 실패합니다.
정리
스크립트는 어디에(head/body) 선언했는지와 어떤 옵션(async/defer/preload/module/fetchpriority)을 붙였는지에 따라 로드 우선순위와 실행 우선순위가 따로 움직입니다. 분석 스크립트처럼 "빨리 준비되어야 하지만 첫 화면 레이아웃을 막으면 안 되는" 성격이라면 preload + async나 type="module" async 조합이 좋은 출발점입니다. 이 표를 기준으로 아래 대안들을 살펴보겠습니다.
대안 1) FIFO 이벤트 큐
로드 전략을 다듬어도 초기화에 걸리는 시간을 완전히 없앨 수는 없습니다. 따라서 그동안 발생한 이벤트를 큐에 보관해야 합니다.
이벤트를 보낼 준비가 되지 않았다면(인스턴스 미초기화 또는 유저 식별자 미설정), 전역 컨텍스트의 FIFO 대기열에 페이로드를 쌓아두었다가, 준비가 끝난 순간 순서대로 flush합니다.
트래킹 진입점에서 준비 여부를 플래그로 판단하고, 조건을 충족하지 못하면 원래 보내려던 이벤트를 대기열에 적재합니다.
function trackEvent(payload) {
if (!인스턴스초기화여부 || !유저식별자세팅여부) {
global.eventQueue.enqueue(new TrackEvent(payload));
return;
}
tracker.dispatch(payload);
}
이벤트 큐는 프로젝트의 표준 이벤트 객체를 순서대로 담는 배열 등으로 구현할 수 있습니다. 초기화가 끝나 이벤트를 보낼 수 있게 되면 대기열을 먼저 비우고(flush) 현재 요청을 처리합니다.
const tracker = {
trackEvent(payload) {
if (!this.isReady) {
global.eventQueue.enqueue(payload);
return;
}
if (global.eventQueue.size >= 1) {
this.flush(); // 대기 이벤트를 발생 순서대로 먼저 전송
}
this.dispatch(payload);
},
};
초기화와 유저 식별이 완료되는 경로에서도 flush()를 호출해야 합니다. 다음 이벤트가 올 때만 비우면, 추가 이벤트 없이 이탈한 사용자의 기록은 전송되지 않습니다. 위 코드는 큐에 넣고 꺼내는 순서를 보여주는 의사 코드이며, 비동기 전송 완료까지 순서를 보장하려면 flush와 dispatch를 직렬화해야 합니다.
여기까지가 기본형입니다. 실전에서 이 큐를 신뢰할 수 있게 만들려면 몇 가지를 더 챙겨야 합니다.
- 순서 보존: FIFO를 지켜 발생 순서대로 flush합니다. 세션 시작 → PV → 첫 클릭의 순서가 뒤바뀌면 퍼널 분석이 왜곡됩니다.
- 큐 상한(bounded queue): 초기화가 끝내 실패하는 경우를 대비해 최대 크기를 둡니다. 상한을 넘겼을 때 오래된 항목부터 버릴지, 새 항목을 버릴지는 지표 성격(초기 퍼널이 중요한지, 최신 상태가 중요한지)에 따라 정합니다.
- flush 실패와 재시도: flush 도중 네트워크 오류가 나면 해당 이벤트는 큐에 남겨 재시도합니다. flush는 멱등(idempotent)하게 설계해 중복 집계를 피합니다. 이벤트에 클라이언트 생성 ID를 붙이고 수신 서버에서도 그 ID로 중복을 제거해야 합니다.
- 이탈 시점 방어: 사용자가 곧 페이지를 떠날 수 있으므로,
pagehide/visibilitychange시점에 남은 큐를navigator.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로 장기 캐싱할 수 있습니다. 재방문 사용자는 네트워크 왕복 없이 캐시에서 즉시 스크립트를 얻어 다운로드 대기 시간을 줄일 수 있습니다. 파싱·실행과 SDK 내부 초기화에 걸리는 시간은 여전히 남습니다.다만 이 조합에는 분명한 트레이드오프가 있습니다. 콘텐츠 해시가 파일명에 들어가지 않기 때문에, 나중에 번들 내용이 바뀌어도
immutable캐시를 가진 재방문자에게는 옛 스크립트가 계속 서빙됩니다(브라우저가 재검증조차 하지 않습니다). 그래서 이 방식은 자주 바뀌지 않는 서드파티 번들에 한정해 쓰고, 갱신이 필요할 때는 파일명 뒤에 버전 쿼리/서픽스를 붙이는 식으로 명시적인 캐시 무효화 수단을 함께 마련해야 합니다.

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

네트워크 패널에서도 두 파일이 별도 스크립트 요청으로 나타납니다. 이 캡처에서는 이미 받아 둔 파일을 메모리 캐시에서 읽어 다운로드 시간이 들지 않았습니다.

빌드 결과에서는 두 청크가 async 스크립트로 삽입되어 다운로드 완료 순서대로 실행될 수 있습니다. webpack의 cacheGroups.priority는 청크 분리 규칙 간 우선순위이며, HTML의 async나 네트워크 우선순위를 정하는 값이 아닙니다. SDK 내부 초기화의 선후 관계는 완료 신호나 Promise로 별도로 연결해야 합니다.
정리
청크 분리는 이벤트 핸들링 스크립트를 애플리케이션 번들에서 떼어내, 고정된 파일명으로 캐싱하고 병렬 로드해 초기화 시점 자체를 앞당기는 접근입니다. immutable 캐싱은 재방문 시 다운로드 비용을 줄이지만 고정 파일명은 갱신 시 캐시 무효화를 따로 챙겨야 합니다. 콘텐츠 해시를 붙인 파일명도 장기 캐싱할 수 있으며 갱신 관리에는 더 유리합니다. 청크 분리만으로 초기화 순서까지 보장되지는 않습니다.
대안 3) next/script strategy
Next.js를 사용한다면, next/script의 strategy 프로퍼티로 스크립트의 로드·실행 시점을 프레임워크 차원에서 조정할 수 있습니다. 로드 순서를 HTML 구조에 직접 손대지 않고도 선언적으로 제어할 수 있다는 것이 장점입니다.
strategy | 실행 시점 | 쓰임새 |
|---|---|---|
beforeInteractive | 하이드레이션 이전, 초기 HTML에 주입되어 최우선 | 봇 탐지·동의 관리 등 상호작용 전 필요한 스크립트 |
afterInteractive | 페이지의 일부 또는 전체 하이드레이션 이후 (기본값) | 대부분의 분석·태그 매니저 |
lazyOnload | 브라우저 유휴 시점 | 채팅 위젯 등 후순위 스크립트 |
worker (실험적) | 웹 워커에서 실행 (Partytown 기반) | 메인 스레드에서 덜어낼 무거운 스크립트 |
저는 트래킹 클라이언트를 최대한 빨리 준비시키기 위해 beforeInteractive를 사용했습니다.
worker 전략은 아직 실험적 기능이라 next.config.js의 experimental.nextScriptWorkers를 켜야 활성화되며, App Router에서는 지원하지 않습니다. 내부적으로 Partytown을 사용합니다(뒤의 대안 4와 연결됩니다).
beforeInteractive 전략의 동작
beforeInteractive 옵션의 스크립트는 서버에서 초기 HTML에 주입되며, 다른 Next.js 모듈(번들)보다 먼저 다운로드되고 페이지 하이드레이션이 발생하기 전에 실행됩니다. 다만 스크립트 안에서 시작한 비동기 초기화의 완료까지 보장하는 것은 아닙니다. (단, 이 전략은 루트 레이아웃/문서에서만 유효합니다.)
로드 전략이 서빙된 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로 불러온 두 URL이 모두 preload 링크에 포함되어 있습니다.

body에는 beforeInteractive로 지정한 URL과 ID가 self.__next_s 배열에 들어갑니다. 소스 코드에서 Script 컴포넌트를 아래쪽에 작성했더라도 Next.js가 이 정보를 읽어 하이드레이션보다 먼저 실행합니다.
이 빌드에서는 beforeInteractive 스크립트 정보가 __next_s에 담깁니다. 함께 보이는 self.__next_f는 App Router의 React Server Components 페이로드를 전달하는 경로입니다. 두 이름은 Next.js 내부 구현이며, 브라우저의 우선순위 큐 두 개를 뜻하지는 않습니다.
이렇게 next/script를 사용하면 명시적으로 더 높은 우선순위를 부여할 수 있고, beforeInteractive로 트래킹 초기화 스크립트를 일찍 실행하도록 배치할 수 있습니다. SDK가 준비됐는지는 별도 완료 신호로 확인해야 합니다. 라이브러리를 사용하더라도 애플리케이션 빌드 타임에 실행 우선순위를 명시적으로 지정할 수 있다는 점에서, strategy는 번들 스크립트와 서드파티 초기화의 순서를 다루는 가장 간편한 도구였습니다.
정리
next/script의 strategy는 프레임워크 차원에서 로드·실행 순서에 개입할 수 있는 스위치입니다. beforeInteractive로 선언한 스크립트는 preload로 우선순위가 오르고 __next_s 큐를 통해 하이드레이션보다 앞서 실행되므로, "번들보다 먼저 준비되어야 하는" 초기화 스크립트에 적합합니다. 청크 분리가 로드를 앞당긴다면, 이 전략은 실행 순서를 정해 줍니다.
대안 4) Web Worker
앞선 대안은 초기화 시점을 다뤘지만 웹 워커는 서드파티 스크립트가 메인 스레드를 오래 점유하는 문제를 다룹니다. 웹 워커(Web Worker)는 메인 스레드와 분리된 스레드에서 연산하므로, 옮길 수 있는 작업은 화면의 상호작용을 막지 않고 처리할 수 있습니다.
또 다른 장점은, 셰어드 워커(shared worker)의 경우 서로 다른 윈도우·탭·아이프레임 등 다른 스크립트 컨텍스트(realm)이더라도 같은 오리진이면 활성화된 포트를 통해 서로 통신할 수 있다는 점입니다. 워커가 다른 워커를 생성하는 중첩 워커는 별개의 기능이며, 이것만으로 여러 탭이 워커를 공유하게 되지는 않습니다.
라이브러리 구현체로는 Builder.io의 Partytown(@builder.io/partytown)이 있습니다. Partytown은 서드파티 스크립트를 웹 워커에서 실행시키되, 워커에서는 원래 접근할 수 없는 DOM/window API 호출을 동기 XHR과 서비스 워커로 프록시해 메인 스레드로 중계하는 방식으로 동작합니다.

Partytown은 사용자 인터랙션에 필요한 작업을 메인 스레드에 두고, 지원하는 서드파티 스크립트의 연산을 워커로 옮겨 메인 스레드의 부하를 줄입니다.

서드파티 스크립트는 많은 네트워크 요청과 JavaScript 파싱·실행, DOM 조작, 부족한 캐시 등 여러 비용을 만들 수 있습니다. Web Worker가 직접 줄이는 대상은 이 가운데 옮길 수 있는 JavaScript 연산으로 인한 메인 스레드 점유입니다. 네트워크 요청 수나 캐시 정책은 별도로 개선해야 합니다.
Partytown 적용 제약
Partytown의 기본 프록시 방식은 Service Worker를 사용하므로 서비스 워커를 등록할 수 있는 보안 컨텍스트와 배포 설정이 필요합니다. 이미 서비스 워커를 운영 중이라면 경로와 범위가 충돌하지 않는지도 확인해야 합니다. 앞서 언급한 Shared Worker를 직접 사용하는 기능은 브라우저별 지원 여부를 기능 감지한 뒤 폴백을 마련합니다.
Partytown 제작자도 트레이드오프를 문서에 밝혀 두었습니다. 프록시 계층을 거치는 만큼 아래 경우에는 제약이 있거나 주의가 필요합니다.
- 네트워크 요청을 가로채는 방식과 함께 쓸 때 동작을 확인해야 합니다.
- DOM 작업은 프록시를 거치므로 지연될 수 있습니다.
- UI 조작이 많은 서드파티 스크립트에는 적합하지 않을 수 있습니다.
- CORS 헤더가 없는 서드파티 리소스는 별도 프록시 설정이 필요할 수 있습니다.
- 워커에서 전달한 이벤트로는 메인 스레드 이벤트의 기본 동작을 막을 수 없습니다.
- 교차 출처 iframe의 쿠키와 저장소에는 제약이 있습니다.
- Service Worker 등록과 배포 경로를 함께 관리해야 합니다.
즉 DOM을 무겁게 조작하거나(UI 위젯), 이벤트의 기본 동작을 막아야 하는 스크립트에는 적합하지 않습니다. 순수하게 백그라운드에서 이벤트를 수집·전송하는 분석 스크립트가 가장 잘 맞습니다. 자세한 정보는 Partytown 공식 웹사이트에서 확인해보세요.
정리
웹 워커는 이벤트를 붙잡거나 순서를 조정하는 대신, 서드파티 스크립트의 연산을 메인 스레드 밖으로 옮겨 초기화와 실행이 서비스 성능을 갉아먹지 않게 하는 접근입니다. 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)
이벤트 전송 · 이탈 방어
웹 워커
