Skip to content

๐Ÿชต 6. ์›น์†Œ์ผ“ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋ถ„์„ ๋ฐ ๊ณต์œ 

Taeyeon Yoon edited this page Dec 5, 2024 · 1 revision

๊ฐœ์š”

์›น์†Œ์ผ“ ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„์„ ๋ถ„์„ํ•˜๊ณ  ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•œ ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ฒŒ์ž„์„ ์œ„ํ•œ ์•ˆ์ •์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น์†Œ์ผ“ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

โ”œโ”€โ”€ stores/socket/          # ์†Œ์ผ“ ๊ด€๋ จ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚   โ”œโ”€โ”€ socket.config.ts    # ์†Œ์ผ“ ์„ค์ • ๋ฐ ํƒ€์ž…
โ”‚   โ”œโ”€โ”€ socket.store.ts     # ์ „์—ญ ์†Œ์ผ“ ๊ด€๋ฆฌ store
โ”‚   โ”œโ”€โ”€ gameSocket.store.ts # ๊ฒŒ์ž„ ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚   โ””โ”€โ”€ chatSocket.store.ts # ์ฑ„ํŒ… ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚
โ”œโ”€โ”€ hooks/socket/           # ์†Œ์ผ“ ๊ด€๋ จ ์ปค์Šคํ…€ ํ›…
โ”‚   โ”œโ”€โ”€ useGameSocket.ts    # ๊ฒŒ์ž„ ์†Œ์ผ“ ํ›…
โ”‚   โ”œโ”€โ”€ useDrawingSocket.ts # ๋“œ๋กœ์ž‰ ์†Œ์ผ“ ํ›…
โ”‚   โ””โ”€โ”€ useChatSocket.ts    # ์ฑ„ํŒ… ์†Œ์ผ“ ํ›…
โ”‚
โ””โ”€โ”€ handlers/socket/        # ์†Œ์ผ“ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
    โ”œโ”€โ”€ gameSocket.handler.ts
    โ”œโ”€โ”€ drawingSocket.handler.ts
    โ””โ”€โ”€ chatSocket.handler.ts

์ด ๊ธ€์€ ์›น์†Œ์ผ“ ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„๊ธฐ: React ํ™˜๊ฒฝ์—์„œ ํšจ์œจ์ ์ธ ์›น์†Œ์ผ“ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ „์ œ๋กœ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„ ๊ธ€์—์„œ ์„ค๋ช…ํ•œ ๊ณ„์ธตํ˜• ์•„ํ‚คํ…์ฒ˜๋ฅผ ์‹ค์ œ ์ฝ”๋“œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ๊ฐ ๊ณ„์ธต์˜ ์‹ค์ œ ๊ตฌํ˜„ ๋ฐฉ์‹๊ณผ ํ•ด๊ฒฐํ•œ ๊ธฐ์ˆ ์  ๊ณผ์ œ๋“ค์„ ์ค‘์ ์ ์œผ๋กœ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค. drawing ๋“ฑ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์„ ๊ฐ€์ง„ ์†Œ์ผ“๋“ค์€ ํ˜„์žฌ ๋‹ค ๊ตฌ์กฐ๊ฐ€ ๊ฐ™์•„ ๋„๋ฉ”์ธ ์ƒํƒœ ๊ด€๋ฆฌ, ์‘๋‹ต ์ด๋ฒคํŠธ ๋“ฑ๋ก ์ปค์Šคํ…€ ํ›…, ์š”์ฒญ ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ฒŒ์ž„ ๋„๋ฉ”์ธ๋งŒ ์˜ˆ์‹œ๋ฅผ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ์„ค๋ช…

1. Socket Config(๊ธฐ๋ณธ ์„ค์ • ๊ณ„์ธต): socket.config.ts

Socket Config ๊ณ„์ธต์€ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ์˜ ๊ธฐ๋ณธ ์„ค์ •๊ณผ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ํ•ต์‹ฌ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

์ด ๊ณ„์ธต์—์„œ๋Š” ์†Œ์ผ“ ์—ฐ๊ฒฐ์— ํ•„์š”ํ•œ ๋ชจ๋“  ๊ธฐ๋ณธ ์„ค์ •๊ฐ’๊ณผ ํƒ€์ž… ์ •์˜๋ฅผ ์ค‘์•™ํ™”ํ•ด ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Socket Store ๋“ฑ์—์„œ ์‚ฌ์šฉํ•  ์„ค์ •๋“ค์˜ ์›์ฒœ์ด ๋˜๊ณ , Socket.IO ํด๋ผ์ด์–ธํŠธ์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ๊ฒฐ์ •ํ•˜๋Š” ๊ธฐ์ค€์ด ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  1. ์„ค์ •์˜ ์ค‘์•™ํ™”

    • ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•œ ์„œ๋ฒ„ URL ๊ด€๋ฆฌ
    • ์žฌ์—ฐ๊ฒฐ ์ •์ฑ… ํ†ตํ•ฉ ๊ด€๋ฆฌ
    • ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ณ„ ๊ฒฝ๋กœ ์ •์˜
  2. ํƒ€์ž… ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜

    • ์—ด๊ฑฐํ˜•์„ ํ†ตํ•œ ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๊ด€๋ฆฌ
    • const assertion์„ ํ†ตํ•œ ์„ค์ •๊ฐ’ ํƒ€์ž… ์ถ”๋ก 
    • ํƒ€์ž… ์•ˆ์ „ํ•œ ์„ค์ • ์ ‘๊ทผ
  3. ๋ช…์‹œ์ ์ธ ์—ฐ๊ฒฐ ์ •์ฑ…

    BASE_OPTIONS: {
      autoConnect: false,// ๋ช…์‹œ์ ์ธ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
      reconnection: true,// ์ž๋™ ์žฌ์—ฐ๊ฒฐ ํ™œ์„ฑํ™”
      reconnectionAttempts: 5,// ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜
      reconnectionDelay: 1000,// ์žฌ์—ฐ๊ฒฐ ๊ฐ„๊ฒฉ(ms)
    }
  4. ์œ ์ง€๋ณด์ˆ˜์„ฑ

    • ์„ค์ •๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ๋‹จ์ผ ์ง€์  ์ˆ˜์ •
    • ๊ณตํ†ต ์„ค์ •์˜ ์ค‘๋ณต ์ œ๊ฑฐ
    • ์„ค์ • ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์  ์šฉ์ด
  5. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ

    • ์ƒˆ๋กœ์šด ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ถ”๊ฐ€ ์šฉ์ด
    • ์„ค์ • ์˜ต์…˜ ํ™•์žฅ ๊ฐ€๋Šฅ
    • Socket.IO ์˜ต์…˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ง€์›

1.1 ๊ธฐ๋ณธ ์„ค์ • ๊ตฌ์กฐํ™”

// ์›น์†Œ์ผ“ ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ •์˜
export enum SocketNamespace {
  GAME = 'game',
  DRAWING = 'drawing',
  CHAT = 'chat',
}

// ๊ธฐ๋ณธ ์„ค์ • ์ƒ์ˆ˜ํ™”
export const SOCKET_CONFIG = {
  URL: import.meta.env.VITE_SOCKET_URL || '<http://localhost:3000>',
  BASE_OPTIONS: {
    autoConnect: false,  // ์ˆ˜๋™ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
    reconnection: true,  // ์ž๋™ ์žฌ์—ฐ๊ฒฐ ํ™œ์„ฑํ™”
    reconnectionAttempts: 5,
    reconnectionDelay: 1000,
  },
  PATHS: {
    [SocketNamespace.GAME]: '/game',
    [SocketNamespace.DRAWING]: '/drawing',
    [SocketNamespace.CHAT]: '/chat',
  },
} as const;

1.2 ํƒ€์ž… ์•ˆ์ „์„ฑ ๊ฐ•ํ™”

// ์ธ์ฆ ์ •๋ณด ํƒ€์ž… ์ •์˜
export interface SocketAuth {
  roomId: string;
  playerId: string;
}

// ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ณ„ ์ธ์ฆ ์š”๊ตฌ์‚ฌํ•ญ
export const NAMESPACE_AUTH_REQUIRED: Record<SocketNamespace, boolean> = {
  [SocketNamespace.GAME]: false,    // ์ดˆ๊ธฐ ์—ฐ๊ฒฐ์€ ์ธ์ฆ ๋ถˆํ•„์š”
  [SocketNamespace.DRAWING]: true,  // ๋“œ๋กœ์ž‰/์ฑ„ํŒ…์€ ์ธ์ฆ ํ•„์ˆ˜
  [SocketNamespace.CHAT]: true,
} as const;

// ํƒ€์ž… ๋งคํ•‘
type SocketType = GameSocket | DrawingSocket | ChatSocket;
type NamespaceSocketMap = {
  [SocketNamespace.GAME]: GameSocket;
  [SocketNamespace.DRAWING]: DrawingSocket;
  [SocketNamespace.CHAT]: ChatSocket;
};

1.3 ์†Œ์ผ“ ์ƒ์„ฑ ๋กœ์ง

const createSocket = <T extends SocketType>(
  namespace: SocketNamespace,
  auth?: SocketAuth
): T => {
  const options = auth
    ? { ...SOCKET_CONFIG.BASE_OPTIONS, auth }
    : SOCKET_CONFIG.BASE_OPTIONS;

  return io(
    `${SOCKET_CONFIG.URL}${SOCKET_CONFIG.PATHS[namespace]}`,
    options
  ) as T;
};

export const socketCreators = {
  [SocketNamespace.GAME]: () =>
    createSocket<GameSocket>(SocketNamespace.GAME),
  [SocketNamespace.DRAWING]: (auth) =>
    createSocket<DrawingSocket>(SocketNamespace.DRAWING, auth),
  [SocketNamespace.CHAT]: (auth) =>
    createSocket<ChatSocket>(SocketNamespace.CHAT, auth),
};

2. Socket Store: ์ „์—ญ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ๊ณ„์ธต

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“  WebSocket ์—ฐ๊ฒฐ์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

Zustand๋ฅผ ์‚ฌ์šฉํ•ด ๊ตฌํ˜„ํ–ˆ๊ณ , ๊ฐ ๋„ค์ž„์ŠคํŽ˜์ด์Šค(๊ฒŒ์ž„, ๋“œ๋กœ์ž‰, ์ฑ„ํŒ…)๋ณ„ ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค์™€ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ์ถ”์ ํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ณ„์ธต์€ ์†Œ์ผ“ ์—ฐ๊ฒฐ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ฑ…์ž„์ ธ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ์•ˆ์ •์ ์ด๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ๋‹ค๋ฅธ ๊ณ„์ธต์—์„œ ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  1. ์ƒํƒœ ๊ตฌ์กฐ

    sockets: {
      game: Socket | null,     // ๊ฒŒ์ž„ ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค
      drawing: Socket | null,  // ๋“œ๋กœ์ž‰ ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค
      chat: Socket | null      // ์ฑ„ํŒ… ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค
    }
    • ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ณ„ ์†Œ์ผ“ ์ธ์Šคํ„ด์Šค ๊ด€๋ฆฌ
    • ์—ฐ๊ฒฐ ์ƒํƒœ์˜ ์‹ค์‹œ๊ฐ„ ์ถ”์ 
    • null ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ํƒ€์ž… ์„ค๊ณ„
  2. ์—ฐ๊ฒฐ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ

    connect: (namespace, auth?) => {
      // ์ค‘๋ณต ์—ฐ๊ฒฐ ๋ฐฉ์ง€
      if (currentSocket?.connected) return;
    
      // ์ธ์ฆ ๊ฒ€์ฆ
      if (NAMESPACE_AUTH_REQUIRED[namespace] && !auth) {
        console.error(`Auth required for ${namespace}`);
        return;
      }
    
      // ์†Œ์ผ“ ์ƒ์„ฑ ๋ฐ ์—ฐ๊ฒฐ
      const socket = socketCreators[namespace](auth);
      socket.connect();
    }
    • ์—ฐ๊ฒฐ ์ค‘๋ณต ๋ฐฉ์ง€
    • ์ธ์ฆ ์š”๊ตฌ์‚ฌํ•ญ ์ž๋™ ๊ฒ€์ฆ
    • ์—ฐ๊ฒฐ ์ƒํƒœ ์ž๋™ ์—…๋ฐ์ดํŠธ
  3. ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ

    socket.on('connect', () => {
      set(state => ({
        connected: { ...state.connected, [namespace]: true }
      }));
    });
    
    socket.on('disconnect', () => {
      set(state => ({
        connected: { ...state.connected, [namespace]: false }
      }));
    });
    • ์—ฐ๊ฒฐ/ํ•ด์ œ ์ด๋ฒคํŠธ ์ž๋™ ์ฒ˜๋ฆฌ
    • ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์ž๋™ํ™”
    • ์ผ๊ด€๋œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง
  4. ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ

    disconnect: (namespace) => {
      const socket = get().sockets[namespace];
      if (socket) {
        socket.disconnect();
        // ์ƒํƒœ ์ •๋ฆฌ
      }
    }
    • ๋ช…์‹œ์ ์ธ ์—ฐ๊ฒฐ ํ•ด์ œ
    • ๋ฆฌ์†Œ์Šค ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
    • ํด๋ฆฐ์—… ์ •๋ฆฌ ์ž๋™ํ™”
  5. ํ™•์žฅ์„ฑ

    • ์ƒˆ๋กœ์šด ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ถ”๊ฐ€ ์šฉ์ด
    • ๊ธฐ์กด ๊ธฐ๋Šฅ ์ˆ˜์ • ์—†์ด ๊ธฐ๋Šฅ ํ™•์žฅ ๊ฐ€๋Šฅ
    • ํƒ€์ž… ์‹œ์Šคํ…œ์„ ํ†ตํ•œ ์•ˆ์ „ํ•œ ํ™•์žฅ

Zustand Store ๊ตฌํ˜„: socket.store.ts

interface SocketState {
  sockets: Record<SocketNamespace, Socket | null>;
  connected: Record<SocketNamespace, boolean>;
  actions: {
    connect: (namespace: SocketNamespace, auth?: SocketAuth) => void;
    disconnect: (namespace: SocketNamespace) => void;
    disconnectAll: () => void;
  };
}

export const useSocketStore = create<SocketState>((set, get) => ({
  sockets: {
    [SocketNamespace.GAME]: null,
    [SocketNamespace.DRAWING]: null,
    [SocketNamespace.CHAT]: null,
  },
  connected: {
    [SocketNamespace.GAME]: false,
    [SocketNamespace.DRAWING]: false,
    [SocketNamespace.CHAT]: false,
  },
  actions: {
    connect: (namespace, auth?) => {
      // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ์†Œ์ผ“์€ ์žฌ์‚ฌ์šฉ
      const currentSocket = get().sockets[namespace];
      if (currentSocket?.connected) return;

      // ์ธ์ฆ ์š”๊ตฌ์‚ฌํ•ญ ๊ฒ€์ฆ
      if (NAMESPACE_AUTH_REQUIRED[namespace] && !auth) {
        console.error(`Auth required for ${namespace}`);
        return;
      }

      const socket = socketCreators[namespace](auth);

      // ์—ฐ๊ฒฐ ์ƒํƒœ ๊ด€๋ฆฌ
      socket.on('connect', () => {
        set(state => ({
          connected: {
            ...state.connected,
            [namespace]: true
          }
        }));
      });

      socket.on('disconnect', () => {
        set(state => ({
          connected: {
            ...state.connected,
            [namespace]: false
          }
        }));
      });

      socket.connect();

      set(state => ({
        sockets: {
          ...state.sockets,
          [namespace]: socket
        }
      }));
    },

    disconnect: (namespace) => {
      const socket = get().sockets[namespace];
      if (socket) {
        socket.disconnect();
        set(state => ({
          sockets: {
            ...state.sockets,
            [namespace]: null
          },
          connected: {
            ...state.connected,
            [namespace]: false
          }
        }));
      }
    },

    disconnectAll: () => {
      Object.values(SocketNamespace).forEach(namespace => {
        get().actions.disconnect(namespace);
      });
    }
  }
}));

์ฐธ๊ณ : disconnectAll์€ ํ˜„์žฌ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ ์ถ”ํ›„ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๋Œ€๋น„ํ•ด ๋ฏธ๋ฆฌ ๊ตฌํ˜„ํ•ด๋†“์€ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

3. Domain Store(๋„๋ฉ”์ธ๋ณ„ ์ƒํƒœ ๊ด€๋ฆฌ ๊ณ„์ธต)

Domain Store ๊ณ„์ธต์€ ๊ฐ ๋„๋ฉ”์ธ(๊ฒŒ์ž„, ๋“œ๋กœ์ž‰, ์ฑ„ํŒ…)๋ณ„ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ณ„์ธต์ž…๋‹ˆ๋‹ค. Socket Store๊ฐ€ ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌํ•œ๋‹ค๋ฉด, Domain Store๋Š” ํ•ด๋‹น ์†Œ์ผ“์„ ํ†ตํ•ด ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ์™€ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ฒŒ์ž„ ๋„๋ฉ”์ธ์„ ์˜ˆ์‹œ๋กœ ๋“ค๋ฉด, ๋ฐฉ ์ •๋ณด, ํ”Œ๋ ˆ์ด์–ด ๋ชฉ๋ก, ๊ฒŒ์ž„ ์„ค์ • ๋“ฑ ๊ฒŒ์ž„ ์ง„ํ–‰์— ํ•„์š”ํ•œ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.drawing ๋“ฑ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์„ ๊ฐ€์ง„ ์†Œ์ผ“๋“ค์€ ํ˜„์žฌ ๋‹ค ๊ตฌ์กฐ๊ฐ€ ๊ฐ™์•„ ๋„๋ฉ”์ธ ์ƒํƒœ ๊ด€๋ฆฌ, ์ปค์Šคํ…€ ํ›…, ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ฒŒ์ž„ ๋„๋ฉ”์ธ๋งŒ ์˜ˆ์‹œ๋ฅผ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  1. ๋„๋ฉ”์ธ๋ณ„ ์ƒํƒœ ๊ตฌ์กฐํ™”

    interface GameState {
      room: Room | null;          // ํ˜„์žฌ ๊ฒŒ์ž„๋ฐฉ ์ •๋ณด
      roomSettings: RoomSettings | null;  // ๊ฒŒ์ž„ ์„ค์ •
      players: Player[];          // ์ฐธ๊ฐ€์ž ๋ชฉ๋ก
      currentPlayerId: string | null;  // ํ˜„์žฌ ํ”Œ๋ ˆ์ด์–ด ์‹๋ณ„์ž
    }
    • ๋„๋ฉ”์ธ ํŠนํ™”๋œ ์ƒํƒœ ์ •์˜
    • null ์•ˆ์ „์„ฑ ๊ณ ๋ ค
    • ๋ช…ํ™•ํ•œ ํƒ€์ž… ์ •์˜
  2. ์•ก์…˜ ์ค‘์‹ฌ ์ƒํƒœ ๊ด€๋ฆฌ

    interface GameActions {
      updateRoom: (room: Room) => void;
      updatePlayers: (players: Player[]) => void;
      removePlayer: (playerId: string) => void;
      reset: () => void;
    }
    • ๋ช…์‹œ์ ์ธ ์•ก์…˜ ์ •์˜
    • ๋‹จ์ผ ์ฑ…์ž„์„ ๊ฐ€์ง„ ํ•จ์ˆ˜๋“ค
    • ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๋ณ€ํ™”
  3. ์ดˆ๊ธฐ ์ƒํƒœ ๋ถ„๋ฆฌ

    const initialState: GameState = {
      room: null,
      roomSettings: null,
      players: [],
      currentPlayerId: null,
    };
    • ์ƒํƒœ ์ดˆ๊ธฐํ™” ์šฉ์ด
    • ๋ฆฌ์…‹ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ„ํŽธํ™”
    • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ
  4. ๋””๋ฒ„๊น… ์ง€์›

    export const useGameSocketStore = create<GameState & { actions: GameActions }>()(
      devtools(
        (set) => ({
          // store ๊ตฌํ˜„
        }),
        { name: 'GameSocketStore' }
      )
    );
    • Redux DevTools ํ†ตํ•ฉ
    • ์ƒํƒœ ๋ณ€ํ™” ์ถ”์ 
    • ์•ก์…˜ ํžˆ์Šคํ† ๋ฆฌ ํ™•์ธ
  5. ๋ถˆ๋ณ€์„ฑ ๋ณด์žฅ

    removePlayer: (playerId) => set(state => ({
      players: state.players.filter(p => p.playerId !== playerId)
    }))
    • ์ƒํƒœ ์—…๋ฐ์ดํŠธ์˜ ์•ˆ์ „์„ฑ
    • ์ฐธ์กฐ ํˆฌ๋ช…์„ฑ ์œ ์ง€
    • ๋ฆฌ๋ Œ๋”๋ง ์ตœ์ ํ™”

gameSocket.store.ts ๊ตฌํ˜„

interface GameState {
  room: Room | null;
  roomSettings: RoomSettings | null;
  players: Player[];
  currentPlayerId: string | null;
}

interface GameActions {
  updateRoom: (room: Room) => void;
  updateRoomSettings: (settings: RoomSettings) => void;
  updatePlayers: (players: Player[]) => void;
  updateCurrentPlayerId: (currentPlayerId: string) => void;
  removePlayer: (playerId: string) => void;
  reset: () => void;
}

const initialState: GameState = {
  room: null,
  roomSettings: null,
  players: [],
  currentPlayerId: null,
};

export const useGameSocketStore = create<GameState & { actions: GameActions }>()(
  devtools(
    (set) => ({
      ...initialState,
      actions: {
        updateRoom: (room) => set({ room }),
        updateRoomSettings: (settings) =>
          set({ roomSettings: settings }),
        updatePlayers: (players) => set({ players }),
        updateCurrentPlayerId: (currentPlayerId) =>
          set({ currentPlayerId }),
        removePlayer: (playerId) =>
          set(state => ({
            players: state.players.filter(
              p => p.playerId !== playerId
            )
          })),
        reset: () => set(initialState)
      }
    }),
    { name: 'GameSocketStore' }
  )
);

4. Custom Hooks(์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ณ„์ธต)

Custom Hooks ๊ณ„์ธต์€ ์ปดํฌ๋„ŒํŠธ์™€ WebSocket ๋กœ์ง์„ ์—ฐ๊ฒฐํ•˜๋Š” ๋ธŒ๋ฆฟ์ง€ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. Socket Store์™€ Domain Store์˜ ๊ธฐ๋Šฅ์„ ํ†ตํ•ฉํ•ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ํŠนํžˆ ๋ณต์žกํ•œ ์†Œ์ผ“ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ, ์žฌ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง์„ ์ถ”์ƒํ™”ํ•ด ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ๋‹จ์ˆœํžˆ ํ›…์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  1. ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋ฉ”์ปค๋‹ˆ์ฆ˜

    typescript
    Copy
    // ์žฌ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
    const savedPlayerId = playerIdStorageUtils.getPlayerId(roomId);
    if (savedPlayerId) {
      gameSocketHandlers
        .reconnect({ playerId: savedPlayerId, roomId })
        .catch(error => {
          console.error('Reconnection failed:', error);
          playerIdStorageUtils.removePlayerId(roomId);
        });
    } else {
      playerIdStorageUtils.removeAllPlayerIds();
      gameSocketHandlers.joinRoom({ roomId }).catch(console.error);
    }
    • localStorage๋ฅผ ํ†ตํ•œ ์„ธ์…˜ ์œ ์ง€
    • ์‹คํŒจ ์‹œ ์ž๋™ ํด๋ฐฑ
    • ์—๋Ÿฌ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜
  2. ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ค‘์•™ํ™”

    const handlers = {
      joinedRoom: (response: JoinRoomResponse) => {
        const { room, roomSettings, players, playerId } = response;
        gameActions.updateRoom(room);
        gameActions.updateRoomSettings(roomSettings);
        gameActions.updatePlayers(players);
        if (playerId) {
          playerIdStorageUtils.setPlayerId(roomId, playerId);
          gameActions.updateCurrentPlayerId(playerId);
        }
      },
    // ... ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค
    };
    
    // ์ผ๊ด„ ๋“ฑ๋ก
    Object.entries(handlers).forEach(([event, handler]) => {
      socket.on(event, handler);
    });
    // ์ผ๊ด„ ํ•ด์ œ
    return () => {
      Object.entries(handlers).forEach(([event, handler]) => {
        socket.off(event, handler);
      });
    };
    • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ†ตํ•ฉ ๊ด€๋ฆฌ, ๋“ฑ๋ก ๋ฐ ํ•ด์ œ ๋กœ์ง ๋“ฑ๋ก
    • ์ผ๊ด€๋œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
    • ์ฝ”๋“œ ์ค‘๋ณต ๋ฐฉ์ง€
  3. ์ž์› ๊ด€๋ฆฌ

return () => {
  socketActions.disconnect(SocketNamespace.GAME);
  playerIdStorageUtils.removePlayerId(roomId);
};
  • ์ž๋™ ์ •๋ฆฌ(cleanup)
  • ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
  • ์ƒํƒœ ์ดˆ๊ธฐํ™”
  1. ์ปดํฌ๋„ŒํŠธ ์ธํ„ฐํŽ˜์ด์Šค ๋‹จ์ˆœํ™”

    // ์ปดํฌ๋„ŒํŠธ์—์„œ์˜ ์‚ฌ์šฉ
    const GameRoom = () => {
      const { isConnected, actions } = useGameSocket();
    
      if (!isConnected) {
        return <LoadingSpinner />;
      }
    
      return <GameUI />;
    };
    • ์„ ์–ธ์  ์ธํ„ฐํŽ˜์ด์Šค
    • ์—ฐ๊ฒฐ ์ƒํƒœ ์ž๋™ ๊ด€๋ฆฌ
    • ๊ฐ„๋‹จํ•œ ์•ก์…˜ ํ˜ธ์ถœ
  2. ์ƒํƒœ ํ†ตํ•ฉ

    // store๋“ค์˜ ํ†ตํ•ฉ
    const { sockets, connected, actions: socketActions } = useSocketStore();
    const { actions: gameActions } = useGameSocketStore();
    • ์—ฌ๋Ÿฌ store์˜ ํ†ตํ•ฉ
    • ๋‹จ์ผ ์ ‘๊ทผ์  ์ œ๊ณต
    • ์ƒํƒœ ๋™๊ธฐํ™” ๋ณด์žฅ

useGameSocket.ts ๊ตฌํ˜„

export const useGameSocket = () => {
  const { roomId } = useParams<{ roomId: string }>();
  const { sockets, connected, actions: socketActions } = useSocketStore();
  const { actions: gameActions } = useGameSocketStore();

  useEffect(() => {
    if (!roomId) return;

    // ์†Œ์ผ“ ์—ฐ๊ฒฐ ์„ค์ •
    socketActions.connect(SocketNamespace.GAME);

    // ์žฌ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
    const savedPlayerId = playerIdStorageUtils.getPlayerId(roomId);
    if (savedPlayerId) {
      gameSocketHandlers
        .reconnect({ playerId: savedPlayerId, roomId })
        .catch(error => {
          console.error('Reconnection failed:', error);
          playerIdStorageUtils.removePlayerId(roomId);
        });
    } else {
      playerIdStorageUtils.removeAllPlayerIds();
      gameSocketHandlers.joinRoom({ roomId }).catch(console.error);
    }

    // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
    const socket = sockets.game;
    if (!socket) return;

    const handlers = {
      joinedRoom: (response: JoinRoomResponse) => {
        const { room, roomSettings, players, playerId } = response;
        gameActions.updateRoom(room);
        gameActions.updateRoomSettings(roomSettings);
        gameActions.updatePlayers(players);
        if (playerId) {
          playerIdStorageUtils.setPlayerId(roomId, playerId);
          gameActions.updateCurrentPlayerId(playerId);
        }
      },
      playerJoined: (response: JoinRoomResponse) => {
        const { room, roomSettings, players } = response;
        gameActions.updateRoom(room);
        gameActions.updateRoomSettings(roomSettings);
        gameActions.updatePlayers(players);
      },
      playerLeft: (response: PlayerLeftResponse) => {
        const { leftPlayerId, players } = response;
        gameActions.removePlayer(leftPlayerId);
        gameActions.updatePlayers(players);
      }
    };

    Object.entries(handlers).forEach(([event, handler]) => {
      socket.on(event, handler);
    });

    return () => {
      socketActions.disconnect(SocketNamespace.GAME);
      playerIdStorageUtils.removePlayerId(roomId);
    };
  }, [roomId]);

  return {
    isConnected: connected.game,
    actions: gameActions
  };
};

Socket Handlers(์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ณ„์ธต)

Socket Handlers ๊ณ„์ธต์€ ์„œ๋ฒ„๋กœ์˜ ์ด๋ฒคํŠธ ๋ฐœ์‹ ์„ ๋‹ด๋‹นํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋“ค์˜ ๋ชจ์Œ์ž…๋‹ˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ๋‚˜ Custom Hooks์—์„œ ์„œ๋ฒ„๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉ๋˜๊ณ , Promise ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ•ธ๋“ค๋Ÿฌ๋Š” ํ•˜๋‚˜์˜ ํŠน์ • ์ด๋ฒคํŠธ ๋ฐœ์‹ ์„ ๋‹ด๋‹นํ•˜๊ณ , ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ํƒ€์ž… ๊ฒ€์ฆ์„ ์ผ๊ด€๋˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  • Promise ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ๊ตฌํ˜„

  • ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๋ฐœ์‹ 

  • Socket Store์™€์˜ ๊ธด๋ฐ€ํ•œ ํ†ตํ•ฉ, ์ „์—ญ ์ƒํƒœ ์ ‘๊ทผ

    const socket = useSocketStore.getState().sockets.game;
  • ์ค‘์•™ํ™”๋œ ์š”์ฒญ ๊ด€๋ฆฌ

  • ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํ†ตํ•ฉ

    const socket = useSocketStore.getState().sockets.game;
    if (!socket) throw new Error('Socket not connected');

gameSocket.handler.ts ๊ตฌํ˜„

import type { 
  JoinRoomRequest, 
  JoinRoomResponse, 
  ReconnectRequest 
} from '@troublepainter/core';
import { useSocketStore } from '@/stores/socket/socket.store';

export const gameSocketHandlers = {
  joinRoom: (request: JoinRoomRequest): Promise<void> => {
    const socket = useSocketStore.getState().sockets.game;
    if (!socket) throw new Error('Socket not connected');

    return new Promise(() => {
      socket.emit('joinRoom', request);
    });
  },

  reconnect: (request: ReconnectRequest): Promise<void> => {
    const socket = useSocketStore.getState().sockets.game;
    if (!socket) throw new Error('Socket not connected');

    return new Promise(() => {
      socket.emit('reconnect', request);
    });
  },

  // ์˜ˆ์‹œ: ์‘๋‹ต ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ฝœ๋ฐฑ์ด ํ•„์š”ํ•œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
  updatePlayerStatus: async (request: ReadyRequest): Promise<void> => {
    const socket = useSocketStore.getState().sockets.game;
    if (!socket) throw new Error('Socket not connected');

    return new Promise((resolve, reject) => {
      socket.emit('updatePlayerStatus', request, (error?: SocketError) => {
        if (error) {
          reject(error);
        } else {
          resolve();
        }
      });
    });
  },
};

export type GameSocketHandlers = typeof gameSocketHandlers;

ํ˜„์žฌ ์šฐ๋ฆฌ ์›น์†Œ์ผ“ ์„œ๋ฒ„ ํŠน์„ฑ์ƒ ์š”์ฒญ ์‹œ ์‘๋‹ต์ด ์˜ค์ง€ ์•Š๊ณ , ์—๋Ÿฌ๋Š” error ์ด๋ฒคํŠธ๋กœ๋งŒ ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์š”์ฒญ ํ›„ ์‘๋‹ต์ด๋‚˜ ์—๋Ÿฌ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์ง€ ์•Š์•„ ์ด๋Ÿฐ ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

  1. ๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ: ๊ณ„์ธตํ™”๋œ ๊ตฌ์กฐ
    • Store โ†’ Hooks โ†’ Handlers ์˜ ๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ
    • ์„ค์ •, ์ƒํƒœ ๊ด€๋ฆฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋ถ„๋ฆฌ๋จ
    • ๊ฐ ๊ณ„์ธต์ด ๋‹จ์ผ ์ฑ…์ž„์„ ๊ฐ€์ง
    • ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•œ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ
  2. ํƒ€์ž… ์•ˆ์ „์„ฑ
    • TypeScript์™€ [Socket.IO](http://socket.io/) ํƒ€์ž… ํ†ตํ•ฉ
    • ๊ณต์œ  ํƒ€์ž… ์ •์˜๋ฅผ ํ†ตํ•œ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ์ผ๊ด€์„ฑ
  3. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ
    • ์ƒˆ๋กœ์šด ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ถ”๊ฐ€ ์šฉ์ด
    • ๊ณตํ†ต ๋กœ์ง์˜ ์ถ”์ƒํ™” + ์„ค์ •์˜ ์ค‘์•™ํ™”๋กœ ์žฌ์‚ฌ์šฉ์„ฑ ์ฆ๊ฐ€
    • ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์ตœ์†Œํ™”
  4. ์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต
    • zustand๋ฅผ ํ™œ์šฉํ•œ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๊ด€๋ฆฌ
    • ๋„๋ฉ”์ธ๋ณ„ ๋…๋ฆฝ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ
    • devtools ํ†ตํ•ฉ์œผ๋กœ ๋””๋ฒ„๊น… ์šฉ์ด
  5. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    • ์ค‘์•™ํ™”๋œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง, error ์ด๋ฒคํŠธ๋กœ๋งŒ ์ฒ˜๋ฆฌ
    • ์žฌ์—ฐ๊ฒฐ ๋ฉ”์ปค๋‹ˆ์ฆ˜
    • ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ํ†ตํ•ฉ

๐Ÿ˜Ž ์›จ๋ฒ ๋ฒ ๋ฒ ๋ฒฑ

๐Ÿ‘ฎ๐Ÿป ํŒ€ ๊ทœ์น™

๐Ÿ’ป ํ”„๋กœ์ ํŠธ

๐Ÿชต ์›จ๋ฒ ๋ฒฑ ๊ธฐ์ˆ ๋กœ๊ทธ

๐Ÿช„ ๋ฐ๋ชจ ๊ณต์œ 

๐Ÿ”„ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก

๐Ÿ“— ํšŒ์˜๋ก

Clone this wiki locally