C.W.K.
Stream
Lesson 04 of 05 · published

Socket 소유자 하나, Listener 여럿

~11 min · frontend-architecture, single-owner, events, react

Level 0Open Gate
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"자기 socket 을 여는 모든 컴포넌트는 일어나길 기다리는 작은 누수야. 연결은 한 번 소유하고; 나머지는 그냥 듣게 해."

스트림은 컴포넌트별이 아니라 앱 레벨이야

Keep 은 live 가격을 원하는 workspace 여럿을 가져 — 포트폴리오 view, market pulse, focused ticker. 유혹적인 React 패턴은 그 컴포넌트마다 필요한 WebSocket 을 여는 거야. Keep 은 그걸 금지해. 단일 앱 레벨 hook(useMarketStream)이 유일한 WebSocket 을 소유해. 모든 workspace 는 자기 연결을 여는 대신 broadcast 이벤트(keep-market-update 이벤트)를 들어. 소유자 하나, listener 여럿.

왜 컴포넌트별 socket 이 엉망인가

컴포넌트마다 자기 socket 을 열게 하면 문제 더미에 등록하는 거야. 이제 세 컴포넌트가 같은 provider hub 로 세 연결을 쥐어, 부하 세 배. 각각 자기 mount/unmount 라이프사이클이라, 컴포넌트가 정리 없이 unmount 하면 socket 이 누수되거나, view 가 remount 하면 reconnect-storm 이 나. 컴포넌트들이 현재 가격을 두고 의견 갈릴 수 있어, socket 이 살짝 다른 순간에 tick 을 받았으니까. 그리고 'pause' 가 악몽이 돼 — 하나 대신 모든 컴포넌트의 socket 을 찾아 멈춰야 해. 컴포넌트별 연결은 깔끔한 관심사 하나를 엉킨 N 개로 바꿔.

공유 외부 자원은 정확히 소유자 하나를 가져야 해. 연결·구독·타이머·스트림은 소비자당 하나가 아니라 단일 라이프사이클을 원해. 자원에 소유자 하나를 주고 그 출력을 broadcast 해; 소비자는 데이터를 구독하지 연결을 절대 안 해. 이게 N 개 라이프사이클을 하나로 무너뜨리고 '전부 멈춤' 을 단일 행위로 만들어.

bridge 와 overlay

밑에서 흐름은 깔끔한 모양이야. broadcast tick 이 먼저 thread-safe 하고 process-local 한 price bridge — 최신 값의 작은 in-memory store — 를 업데이트해. 그다음 REST projection(포트폴리오, watchlist, focus, pulse, export)이 렌더할 때 그 새 값을 durable 데이터 위에 overlay 해. 그래서 live 와 durable 은 UI 에서 싸우는 두 경쟁 source 가 아냐; live 는 읽을 때 안정된 base 위에 칠해진 얇고 fresh 한 레이어야. socket 소유자가 bridge 를 최신으로 유지하고; 나머지 모두는 bridge-over-durable 합성을 읽어.

'이 view 하나만' 이라고 workspace 안에 socket 을 재도입하지 마. 항상 지름길로 시작해 — 컴포넌트 하나가 데이터를 좀 더 빨리 원해서 자기 연결을 열어. 이제 소유자가 둘이고, single-owner 보장 전체가 사라졌어: 두 배 부하, 쪼개진 라이프사이클, 하나를 놓치는 pause. view 가 스트림이 필요하면 앱 레벨 소유자를 들어; 자기 걸 절대 안 열어.
이건 single DB writer 와 같은 'owner 하나' 본능이야. 백엔드에선 프로세스 하나가 모든 쓰기를 소유; 프론트엔드에선 hook 하나가 모든 socket 을 소유. 둘 다 공유 자원의 소유자 여럿을 거부해서 concurrency 버그 한 부류를 녹여. 패턴이 스택 전체에 각운을 맞춰: 모두가 공유하는 걸 중앙화하고, 그 결과를 broadcast 해.

Code

hook 하나가 socket 을 소유; workspace 는 듣기만·typescript
// 앱 레벨: socket 이 열리는 유일한 곳.
function useMarketStream() {
  useEffect(() => {
    const socket = openSocket(`/ws/stocks?live=1`);
    socket.onMessage = (tick) => {
      priceBridge.update(tick);                       // 최신값 store
      window.dispatchEvent(                            // listener 에게 broadcast
        new CustomEvent("keep-market-update", { detail: tick }));
    };
    return () => socket.close();                       // 정리할 라이프사이클 하나
  }, []);
}

// workspace 는 LISTEN — 자기 socket 을 절대 안 엶.
function useLivePrice(ticker: string) {
  const [v, setV] = useState(durableClose(ticker));
  useEffect(() => {
    const on = (e: CustomEvent) => { if (e.detail.ticker === ticker) setV(e.detail.value); };
    window.addEventListener("keep-market-update", on as EventListener);
    return () => window.removeEventListener("keep-market-update", on as EventListener);
  }, [ticker]);
  return v;   // durable close, live bridge 값으로 overlay
}

External links

Exercise

같은 live feed 가 각자 필요한 컴포넌트 셋을 상상해. 컴포넌트별 버전(각자 socket 엶)을 스케치하고 그게 만드는 모든 문제를 나열해: 부하, 누수, 불일치, pause 복잡성. 그다음 broadcast 하는 소유자 하나에 컴포넌트가 듣는 걸로 리팩터해. 각 설계에서 '전부 pause' 가 뭘 드는지 설명해 — 행위 하나 대 N 개.
Hint
컴포넌트별 버전은 cleanup 과 pause 에서 가장 눈에 띄게 실패해: 컴포넌트 하나 unmount 하면 그 socket 이 실제로 닫혀? 앱을 pause 하면 모든 컴포넌트의 연결을 잡았어? 소유자 하나가 둘 다 사소하게 만들어 — close() 하나, stop 하나 — 관리할 연결이 애초에 하나뿐이었으니까.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.