μλ 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.ts νμΌμμ console.logλ₯Ό νΈμΆνλ€λ©΄, μλμ κ°μ΄ μ½μμ λ‘κ·Έκ° μ°νκ² λ©λλ€.
νκ³
κ²°κ³Όμ μΌλ‘ μλ² λΆνλ λμ λκ² κ°μνκ³ , μ΄ κΈμ μμ±νλ νμ¬κΉμ§ λͺ¨λν°λ§ μλ²μμ OOM(Out Of Memory) νμμ λ°μνμ§ μμμ΅λλ€. λ¬Όλ‘ μ΄λ² μ΄μλ νλ‘ νΈμλλ§μ κ°μ μΌλ‘ ν΄κ²°λ κ²μ΄ μλλΌ, λ°±μλ μμκ³Ό λ³ννμ¬ λμν κ²°κ³Όμ΄κΈ° λλ¬Έμ SharedWorkerλ₯Ό μ μ©νκΈ° λλ¬Έμ OOMμ΄ ν΄κ²°λμλ€κ³ λ¨μ ν μλ μμ΅λλ€. λ€λ§, μ 체μ μΈ μμ μ± κ°μ μ λΆλͺ μΌμ λΆλΆ κΈ°μ¬νμ κ²μ΄λΌκ³ μκ°ν©λλ€.
λν MessagePortλ₯Ό WeakRefλ‘ κ΄λ¦¬ν λΆλΆ μμ μλ―Έ μλ μλμμ΅λλ€. ν μ€μ¦κΆκ³Ό κ°μ΄ λ€μν λλ°μ΄μ€μ λΈλΌμ°μ νκ²½μ μ§μν΄μΌ νλ μλΉμ€λΌλ©΄, μμΈμ μΈ μ’ λ£ μΌμ΄μ€κΉμ§ κ³ λ €ν μλͺ μ£ΌκΈ° κ΄λ¦¬λ μ€μ μ΄μ νκ²½μμ λΆλͺ 체κ°ν μ μλ ν¨κ³Όκ° μμμ κ²μ΄λΌ μκ°ν©λλ€.
λ°λ©΄, μ ν¬ ν νλ‘μ νΈλ λλΆλΆ λ°μ€ν¬ν± νκ²½μμ μ€νλκ³ λͺ¨λ°μΌμ μ§μνμ§ μκΈ° λλ¬Έμ, WeakRef κΈ°λ° κ΄λ¦¬ λ°©μμ΄ λ€μ μ€λ²μμ§λμ΄λ§μ΄ μλμμκΉ νλ μμ¬μλ λ¨μ΅λλ€. κ·ΈλΌμλ λΆκ΅¬νκ³ , μ μ¬μ μΈ λ©λͺ¨λ¦¬ λμ κ°λ₯μ±μ ꡬ쑰μ μΌλ‘ μ°¨λ¨ν΄λ³΄λ κ²½νμ΄μλ€λ μ μμ μΆ©λΆν μλ―Έ μλ μλμλ€κ³ μ 리νκ³ μΆμ΅λλ€.
μ΄μμΌλ‘ κΈμ λ§μΉκ² μ΅λλ€.