μž‘λ…„ 7μ›”μ―€, 고객사에 λ‚˜κ°€ 있던 μ œν’ˆμ˜ λͺ¨λ‹ˆν„°λ§μ„ λ‹΄λ‹Ήν•˜λŠ” μ„œλ²„μ—μ„œ OOM(Out Of Memory) μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, 이 λ¬Έμ œλŠ” νŒ€ λ‚΄λΆ€μ—μ„œ μž‘μ—…μ„ μ§„ν–‰ν•˜λ©΄μ„œλ„ λΉˆλ²ˆν•˜κ²Œ λ°œμƒν•˜λŠ” λ¬Έμ œμ˜€μŠ΅λ‹ˆλ‹€.

μ •ν™•ν•œ 원인을 단정 지을 μˆ˜λŠ” μ—†μ—ˆμ§€λ§Œ,

β€œν˜Ήμ‹œ ν”„λ‘ νŠΈμ—”λ“œ μ˜μ—­μ—μ„œ 쀄일 수 μžˆλŠ” μ„œλ²„ λ¦¬μ†ŒμŠ€κ°€ μžˆμ§€ μ•Šμ„κΉŒ?”

λΌλŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.

λ°±μ—”λ“œ μ˜μ—­μ—μ„œ λ°œμƒν•œ λ¬Έμ œμ˜€μ§€λ§Œ, ν΄λΌμ΄μ–ΈνŠΈμ—μ„œλ„ λΆ„λͺ… 영ν–₯을 쀄 수 μžˆλŠ” μ˜μ—­μ΄ μžˆμ„ 것이라 νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.


문제의 원인 νŒŒμ•…ν•˜κΈ°

νŒ€ λ‚΄λΆ€μ—μ„œλŠ” 보톡 ν•˜λ‚˜μ˜ 개발 μ„œλ²„λ₯Ό 두고 μž‘μ—…μ„ ν•©λ‹ˆλ‹€.

  • λŒ€μ‹œλ³΄λ“œ νƒ­ μ—¬λŸ¬ 개
  • 둜그 ν™•μΈμš© νƒ­
  • κΈ°λŠ₯ ν…ŒμŠ€νŠΈ 용

특히 νŠΉμ • ν”„λ‘œμ„ΈμŠ€λ₯Ό μˆ˜ν–‰ν•œ λ’€, λŒ€μ‹œλ³΄λ“œλ₯Ό 보며 ν˜„ν™© 체크λ₯Ό ν•˜λŠ” 일이 λΉˆλ²ˆν–ˆμŠ΅λ‹ˆλ‹€.
κ·Έ κ²°κ³Ό νŒ€μ› ν•œ λͺ…λ‹Ή μ—¬λŸ¬ 개의 λŒ€μ‹œλ³΄λ“œ 탭을 λ„μ›Œλ‘λŠ” 상황이 μžμ—°μŠ€λŸ½κ²Œ λ°˜λ³΅λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

λ¬Έμ œλŠ” μ—¬κΈ°μ„œ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

각 탭이 λͺ¨λ‘ 독립적인 WebSocket 연결을 μƒμ„±ν•˜κ³  μžˆμ—ˆλ˜ κ²ƒμž…λ‹ˆλ‹€.

μ΄λŠ” λ‹€μŒκ³Ό 같은 κ²°κ³Όλ₯Ό μ΄ˆλž˜ν–ˆμŠ΅λ‹ˆλ‹€.

νƒ­ 수 Γ— νŒ€μ› 수 = WebSocket 컀λ„₯μ…˜ 폭증


λͺ¨λ‹ˆν„°λ§ μ„œλ²„ νŠΉμ„±μƒ 연결은 였래 μœ μ§€λ˜κ³ , μ‹€μ‹œκ°„ λ°μ΄ν„°λŠ” μ§€μ†μ μœΌλ‘œ μŠ€νŠΈλ¦¬λ°λ©λ‹ˆλ‹€.
μ΄λŠ” κ²°κ΅­ μ„œλ²„ λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰ μ¦κ°€λ‘œ μ΄μ–΄μ§ˆ μˆ˜λ°–μ— μ—†λ‹€κ³  μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.


ν•΄κ²°μ±… 생각해보기

κ°€μž₯ λ‹¨μˆœν•˜κ²ŒλŠ” μ΄λ ‡κ²Œ μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

탭이 λΉ„ν™œμ„±ν™”λ˜λ©΄ WebSocket을 끊고, ν™œμ„±ν™”λ˜λ©΄ λ‹€μ‹œ μ—°κ²°ν•˜λ©΄ λ˜μ§€ μ•Šμ„κΉŒ?


μ‹€μ œλ‘œ visibilitychange 이벀트λ₯Ό ν™œμš©ν•΄ κ΅¬ν˜„ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ λ‹€μŒκ³Ό 같은 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

  • νƒ­ λΉ„ν™œμ„±ν™” β†’ WebSocket disconnect
  • λ‹€μ‹œ ν™œμ„±ν™” β†’ reconnect
  • 데이터 μˆ˜μ‹  μ „κΉŒμ§€ UIλŠ” Error 차트 μƒνƒœ
  • 데이터 μˆ˜μ‹  ν›„ 정상 UI 볡ꡬ

즉, μ‚¬μš©μžκ°€ 탭을 μ „ν™˜ν•  λ•Œλ§ˆλ‹€ UIκ°€ κΉ¨μ‘Œλ‹€κ°€ λ³΅κ΅¬λ˜λŠ” κ²½ν—˜μ„ ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ΄λŠ” λͺ…λ°±νžˆ μ‚¬μš©μž κ²½ν—˜μ„ ν•΄μΉ˜λŠ” 방식이라 μƒκ°ν•˜μ˜€κ³ , μ‚¬μš©μžμ˜ μ˜€ν•΄λ₯Ό λΆˆλŸ¬μΌμœΌν‚¬ 수 μžˆλŠ” 방식이라 μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.


Toss Slash 2024

그러던 쀑 Toss Slash 2024μ—μ„œ SharedWorkerλ₯Ό ν™œμš©ν•΄ 닀쀑 νƒ­μ˜ WebSocket 연결을 ν•˜λ‚˜λ‘œ μ€‘μ•™ν™”ν•œ 사둀λ₯Ό μ ‘ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή μ„Έμ…˜ λ°œν‘œλ₯Ό μ§„ν–‰ν–ˆλ˜ κ°œλ°œμžλΆ„λ„ SharedWorker을 μ•ŒκΈ° 전에 저와 같은 ν•΄κ²° 방법을 κ³ λ―Όν–ˆλ˜ κ²ƒμœΌλ‘œ 보아 μ μš©ν•˜κΈ° 전에 더 μ‹ λ’°κ°€ κ°”λ˜ 것 κ°™μŠ΅λ‹ˆλ‹€.

Reference


λΈŒλΌμš°μ € Worker 쀑 ν•˜λ‚˜μΈ SharedWorkerλŠ” μ—¬λŸ¬ νƒ­μ—μ„œ ν•˜λ‚˜μ˜ Worker μΈμŠ€ν„΄μŠ€λ₯Ό κ³΅μœ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

즉 λ‹€μŒκ³Ό 같은 ꡬ쑰가 κ°€λŠ₯ν•©λ‹ˆλ‹€.

νƒ­ (100개)
↓
SharedWorker
↓
WebSocket μ—°κ²° (1개)

SharedWorkerλ₯Ό ν™œμš©ν•œ 닀쀑 νƒ­μ˜ WebSocket μ—°κ²° μ€‘μ•™ν™”λŠ” κ²°κ΅­ μ•„λž˜μ™€ 같은 μž₯점을 κ°€μ§€κ²Œ λœλ‹€κ³  μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

  • 탭이 λͺ‡ λ°±κ°œμ—¬λ„ WebSocket은 1개
  • μ„œλ²„ 컀λ„₯μ…˜ 수 κ°μ†Œ
  • UX μ €ν•˜ μ—†μŒ
  • μ‹€μ‹œκ°„ 데이터 μœ μ§€ κ°€λŠ₯

SharedWorkerλ₯Ό ν™œμš©ν•˜λ©΄ ν”„λ‘ νŠΈμ—”λ“œ μ˜μ—­μ—μ„œ μ„œλ²„μ˜ λΆ€ν•˜λ₯Ό 무쑰건적으둜 쀄일 수 μžˆλ‹€ μƒκ°ν•˜μ˜€κ³ , WebSocket 연결이 도쀑에 λŠμ–΄μ§€λŠ” 방법 λ˜ν•œ μ•„λ‹ˆκΈ° λ•Œλ¬Έμ—, μ‚¬μš©μž κ²½ν—˜μ„ ν•΄μΉ  일이 μ—†λ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

λ”°λΌμ„œ SharedWorkerλ₯Ό 적극적으둜 μ μš©ν•΄λ³΄κΈ°λ‘œ κ²°μ •ν–ˆμŠ΅λ‹ˆλ‹€.


SharedWorker μ μš©ν•΄λ³΄κΈ°

SharedWorker μ»¨ν…μŠ€νŠΈμ—μ„œ 싀행될 슀크립트 νŒŒμΌμ„ λ¨Όμ € μž‘μ„±ν•΄μ£Όμ–΄μ•Όν•©λ‹ˆλ‹€.

SharedWorker μΈμŠ€ν„΄μŠ€λ₯Ό 생성할 λ•Œ, 파일 경둜λ₯Ό ν•„μˆ˜ 인자둜 μ „λ‹¬ν•΄μ£Όμ–΄μ•Όν•˜κ³ , κ·Έ 경둜의 파일이 SharedWorker μ»¨ν…μŠ€νŠΈμ—μ„œ μ‹€ν–‰λ˜κ²Œ λ©λ‹ˆλ‹€.

worker.ts νŒŒμΌμ„ λ¨Όμ € μž‘μ„±ν•΄μ£Όκ² μŠ΅λ‹ˆλ‹€.

SharedWorker μ „μ—­ μ„€μ •

파일 상단에 /// <reference lib="webworker" /> λ₯Ό μΆ”κ°€ν•΄μ£Όμ–΄μ•Όν•©λ‹ˆλ‹€. μ΄λŠ” Worker μ»¨ν…μŠ€νŠΈμ—μ„œ 싀행될 νŒŒμΌμž„μ„ λͺ…μ‹œν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€.

그리고 workerScope λ³€μˆ˜μ— self ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ SharedWorkerGlobalScope νƒ€μž…μœΌλ‘œ νƒ€μž… 좔둠이 κ°€λŠ₯ν•˜κ²Œ 해주도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

/// <reference lib="webworker" />
const workerScope = self as unknown as SharedWorkerGlobalScope

ν•΄λ‹Ή λ³€μˆ˜λŠ” 좔후에 νƒ­κ³Ό SharedWorker κ°„ 톡신 μ‹œ μ‚¬μš©λ©λ‹ˆλ‹€.

νƒ­ 고유 ID 생성

각 탭을 μ‹λ³„ν•˜κΈ° μœ„ν•΄ Worker λ‚΄λΆ€μ—μ„œ UUID v4λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈμ—μ„œλŠ” uuid 생성 κ΄€λ ¨ 라이브러리λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ—ˆμ§€λ§Œ, WorkerλŠ” 별도 μŠ€λ ˆλ“œμ΄λ―€λ‘œ μ„€μΉ˜λœ νŒ¨ν‚€μ§€μ˜ μ°Έμ‘°λ₯Ό 찾을 수 μ—†μœΌλ―€λ‘œ, 빌트인 crypto APIλ₯Ό 직접 μ‚¬μš©ν•˜κ² μŠ΅λ‹ˆλ‹€.

const generateUUID = () => {
  const randomValues = new Uint8Array(16)
  self.crypto.getRandomValues(randomValues)
 
  // UUID v4 ν˜•μ‹
  randomValues[6] = (randomValues[6] & 0x0f) | 0x40
  randomValues[8] = (randomValues[8] & 0x3f) | 0x80
 
  let uuid = ""
 
  for (let i = 0; i < 16; i++) {
    if (i === 4 || i === 6 || i === 8 || i === 10) uuid += "-"
    uuid += randomValues[i].toString(16).padStart(2, "0")
  }
 
  return uuid
}

νƒ­ 별 MessagePort 생λͺ…μ£ΌκΈ° 관리 클래슀 μƒμ„±ν•˜κΈ°

SharedWorkerλŠ” νƒ­κ³Ό 톡신을 μœ„ν•΄ MessagePortλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. 이 MessagePortλŠ” 탭이 μ’…λ£Œλ˜λ©΄ μžλ™μœΌλ‘œ λ‹«νžˆκ²Œ λ©λ‹ˆλ‹€. 이λ₯Ό κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ BrowserPort 클래슀λ₯Ό μƒμ„±ν•˜κ² μŠ΅λ‹ˆλ‹€.

class BrowserPort {
  private readonly weakRef: WeakRef<MessagePort>
  readonly id: string
 
  constructor(port: MessagePort) {
    this.weakRef = new WeakRef(port)
    this.id = generateUUID()
  }
 
  isAlive(): boolean {
    return !!this.weakRef.deref()
  }
 
  postMessage(message: unknown): void {
    this.weakRef.deref()?.postMessage(message)
  }
 
  close(): void {
    this.weakRef.deref()?.close()
  }
}

μœ„μ—μ„œ λ³΄μ‹œλ‹€μ‹œν”Ό BrowserPort ν΄λž˜μŠ€μ—μ„œ Portλ₯Ό WeakRef둜 ν•œ 번 λž˜ν•‘ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

Toss Slash 2024μ—μ„œ λ°œν‘œν•œ μ„Έμ…˜μ—μ„œλŠ” WeakRefλ₯Ό μ‚¬μš©ν•˜μ—¬ νƒ­ 별 MessagePortλ₯Ό κ΄€λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

특히 λͺ¨λ°”일 ν™˜κ²½μ΄λ‚˜ 비정상 μ’…λ£Œμ˜ 경우, μ‚¬μš©μž μž…μž₯μ—μ„œλŠ” 탭이 λ‹«νžŒ κ²ƒμ²˜λŸΌ 보이더라도 μ‹€μ œλ‘œλŠ” κ΄€λ ¨ μ΄λ²€νŠΈκ°€ μ •μƒμ μœΌλ‘œ μ „νŒŒλ˜μ§€ μ•ŠλŠ” 상황이 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이둜 인해 MessagePortκ°€ λͺ…μ‹œμ μœΌλ‘œ μ •λ¦¬λ˜μ§€ μ•Šκ³  남아 있게 λ©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄,MessagePortλ₯Ό λ‹¨μˆœ λ°°μ—΄λ‘œ κ΄€λ¦¬ν•˜λ©΄ 더 이상 μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” ν¬νŠΈκ°€ 배열에 계속 남아 μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λ•Œ κ°•ν•œ μ°Έμ‘°κ°€ μœ μ§€λ˜λ©΄ κ°€λΉ„μ§€ μ»¬λ ‰μ…˜μ˜ λŒ€μƒμ΄ λ˜μ§€ μ•Šμ•„ λ©”λͺ¨λ¦¬ λˆ„μˆ˜λ‘œ μ΄μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ WeakRefλ₯Ό μ‚¬μš©ν•˜λ©΄, MessagePort에 λŒ€ν•œ μ•½ν•œ μ°Έμ‘°(Weak Reference)만 μœ μ§€ν•˜κ²Œ λ˜μ–΄ μ‹€μ œλ‘œ 더 이상 μ°Έμ‘°λ˜μ§€ μ•ŠλŠ” κ°μ²΄λŠ” GC의 λŒ€μƒμ΄ 될 수 μžˆμŠ΅λ‹ˆλ‹€. 즉, 비정상 μ’…λ£Œλ‚˜ μ˜ˆμ™Έμ μΈ μƒν™©μ—μ„œλ„ λΆˆν•„μš”ν•œ λ©”λͺ¨λ¦¬ 점유λ₯Ό λ°©μ§€ν•  수 μžˆλ‹€κ³  μ΄ν•΄ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

νƒ­ 별 MessagePort 관리 및 WebSocket μ—°κ²° 관리

μ•„λž˜μ™€ 같이 <uuid, BrowserPort> ν˜•νƒœμ˜ Map μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜κ³ , νƒ­ 별 MessagePortλ₯Ό κ΄€λ¦¬ν•˜κ² μŠ΅λ‹ˆλ‹€.

const ports = new Map<string, BrowserPort>()

λ‹€μŒμœΌλ‘œ WebSocket μ—°κ²° 관리λ₯Ό μœ„ν•΄ ws λ³€μˆ˜λ₯Ό μƒμ„±ν•˜κ³ , connectWebSocket ν•¨μˆ˜λ₯Ό μƒμ„±ν•˜κ² μŠ΅λ‹ˆλ‹€.

let ws: WebSocket | null = null
 
const connectWebSocket = (url: string) => {
  if (ws && ws.readyState === WebSocket.OPEN) return // μž¬μ—°κ²° λ°©μ§€
 
  ws = new WebSocket(url)
 
  ws.onclose = () => {
    ws = null
  }
 
  ws.onmessage = (event) => {
    const { portId, ...chartData } = JSON.parse(event.data)
 
    //ν›„μ²˜λ¦¬..
  }
}

νƒ­ μ—°κ²° 처리 (onconnect)

제일 μ²˜μŒμ— SharedWorker μΈμŠ€ν„΄μŠ€κ°€ 생성될 λ•Œ, onconnect 이벀트 ν•Έλ“€λŸ¬κ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€.

이벀트 ν•Έλ“€λŸ¬ λ‚΄λΆ€μ—μ„œλŠ” νƒ­κ³Ό 톡신을 μœ„ν•΄ MessagePortλ₯Ό μƒμ„±ν•˜κ³ , ν•΄λ‹Ή MessagePortλ₯Ό WeakRef둜 ν•œ 번 감싸기 μœ„ν•΄ BrowserPort μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜κ³ , Map μΈμŠ€ν„΄μŠ€μ— μ €μž₯ν•˜κ² μŠ΅λ‹ˆλ‹€.

e.ports[0] 은 νƒ­κ³Ό 톡신을 μœ„ν•΄ μƒμ„±λœ MessagePort μΈμŠ€ν„΄μŠ€μž…λ‹ˆλ‹€.

ports ν”„λ‘œνΌν‹°κ°€ λ°°μ—΄λ‘œ λ„˜μ–΄μ˜€μ§€λ§Œ, μ—°κ²°λœ νƒ­μ˜ κ΄€λ ¨ port μ•„μ΄ν…œλ§Œ λ„˜μ–΄μ˜€κ²Œ λ©λ‹ˆλ‹€.

그리고 ν•΄λ‹Ή MessagePort의 onmessage 이벀트 ν•Έλ“€λŸ¬λ₯Ό μ„€μ •ν•˜κ³ , start λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ 탭이 λ©”μ‹œμ§€λ₯Ό 받을 수 있게 ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

workerScope.onconnect = (e) => {
  const port = e.ports[0]
 
  const browserPort = new BrowserPort(port)
 
  ports.set(browserPort.id, browserPort)
 
  port.onmessage = (portEvent) => {
    const { type, data: recvData } = portEvent.data
 
    switch (type) {
      case "CONNECT_WS":
        connectWebSocket(recvData.url)
 
        break
 
      case "SEND_DATA":
        if (ws?.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ ...recvData, portId: browserPort.id }))
        }
 
        break
 
      case "DISCONNECT":
        ports.delete(browserPort.id)
 
        browserPort.close()
 
        checkAndCleanupWebSocket()
 
        break
 
      case "CLEANUP_WS":
        ws?.close()
        ws = null
        break
    }
  }
 
  port.addEventListener("messageerror", () => {
    ports.delete(browserPort.id)
 
    browserPort.close()
 
    checkAndCleanupWebSocket()
  })
 
  port.start()
}

μœ„μ—μ„œ μ‚¬μš©λœ sendToPortλ©”μ„œλ“œμ™€ checkAndCleanupWebSocket λ©”μ„œλ“œλŠ” λ‹€μŒκ³Ό 같은 역할을 ν•©λ‹ˆλ‹€.

/**
 * νƒ­κ³Ό 톡신을 μœ„ν•΄ MessagePortλ₯Ό μ „μ†‘ν•˜λŠ” λ©”μ„œλ“œ
 * @param portId - νƒ­ 고유 ID
 * @param message - 전솑할 λ©”μ‹œμ§€
 */
const sendToPort = (portId: string, message: unknown) => {
  const port = ports.get(portId)
 
  if (port?.isAlive()) {
    port.postMessage(message)
  }
}
 
/**
 * WebSocket μ—°κ²° μƒνƒœλ₯Ό ν™•μΈν•˜κ³ , 연결이 λŠμ–΄μ§„ 경우 WebSocket을 λ‹«λŠ” λ©”μ„œλ“œ
 */
const checkAndCleanupWebSocket = () => {
  const hasActivePorts = Array.from(ports.values()).some((port) => port.isAlive())
 
  if (!hasActivePorts && ws) {
    ws.close()
    ws = null
  }
}

디버깅 ν•˜κΈ°

SharedWorker μΈμŠ€ν„΄μŠ€λ₯Ό 디버깅 ν•˜κΈ° μœ„ν•΄μ„œλŠ” νƒ­ 별 λΈŒλΌμš°μ € 개발자 도ꡬ가 μ•„λ‹Œ λ‹€λ₯Έ λ°©λ²•μœΌλ‘œ 디버깅을 ν•΄μ•Όν•©λ‹ˆλ‹€.

chrome://inspect/#workers
 
ν˜Ήμ€
 
edge://inspect/#workers

μœ„ 경둜둜 μ ‘μ†ν•˜κ³ , ν…ŒμŠ€νŠΈ ν•˜λŠ” url μ•„μ΄ν…œμ˜ inspect λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ μ•„λž˜μ™€ 같이 λΈŒλΌμš°μ € 개발자 도ꡬ와 λ™μΌν•œ UI νŒμ—…μ΄ λ‚˜μ˜€κ²Œ λ©λ‹ˆλ‹€.

worker_websocket_devtool

worker.ts νŒŒμΌμ—μ„œ console.logλ₯Ό ν˜ΈμΆœν–ˆλ‹€λ©΄, μ•„λž˜μ™€ 같이 μ½˜μ†”μ— λ‘œκ·Έκ°€ 찍히게 λ©λ‹ˆλ‹€.

worker_console_devtool

회고

결과적으둜 μ„œλ²„ λΆ€ν•˜λŠ” λˆˆμ— λ„κ²Œ κ°μ†Œν–ˆκ³ , 이 글을 μž‘μ„±ν•˜λŠ” ν˜„μž¬κΉŒμ§€ λͺ¨λ‹ˆν„°λ§ μ„œλ²„μ—μ„œ OOM(Out Of Memory) ν˜„μƒμ€ λ°œμƒν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. λ¬Όλ‘  이번 μ΄μŠˆλŠ” ν”„λ‘ νŠΈμ—”λ“œλ§Œμ˜ κ°œμ„ μœΌλ‘œ ν•΄κ²°λœ 것이 μ•„λ‹ˆλΌ, λ°±μ—”λ“œ μ˜μ—­κ³Ό λ³‘ν–‰ν•˜μ—¬ λŒ€μ‘ν•œ 결과이기 λ•Œλ¬Έμ— SharedWorkerλ₯Ό μ μš©ν–ˆκΈ° λ•Œλ¬Έμ— OOM이 ν•΄κ²°λ˜μ—ˆλ‹€κ³  단정할 μˆ˜λŠ” μ—†μŠ΅λ‹ˆλ‹€. λ‹€λ§Œ, 전체적인 μ•ˆμ •μ„± κ°œμ„ μ— λΆ„λͺ… 일정 λΆ€λΆ„ κΈ°μ—¬ν–ˆμ„ 것이라고 μƒκ°ν•©λ‹ˆλ‹€.

λ˜ν•œ MessagePortλ₯Ό WeakRef둜 κ΄€λ¦¬ν•œ λΆ€λΆ„ μ—­μ‹œ 의미 μžˆλŠ” μ‹œλ„μ˜€μŠ΅λ‹ˆλ‹€. ν† μŠ€μ¦κΆŒκ³Ό 같이 λ‹€μ–‘ν•œ λ””λ°”μ΄μŠ€μ™€ λΈŒλΌμš°μ € ν™˜κ²½μ„ 지원해야 ν•˜λŠ” μ„œλΉ„μŠ€λΌλ©΄, μ˜ˆμ™Έμ μΈ μ’…λ£Œ μΌ€μ΄μŠ€κΉŒμ§€ κ³ λ €ν•œ 생λͺ…μ£ΌκΈ° κ΄€λ¦¬λŠ” μ‹€μ œ 운영 ν™˜κ²½μ—μ„œ λΆ„λͺ… 체감할 수 μžˆλŠ” νš¨κ³Όκ°€ μžˆμ—ˆμ„ 것이라 μƒκ°ν•©λ‹ˆλ‹€.

반면, 저희 νŒ€ ν”„λ‘œμ νŠΈλŠ” λŒ€λΆ€λΆ„ λ°μŠ€ν¬ν†± ν™˜κ²½μ—μ„œ μ‹€ν–‰λ˜κ³  λͺ¨λ°”일을 μ§€μ›ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, WeakRef 기반 관리 방식이 λ‹€μ†Œ μ˜€λ²„μ—”μ§€λ‹ˆμ–΄λ§μ΄ μ•„λ‹ˆμ—ˆμ„κΉŒ ν•˜λŠ” 아쉬움도 λ‚¨μŠ΅λ‹ˆλ‹€. κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³ , 잠재적인 λ©”λͺ¨λ¦¬ λˆ„μˆ˜ κ°€λŠ₯성을 ꡬ쑰적으둜 μ°¨λ‹¨ν•΄λ³΄λŠ” κ²½ν—˜μ΄μ—ˆλ‹€λŠ” μ μ—μ„œ μΆ©λΆ„νžˆ 의미 μžˆλŠ” μ‹œλ„μ˜€λ‹€κ³  μ •λ¦¬ν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

μ΄μƒμœΌλ‘œ 글을 λ§ˆμΉ˜κ² μŠ΅λ‹ˆλ‹€.