-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 6. ์น์์ผ ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ถ์ ๋ฐ ๊ณต์
์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ์ ๋ถ์ํ๊ณ ๊ณต์ ํ๊ธฐ ์ํ ๋ฌธ์์ ๋๋ค. ์ค์๊ฐ ๋ฉํฐํ๋ ์ด์ด ๊ฒ์์ ์ํ ์์ ์ ์ด๊ณ ํ์ฅ ๊ฐ๋ฅํ ์น์์ผ ์ํคํ ์ฒ๋ฅผ ์๋์ ๊ฐ์ด ๊ตฌ์ฑํ์ต๋๋ค.
โโโ 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
๋ฑ ๋ค๋ฅธ ๋๋ฉ์ธ์ ๊ฐ์ง ์์ผ๋ค์ ํ์ฌ ๋ค ๊ตฌ์กฐ๊ฐ ๊ฐ์ ๋๋ฉ์ธ ์ํ ๊ด๋ฆฌ, ์๋ต ์ด๋ฒคํธ ๋ฑ๋ก ์ปค์คํ
ํ
, ์์ฒญ ํธ๋ค๋ฌ๋ ๊ฒ์ ๋๋ฉ์ธ๋ง ์์๋ฅผ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
Socket Config ๊ณ์ธต์ ์น์์ผ ์ฐ๊ฒฐ์ ๊ธฐ๋ณธ ์ค์ ๊ณผ ํ์ ์ ์ ์ํ๋ ํต์ฌ ๊ณ์ธต์ ๋๋ค.
์ด ๊ณ์ธต์์๋ ์์ผ ์ฐ๊ฒฐ์ ํ์ํ ๋ชจ๋ ๊ธฐ๋ณธ ์ค์ ๊ฐ๊ณผ ํ์ ์ ์๋ฅผ ์ค์ํํด ๊ด๋ฆฌํฉ๋๋ค. Socket Store ๋ฑ์์ ์ฌ์ฉํ ์ค์ ๋ค์ ์์ฒ์ด ๋๊ณ , Socket.IO ํด๋ผ์ด์ธํธ์ ๋์ ๋ฐฉ์์ ๊ฒฐ์ ํ๋ ๊ธฐ์ค์ด ๋ฉ๋๋ค.
-
์ค์ ์ ์ค์ํ
- ํ๊ฒฝ๋ณ์๋ฅผ ํตํ ์๋ฒ URL ๊ด๋ฆฌ
- ์ฌ์ฐ๊ฒฐ ์ ์ฑ ํตํฉ ๊ด๋ฆฌ
- ๋ค์์คํ์ด์ค๋ณ ๊ฒฝ๋ก ์ ์
-
ํ์ ์์คํ ๊ธฐ๋ฐ
- ์ด๊ฑฐํ์ ํตํ ๋ค์์คํ์ด์ค ๊ด๋ฆฌ
-
const
assertion์ ํตํ ์ค์ ๊ฐ ํ์ ์ถ๋ก - ํ์ ์์ ํ ์ค์ ์ ๊ทผ
-
๋ช ์์ ์ธ ์ฐ๊ฒฐ ์ ์ฑ
BASE_OPTIONS: { autoConnect: false,// ๋ช ์์ ์ธ ์ฐ๊ฒฐ ๊ด๋ฆฌ reconnection: true,// ์๋ ์ฌ์ฐ๊ฒฐ ํ์ฑํ reconnectionAttempts: 5,// ์ต๋ ์ฌ์๋ ํ์ reconnectionDelay: 1000,// ์ฌ์ฐ๊ฒฐ ๊ฐ๊ฒฉ(ms) }
-
์ ์ง๋ณด์์ฑ
- ์ค์ ๊ฐ ๋ณ๊ฒฝ ์ ๋จ์ผ ์ง์ ์์
- ๊ณตํต ์ค์ ์ ์ค๋ณต ์ ๊ฑฐ
- ์ค์ ๋ณ๊ฒฝ ์ด๋ ฅ ์ถ์ ์ฉ์ด
-
ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ
- ์๋ก์ด ๋ค์์คํ์ด์ค ์ถ๊ฐ ์ฉ์ด
- ์ค์ ์ต์ ํ์ฅ ๊ฐ๋ฅ
- Socket.IO ์ต์ ์ปค์คํฐ๋ง์ด์ง ์ง์
// ์น์์ผ ๋ค์์คํ์ด์ค ์ ์
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;
// ์ธ์ฆ ์ ๋ณด ํ์
์ ์
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;
};
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),
};
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ WebSocket ์ฐ๊ฒฐ์ ์ค์์์ ๊ด๋ฆฌํ๋ ์ ์ญ ์ํ ๊ด๋ฆฌ ๊ณ์ธต์ ๋๋ค.
Zustand๋ฅผ ์ฌ์ฉํด ๊ตฌํํ๊ณ , ๊ฐ ๋ค์์คํ์ด์ค(๊ฒ์, ๋๋ก์, ์ฑํ )๋ณ ์์ผ ์ธ์คํด์ค์ ์ฐ๊ฒฐ ์ํ๋ฅผ ์ถ์ ํ๊ณ ๊ด๋ฆฌํฉ๋๋ค.
์ด ๊ณ์ธต์ ์์ผ ์ฐ๊ฒฐ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ฑ ์์ ธ ์ค์๊ฐ ํต์ ์ ๊ธฐ๋ฐ์ด ๋๋ ์์ผ ์ฐ๊ฒฐ์ ์์ ์ ์ด๊ณ ์์ธก ๊ฐ๋ฅํ ๋ฐฉ์์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ํ, ๋ค๋ฅธ ๊ณ์ธต์์ ์์ผ ์ธ์คํด์ค๋ฅผ ์์ ํ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ๋ณด์ฅํฉ๋๋ค.
-
์ํ ๊ตฌ์กฐ
sockets: { game: Socket | null, // ๊ฒ์ ์์ผ ์ธ์คํด์ค drawing: Socket | null, // ๋๋ก์ ์์ผ ์ธ์คํด์ค chat: Socket | null // ์ฑํ ์์ผ ์ธ์คํด์ค }
- ๋ค์์คํ์ด์ค๋ณ ์์ผ ์ธ์คํด์ค ๊ด๋ฆฌ
- ์ฐ๊ฒฐ ์ํ์ ์ค์๊ฐ ์ถ์
-
null
์์ ์ฑ์ ์ํ ํ์ ์ค๊ณ
-
์ฐ๊ฒฐ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ
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(); }
- ์ฐ๊ฒฐ ์ค๋ณต ๋ฐฉ์ง
- ์ธ์ฆ ์๊ตฌ์ฌํญ ์๋ ๊ฒ์ฆ
- ์ฐ๊ฒฐ ์ํ ์๋ ์ ๋ฐ์ดํธ
-
์ด๋ฒคํธ ์ฒ๋ฆฌ
socket.on('connect', () => { set(state => ({ connected: { ...state.connected, [namespace]: true } })); }); socket.on('disconnect', () => { set(state => ({ connected: { ...state.connected, [namespace]: false } })); });
- ์ฐ๊ฒฐ/ํด์ ์ด๋ฒคํธ ์๋ ์ฒ๋ฆฌ
- ์ํ ์ ๋ฐ์ดํธ ์๋ํ
- ์ผ๊ด๋ ์ด๋ฒคํธ ํธ๋ค๋ง
-
๋ฆฌ์์ค ๊ด๋ฆฌ
disconnect: (namespace) => { const socket = get().sockets[namespace]; if (socket) { socket.disconnect(); // ์ํ ์ ๋ฆฌ } }
- ๋ช ์์ ์ธ ์ฐ๊ฒฐ ํด์
- ๋ฆฌ์์ค ๋์ ๋ฐฉ์ง
- ํด๋ฆฐ์ ์ ๋ฆฌ ์๋ํ
-
ํ์ฅ์ฑ
- ์๋ก์ด ๋ค์์คํ์ด์ค ์ถ๊ฐ ์ฉ์ด
- ๊ธฐ์กด ๊ธฐ๋ฅ ์์ ์์ด ๊ธฐ๋ฅ ํ์ฅ ๊ฐ๋ฅ
- ํ์ ์์คํ ์ ํตํ ์์ ํ ํ์ฅ
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
์ ํ์ฌ ์ฌ์ฉํ์ง ์์ง๋ง ์ถํ ์ฌ์ฉํ ๊ฒ์ ๋๋นํด ๋ฏธ๋ฆฌ ๊ตฌํํด๋์ ์ํ์
๋๋ค.
Domain Store ๊ณ์ธต์ ๊ฐ ๋๋ฉ์ธ(๊ฒ์, ๋๋ก์, ์ฑํ )๋ณ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ณ์ธต์ ๋๋ค. Socket Store๊ฐ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋ค๋ฉด, Domain Store๋ ํด๋น ์์ผ์ ํตํด ์ฃผ๊ณ ๋ฐ๋ ๋ฐ์ดํฐ์ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
๊ฒ์ ๋๋ฉ์ธ์ ์์๋ก ๋ค๋ฉด, ๋ฐฉ ์ ๋ณด, ํ๋ ์ด์ด ๋ชฉ๋ก, ๊ฒ์ ์ค์ ๋ฑ ๊ฒ์ ์งํ์ ํ์ํ ์ํ๋ฅผ ์ ์ฅํ๊ณ ์
๋ฐ์ดํธํฉ๋๋ค.drawing
๋ฑ ๋ค๋ฅธ ๋๋ฉ์ธ์ ๊ฐ์ง ์์ผ๋ค์ ํ์ฌ ๋ค ๊ตฌ์กฐ๊ฐ ๊ฐ์ ๋๋ฉ์ธ ์ํ ๊ด๋ฆฌ, ์ปค์คํ
ํ
, ํธ๋ค๋ฌ๋ ๊ฒ์ ๋๋ฉ์ธ๋ง ์์๋ฅผ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
-
๋๋ฉ์ธ๋ณ ์ํ ๊ตฌ์กฐํ
interface GameState { room: Room | null; // ํ์ฌ ๊ฒ์๋ฐฉ ์ ๋ณด roomSettings: RoomSettings | null; // ๊ฒ์ ์ค์ players: Player[]; // ์ฐธ๊ฐ์ ๋ชฉ๋ก currentPlayerId: string | null; // ํ์ฌ ํ๋ ์ด์ด ์๋ณ์ }
- ๋๋ฉ์ธ ํนํ๋ ์ํ ์ ์
-
null
์์ ์ฑ ๊ณ ๋ ค - ๋ช ํํ ํ์ ์ ์
-
์ก์ ์ค์ฌ ์ํ ๊ด๋ฆฌ
interface GameActions { updateRoom: (room: Room) => void; updatePlayers: (players: Player[]) => 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) => ({ // store ๊ตฌํ }), { name: 'GameSocketStore' } ) );
- Redux DevTools ํตํฉ
- ์ํ ๋ณํ ์ถ์
- ์ก์ ํ์คํ ๋ฆฌ ํ์ธ
-
๋ถ๋ณ์ฑ ๋ณด์ฅ
removePlayer: (playerId) => set(state => ({ players: state.players.filter(p => p.playerId !== playerId) }))
- ์ํ ์ ๋ฐ์ดํธ์ ์์ ์ฑ
- ์ฐธ์กฐ ํฌ๋ช ์ฑ ์ ์ง
- ๋ฆฌ๋ ๋๋ง ์ต์ ํ
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' }
)
);
Custom Hooks ๊ณ์ธต์ ์ปดํฌ๋ํธ์ WebSocket ๋ก์ง์ ์ฐ๊ฒฐํ๋ ๋ธ๋ฆฟ์ง ์ญํ ์ ํฉ๋๋ค. Socket Store์ Domain Store์ ๊ธฐ๋ฅ์ ํตํฉํด ์ปดํฌ๋ํธ์์ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
ํนํ ๋ณต์กํ ์์ผ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ, ์ฌ์ฐ๊ฒฐ ์ฒ๋ฆฌ, ์ด๋ฒคํธ ํธ๋ค๋ง์ ์ถ์ํํด ์ปดํฌ๋ํธ์์๋ ๋จ์ํ ํ ์ ์ฌ์ฉํ๋ ๊ฒ๋ง์ผ๋ก ์ค์๊ฐ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๋๋ก ํฉ๋๋ค.
-
์๋ ์ฌ์ฐ๊ฒฐ ๋ฉ์ปค๋์ฆ
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๋ฅผ ํตํ ์ธ์ ์ ์ง
- ์คํจ ์ ์๋ ํด๋ฐฑ
- ์๋ฌ ๋ณต๊ตฌ ๋ฉ์ปค๋์ฆ
-
์ด๋ฒคํธ ํธ๋ค๋ฌ ์ค์ํ
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); }); };
- ์ด๋ฒคํธ ํธ๋ค๋ฌ ํตํฉ ๊ด๋ฆฌ, ๋ฑ๋ก ๋ฐ ํด์ ๋ก์ง ๋ฑ๋ก
- ์ผ๊ด๋ ์ํ ์ ๋ฐ์ดํธ
- ์ฝ๋ ์ค๋ณต ๋ฐฉ์ง
-
์์ ๊ด๋ฆฌ
return () => {
socketActions.disconnect(SocketNamespace.GAME);
playerIdStorageUtils.removePlayerId(roomId);
};
- ์๋ ์ ๋ฆฌ(cleanup)
- ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
- ์ํ ์ด๊ธฐํ
-
์ปดํฌ๋ํธ ์ธํฐํ์ด์ค ๋จ์ํ
// ์ปดํฌ๋ํธ์์์ ์ฌ์ฉ const GameRoom = () => { const { isConnected, actions } = useGameSocket(); if (!isConnected) { return <LoadingSpinner />; } return <GameUI />; };
- ์ ์ธ์ ์ธํฐํ์ด์ค
- ์ฐ๊ฒฐ ์ํ ์๋ ๊ด๋ฆฌ
- ๊ฐ๋จํ ์ก์ ํธ์ถ
-
์ํ ํตํฉ
// store๋ค์ ํตํฉ const { sockets, connected, actions: socketActions } = useSocketStore(); const { actions: gameActions } = useGameSocketStore();
- ์ฌ๋ฌ store์ ํตํฉ
- ๋จ์ผ ์ ๊ทผ์ ์ ๊ณต
- ์ํ ๋๊ธฐํ ๋ณด์ฅ
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 ๊ณ์ธต์ ์๋ฒ๋ก์ ์ด๋ฒคํธ ๋ฐ์ ์ ๋ด๋นํ๋ ์์ ํจ์๋ค์ ๋ชจ์์ ๋๋ค.
์ปดํฌ๋ํธ๋ 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');
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
์ด๋ฒคํธ๋ก๋ง ์ค๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์์ฒญ ํ ์๋ต์ด๋ ์๋ฌ๋ฅผ ๊ด๋ฆฌํ๊ณ ์์ง ์์ ์ด๋ฐ ํํ๋ก ๋ง๋ค์์ต๋๋ค.
-
๋ช
ํํ ์ฑ
์ ๋ถ๋ฆฌ: ๊ณ์ธตํ๋ ๊ตฌ์กฐ
- Store โ Hooks โ Handlers ์ ๋ช ํํ ์ฑ ์ ๋ถ๋ฆฌ
- ์ค์ , ์ํ ๊ด๋ฆฌ, ๋น์ฆ๋์ค ๋ก์ง์ด ๋ถ๋ฆฌ๋จ
- ๊ฐ ๊ณ์ธต์ด ๋จ์ผ ์ฑ ์์ ๊ฐ์ง
- ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๋ฅผ ํตํ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ํฅ์
-
ํ์
์์ ์ฑ
- TypeScript์ [Socket.IO](http://socket.io/) ํ์ ํตํฉ
- ๊ณต์ ํ์ ์ ์๋ฅผ ํตํ ํด๋ผ์ด์ธํธ-์๋ฒ ์ผ๊ด์ฑ
-
ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ
- ์๋ก์ด ๋ค์์คํ์ด์ค ์ถ๊ฐ ์ฉ์ด
- ๊ณตํต ๋ก์ง์ ์ถ์ํ + ์ค์ ์ ์ค์ํ๋ก ์ฌ์ฌ์ฉ์ฑ ์ฆ๊ฐ
- ๊ธฐ์กด ์ฝ๋ ์์ ์ต์ํ
-
์ํ ๊ด๋ฆฌ ์ ๋ต
- zustand๋ฅผ ํ์ฉํ ์์ธก ๊ฐ๋ฅํ ์ํ ๊ด๋ฆฌ
- ๋๋ฉ์ธ๋ณ ๋ ๋ฆฝ์ ์ธ ์ํ ๊ด๋ฆฌ
- devtools ํตํฉ์ผ๋ก ๋๋ฒ๊น ์ฉ์ด
-
์๋ฌ ์ฒ๋ฆฌ
- ์ค์ํ๋ ์๋ฌ ํธ๋ค๋ง,
error
์ด๋ฒคํธ๋ก๋ง ์ฒ๋ฆฌ - ์ฌ์ฐ๊ฒฐ ๋ฉ์ปค๋์ฆ
- ์ฌ์ฉ์ ํผ๋๋ฐฑ ํตํฉ
- ์ค์ํ๋ ์๋ฌ ํธ๋ค๋ง,
- 1. ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ๋ฐ ํ๋ก์ ํธ ๋ฌธ์ํ
- 2. ์ค์๊ฐ ํต์
- 3. ์ธํ๋ผ ๋ฐ CI/CD
- 4. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด Canvas ๊ตฌํํ๊ธฐ
- 5. ์บ๋ฒ์ค ๋๊ธฐํ๋ฅผ ์ํ ์์ CRDT ๊ตฌํ๊ธฐ
-
6. ์ปดํฌ๋ํธ ํจํด๋ถํฐ ์น์์ผ๊น์ง, ํจ์จ์ ์ธ FE ์ค๊ณ
- ์ข์ ์ปดํฌ๋ํธ๋ ๋ฌด์์ธ๊ฐ? + Headless Pattern
- ํจ์จ์ ์ธ UI ์ปดํฌ๋ํธ ์คํ์ผ๋ง: Tailwind CSS + cn.ts
- Tailwind CSS๋ก ๋์์ธ ์์คํ ๋ฐ UI ์ปดํฌ๋ํธ ์ธํ
- ์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ๊ธฐ: React ํ๊ฒฝ์์ ํจ์จ์ ์ธ ์น์์ผ ์ํคํ ์ฒ
- ์น์์ผ ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ถ์ ๋ฐ ๊ณต์
- 7. ํธ๋ฌ๋ธ ์ํ ๋ฐ ์ฑ๋ฅ/UX ๊ฐ์
- 1์ฃผ์ฐจ ๊ธฐ์ ๊ณต์
- 2์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 3์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 4์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 5์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- WEEK 06 ์ฃผ๊ฐ ๊ณํ
- WEEK 06 ๋ฐ์ผ๋ฆฌ ์คํฌ๋ผ
- WEEK 06 ์ฃผ๊ฐ ํ๊ณ