๐ŸŒฑ <groot/>
์—ด๋ ค์žˆ๋Š”๋ฐ ์ฃฝ์–ด์žˆ๋‹ค โ€” Manifest V3 Service Worker์˜ ์ข€๋น„ WebSocket ๋‹ค๋ฃจ๊ธฐ | Groot's Tech Blog
๐ŸŒWeb

์—ด๋ ค์žˆ๋Š”๋ฐ ์ฃฝ์–ด์žˆ๋‹ค โ€” Manifest V3 Service Worker์˜ ์ข€๋น„ WebSocket ๋‹ค๋ฃจ๊ธฐ

2026.07.02ยท3๋ถ„ ์ฝ๊ธฐ

์‹ค์‹œ๊ฐ„ ์‹œ์„ธ ํฌ๋กฌ ํ™•์žฅ์„ ๋งŒ๋“ค๋ฉด์„œ ๊ฐ€์žฅ ์˜ค๋ž˜ ์”จ๋ฆ„ํ•œ ๊ฑด "์‹ค์‹œ๊ฐ„ ์—ฐ๊ฒฐ์ด ์ž๊พธ ์กฐ์šฉํžˆ ์ฃฝ๋Š”" ๋ฌธ์ œ์˜€๋‹ค. Manifest V3(MV3)๋กœ ๋„˜์–ด์˜ค๋ฉด์„œ ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๊ฐ€ ๊ทผ๋ณธ์ ์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋ฌธ์ œ 1: Service Worker๋Š” ์–ธ์ œ๋“  ๊บผ์ง„๋‹ค

MV3์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋Š” Service Worker๋‹ค. ์˜ˆ์ „ MV2์˜ background page์™€ ๋‹ฌ๋ฆฌ ์œ ํœด ์ƒํƒœ๊ฐ€ ๋˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ทธ๋ƒฅ ์ข…๋ฃŒํ•ด๋ฒ„๋ฆฐ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ทธ ์•ˆ์—์„œ ๋Œ๋˜ WebSocket, setInterval, setTimeout์ด ์ „๋ถ€ ์‚ฌ๋ผ์ง„๋‹ค. ์‹ค์‹œ๊ฐ„ ์‹œ์„ธ๋ฅผ ๋ฐ›์•„์•ผ ํ•˜๋Š” ํ™•์žฅ ์ž…์žฅ์—์„  ์น˜๋ช…์ ์ด๋‹ค.

(์ •ํ™•ํžˆ ํ•˜๋ฉด, Chrome 116๋ถ€ํ„ฐ๋Š” ํ™œ์„ฑ WebSocket์œผ๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ค๊ฐ€๋ฉด SW์˜ idle ํƒ€์ด๋จธ๊ฐ€ ๋ฆฌ์…‹๋˜์–ด ์—ฐ๊ฒฐ์ด SW๋ฅผ ๊ณ„์† ์‚ด๋ ค๋‘˜ ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฑด "๋ฉ”์‹œ์ง€๊ฐ€ ์‹ค์ œ๋กœ ํ๋ฅผ ๋•Œ" ์–˜๊ธฐ๋‹ค. ์†Œ์ผ“์ด ์กฐ์šฉํ•ด์ง€๊ฑฐ๋‚˜ ๋’ค์—์„œ ๋‹ค๋ฃฐ ์ข€๋น„ ์ƒํƒœ๊ฐ€ ๋˜๋ฉด SW๋Š” ์—ฌ์ „ํžˆ ์ข…๋ฃŒ๋œ๋‹ค. ๊ฒฐ๊ตญ "์–ธ์ œ๋“  ๊บผ์ง„๋‹ค"๋Š” ์ „์ œ๋Š” ์œ ํšจํ•˜๋‹ค.)

ํ•ด๊ฒฐ์˜ ์‹ค๋งˆ๋ฆฌ๋Š” chrome.alarms์˜€๋‹ค. ์ด๊ฑด SW๊ฐ€ ๊บผ์ ธ ์žˆ์–ด๋„ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊นจ์›Œ์ค€๋‹ค. ๊ทธ๋ž˜์„œ 1๋ถ„๋งˆ๋‹ค SW๋ฅผ ๊นจ์›Œ์„œ ๋ชจ๋“  ๊ฑธ ๋ณต๊ตฌํ•˜๊ฒŒ ํ–ˆ๋‹ค.

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name !== REFRESH_ALARM) return;
  // SW๊ฐ€ ์ฃฝ์—ˆ๋‹ค๊ฐ€ ์•Œ๋žŒ์œผ๋กœ ๋‹ค์‹œ ๋œจ๋ฉด ์ „์—ญ ์ƒํƒœ(๊ตฌ๋… ํ‚ค)๊ฐ€ ์ดˆ๊ธฐํ™”๋ผ ์žˆ๋‹ค.
  // ๊ทธ๋ž˜์„œ syncRealtimeTargets๊ฐ€ "๊ตฌ๋…์ด ์—†๋„ค" ํ•˜๊ณ  WebSocket์„ ์ƒˆ๋กœ ๋งŒ๋“ ๋‹ค.
  refreshQuotes();
  syncRealtimeTargets();       // โ† SW ์žฌ๊ธฐ๋™ ์‹œ WebSocket ์žฌ๊ตฌ์ถ•
  scheduleForegroundRefresh(); // โ† ํด๋ง ํƒ€์ด๋จธ ๋ณต๊ตฌ
});

์ฆ‰, ์•Œ๋žŒ ์ฝœ๋ฐฑ์€ "์‹œ์„ธ ํ•œ ๋ฒˆ ์ƒˆ๋กœ๊ณ ์นจ"์ด ์•„๋‹ˆ๋ผ ์ฃฝ์—ˆ์„์ง€ ๋ชจ๋ฅผ ์‹œ์Šคํ…œ ์ „์ฒด๋ฅผ ๋˜์‚ด๋ฆฌ๋Š” ๋ถ€ํ™œ ์ง€์ ์ด๋‹ค.

ํ•œ ๊ฐ€์ง€ ์งš๊ณ  ๊ฐˆ ๊ฒŒ ์žˆ๋Š”๋ฐ, syncRealtimeTargets๋Š” ๊ตฌ๋…ํ•  ์‹ฌ๋ณผ ๋ชฉ๋ก์ด ์ด์ „๊ณผ ๊ฐ™์œผ๋ฉด early-return ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ SW๊ฐ€ ์‚ด์•„์žˆ๋Š” ์ฑ„๋กœ ์†Œ์ผ“๋งŒ ์ฃฝ์€ ๊ฒฝ์šฐ์—” ์•Œ๋žŒ์ด ๋ชป ์‚ด๋ฆฐ๋‹ค. ์•Œ๋žŒ์˜ ์—ญํ• ์€ ์–ด๋””๊นŒ์ง€๋‚˜ SW ์ž์ฒด๊ฐ€ ์ฃฝ์—ˆ๋‹ค ๋˜์‚ด์•„๋‚ฌ์„ ๋•Œ์˜ ์žฌ๊ตฌ์ถ•์ด๋‹ค. ์‚ด์•„์žˆ๋Š” SW ์•ˆ์˜ ์ฃฝ์€ ์†Œ์ผ“์€ ๋’ค์—์„œ ๋‹ค๋ฃฐ 15์ดˆ ํ—ฌ์Šค์ฒดํฌ ํƒ€์ด๋จธ๊ฐ€ ์žก๋Š”๋‹ค. ์ด ํƒ€์ด๋จธ๋Š” ๋งค๋ฒˆ "์ตœ๊ทผ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•˜๋Š”๊ฐ€"๋ฅผ ํ™•์ธํ•˜๊ณ , ์•„๋‹ˆ๋ฉด ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•œ๋‹ค. ์—ฌ๊ธฐ์— ์†Œ์ผ“์ด ๋‹ซํž ๋•Œ ๊ตฌ๋… ํ‚ค๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์„œ, ๋‹ค์Œ syncRealtimeTargets๊ฐ€ early-return์œผ๋กœ ์ฃฝ์€ ์—ฐ๊ฒฐ์„ ๋ฐฉ์น˜ํ•˜์ง€ ์•Š๊ฒŒ ํ–ˆ๋‹ค.

๋ฌธ์ œ 2: ์—ฐ๊ฒฐ์ด "์—ด๋ ค์žˆ๋Š”๋ฐ ์ฃฝ์–ด์žˆ๋‹ค" (์ข€๋น„)

๋” ๊นŒ๋‹ค๋กœ์šด ๊ฑด ์ด๊ฑฐ์˜€๋‹ค. socket.readyState๊ฐ€ OPEN์ธ๋ฐ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ ์˜จ๋‹ค. ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ฒผ๋Š”๋ฐ onclose๊ฐ€ ์•ˆ ๋ถˆ๋ฆฌ๋Š” half-open ์ƒํƒœ. TCP ํŠน์„ฑ์ƒ ํ”ํžˆ ์ƒ๊ธด๋‹ค. readyState๋งŒ ๋ฏฟ์œผ๋ฉด "์—ฐ๊ฒฐ๋ผ ์žˆ๋„ค" ํ•˜๊ณ  ๋ฐฉ์น˜ํ•˜๊ฒŒ ๋œ๋‹ค.

๊ทธ๋ž˜์„œ "๋งˆ์ง€๋ง‰ ์ˆ˜์‹  ์‹œ๊ฐ" ์„ ๊ธฐ์ค€์œผ๋กœ ์ข€๋น„๋ฅผ ํŒ์ •ํ–ˆ๋‹ค.

const REALTIME_STALE_MS = 40 * 1000; // 40์ดˆ ๋™์•ˆ ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ์˜ค๋ฉด ์ข€๋น„

async function ensureRealtimeConnection() {
  if (realtimeSocket?.readyState === WebSocket.OPEN) {
    const stale =
      lastRealtimeEventAt && Date.now() - lastRealtimeEventAt > REALTIME_STALE_MS;
    if (!stale) {
      sendRealtimeSubscription("replace");
      return;
    }
    closeRealtimeSocket(); // OPEN์ด์–ด๋„ ์ฃฝ์—ˆ๋‹ค ํŒ๋‹จ โ†’ ๊ฐ•์ œ ์žฌ์—ฐ๊ฒฐ
  }
  // ... ์ƒˆ ์†Œ์ผ“ ์ƒ์„ฑ
}

๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์„ ๋•Œ๋งˆ๋‹ค lastRealtimeEventAt = Date.now()๋ฅผ ์ฐ๊ณ , 40์ดˆ ๋„˜๊ฒŒ ์†Œ์‹์ด ์—†์œผ๋ฉด readyState๊ฐ€ ๋ญ๋ผ ํ•˜๋“  ๋Š๊ณ  ๋‹ค์‹œ ์—ฐ๊ฒฐํ•œ๋‹ค.

๋ฌธ์ œ 3: ์žฌ์—ฐ๊ฒฐ ํญํ’ (thundering herd)

์„œ๋ฒ„๊ฐ€ ์ž ๊น ์žฌ์‹œ์ž‘๋˜๋ฉด, ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋™์‹œ์— ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•œ๋‹ค. ๊ฐ€๋œฉ์ด๋‚˜ ํž˜๋“  ์„œ๋ฒ„์— ์ˆœ๊ฐ„ ํญํƒ„์ด ๋–จ์–ด์ง„๋‹ค. ๊ทธ๋ž˜์„œ ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ + ์ง€ํ„ฐ๋ฅผ ๋„ฃ์—ˆ๋‹ค.

const RECONNECT_BASE_MS = 1000;      // 1์ดˆ์—์„œ ์‹œ์ž‘
const RECONNECT_MAX_MS = 60 * 1000;  // ์ตœ๋Œ€ 60์ดˆ

function scheduleRealtimeReconnect() {
  const backoff = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** attempt);
  const jitter = Math.floor(Math.random() * (backoff * 0.3)); // โ† ๋ฌด์ž‘์œ„ ์ง€ํ„ฐ
  const delay = backoff + jitter;
  attempt++;
  setTimeout(() => ensureRealtimeConnection(), delay);
}
  • ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1์ดˆ โ†’ 2์ดˆ โ†’ 4์ดˆ โ€ฆ ์‹คํŒจํ• ์ˆ˜๋ก ๊ฐ„๊ฒฉ์„ ๋ฒŒ๋ฆฐ๋‹ค.
  • ์ง€ํ„ฐ: ์—ฌ๊ธฐ์— ๋ฌด์ž‘์œ„ ์‹œ๊ฐ„์„ ๋”ํ•ด, ํด๋ผ์ด์–ธํŠธ๋“ค์˜ ์žฌ์—ฐ๊ฒฐ ์‹œ์ ์„ ํฉ๋ฟŒ๋ฆฐ๋‹ค. ๋ชจ๋‘๊ฐ€ ์ •ํ™•ํžˆ ๊ฐ™์€ ์ˆœ๊ฐ„์— ๋ชฐ๋ฆฌ๋Š” ๊ฑธ ๋ง‰๋Š”๋‹ค.

์ข…ํ•ฉ: ์„ธ ๊ฒน์œผ๋กœ ์‚ด์•„๋‚จ๊ธฐ

๊ฒฐ๊ตญ ์‹ค์‹œ๊ฐ„์„ ์ด๋ ‡๊ฒŒ ์„ธ ๊ฒน์œผ๋กœ ์Œ“์•˜๋‹ค.

  1. WebSocket โ€” ์ •์ƒ์ผ ๋• ์ด๊ฒŒ ์‹ค์‹œ๊ฐ„์„ ๋‹ด๋‹น
  2. 15์ดˆ ํ—ฌ์Šค์ฒดํฌ ํด๋ง โ€” WS๊ฐ€ ๋Š๊ธฐ๊ฑฐ๋‚˜ ์ข€๋น„๋ฉด ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๊ณ , ๊ทธ๋™์•ˆ REST๋กœ ๋ฉ”์šด๋‹ค
  3. 1๋ถ„ ์•Œ๋žŒ โ€” SW ์ž์ฒด๊ฐ€ ์ฃฝ์—ˆ๋‹ค ๋˜์‚ด์•„๋‚ฌ์„ ๋•Œ ์ „๋ถ€ ์žฌ๊ตฌ์ถ•ํ•œ๋‹ค

๊ฐ ๊ณ„์ธต์ด ์„œ๋กœ์˜ ์‹คํŒจ๋ฅผ ๋ฎ์–ด์ค€๋‹ค. "์—ฐ๊ฒฐ์€ ์–ธ์  ๊ฐ€ ๋ฐ˜๋“œ์‹œ ๋Š๊ธด๋‹ค"๋ฅผ ์ „์ œ๋กœ ์„ค๊ณ„ํ•˜๋‹ˆ, ์˜คํžˆ๋ ค ์•ˆ์ •์ ์œผ๋กœ ๊ตด๋Ÿฌ๊ฐ”๋‹ค.

๋ฐฐ์šด ๊ฒƒ

MV3 ํ™•์žฅ ๊ฐœ๋ฐœ์€ "๋ฐฑ๊ทธ๋ผ์šด๋“œ๊ฐ€ ํ•ญ์ƒ ์‚ด์•„์žˆ๋‹ค"๋Š” ๊ฐ€์ •์„ ๋ฒ„๋ฆฌ๋Š” ๋ฐ์„œ ์‹œ์ž‘ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  readyState === OPEN์€ "์—ฐ๊ฒฐ์ด ์‚ด์•„์žˆ๋‹ค"๋Š” ๋ณด์žฅ์ด ์•„๋‹ˆ๋‹ค. ์ง„์งœ ์‚ด์•„์žˆ๋Š”์ง€๋Š” ์‹ค์ œ๋กœ ์ตœ๊ทผ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•˜๋Š”๊ฐ€๋กœ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค. ๋„คํŠธ์›Œํฌ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ฝ”๋“œ๋Š” ์ „๋ถ€ "์ฃฝ์—ˆ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ๋˜์‚ด๋ฆด๊นŒ"๋ฅผ ๋จผ์ € ์„ค๊ณ„ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฑธ ๋ชธ์œผ๋กœ ๋ฐฐ์› ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ