-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 6. ์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ๊ธฐ: React ํ๊ฒฝ์์ ํจ์จ์ ์ธ ์น์์ผ ์ํคํ ์ฒ
React์ ์ ์ธ์ ์ด๊ณ ๋จ๋ฐฉํฅ์ ์ธ ๋ฐ์ดํฐ ํ๋ฆ์ ์ค์๊ฐ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ WebSocket๊ณผ ์์ฐ์ค๋ฝ๊ฒ ์ด์ฐ๋ฌ์ง๊ธฐ ์ด๋ ต์ต๋๋ค. ๊ตฌ์ฒด์ ์ธ ๋ฌธ์ ์ ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
// โ ์ผ๋ฐ์ ์ผ๋ก ์์ฃผ ๋ณด์ด๋ ์๋ชป๋ ๊ตฌํ
const Component = () => {
useEffect(() => {
const socket = io('server-url');
socket.on('event', handleEvent);
return () => socket.disconnect();
}, []);
// ๋ฌธ์ ์ :
// 1. ์ปดํฌ๋ํธ ๋ง์ดํธ๋ง๋ค ์๋ก์ด ์ฐ๊ฒฐ ์์ฑ
// 2. ํ์ด์ง ์ ํ์ ์ฐ๊ฒฐ ๋๊น
// 3. ๋ค์ ์ปดํฌ๋ํธ์์ ์ค๋ณต ์ฐ๊ฒฐ ๊ฐ๋ฅ์ฑ
};
// โ Context API๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ์ ๋ฌธ์ ์
const SocketProvider = ({ children }) => {
const [gameState, setGameState] = useState({});
const [chatState, setChatState] = useState({});
const [drawingState, setDrawingState] = useState({});
useEffect(() => {
socket.on('gameUpdate', setGameState);
socket.on('chatUpdate', setChatState);
socket.on('drawingUpdate', setDrawingState);
// ๋ฌธ์ ์ :
// 1. Provider๊ฐ ๊ฑฐ๋ํด์ง
// 2. ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐ์
// 3. ์ํ ์
๋ฐ์ดํธ ๋ก์ง์ด ํ ๊ณณ์ ์ง์ค
}, []);
};
์ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ 4๊ณ์ธต ์ํคํ ์ฒ๋ฅผ ์ค๊ณํ์ต๋๋ค.
graph TD
A[Socket Config] --> B[Socket Store]
B --> C[Domain Store]
C --> D[Custom Hooks]
C --> E[Handlers]
style A fill:#f9f,stroke:#333
style B fill:#bbf,stroke:#333
style C fill:#bfb,stroke:#333
style D fill:#fb9,stroke:#333
-
๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ
- Socket ๋ก์ง์ 4๊ฐ์ง ํต์ฌ ์์ญ์ผ๋ก ๋ถ๋ฆฌ
- ์ํ ๊ด๋ฆฌ: Store์ ์ก์ ์ ํตํ ์์ ์ํ ๊ด๋ฆฌ
- ์ฐ๊ฒฐ ๊ด๋ฆฌ: ์์ผ ์ฐ๊ฒฐ/์ฌ์ฐ๊ฒฐ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ
- ์์ฒญ ์ฒ๋ฆฌ: ์น์์ผ ์์ฒญ ํธ๋ค๋ฌ ๊ตฌํ
- ์๋ต ์ฒ๋ฆฌ: ์น์์ผ ์ด๋ฒคํธ ๊ตฌ๋ ๋ฐ ์ฒ๋ฆฌ
- ๊ฐ ์์ญ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ ๋ฆฝ์ ์ธ ๋ชจ๋๋ก ๊ตฌ์ฑ
- Socket ๋ก์ง์ 4๊ฐ์ง ํต์ฌ ์์ญ์ผ๋ก ๋ถ๋ฆฌ
-
๋จ์ผ ์ฑ ์ ์์น
- Socket Store
- Domain Store
- Custom Hooks + Handlers
-
ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์ค๊ณ
enum SocketNamespace { GAME = 'game', DRAWING = 'drawing', CHAT = 'chat', }
- ๋ค์์คํ์ด์ค๋ฅผ ํตํ ๊ธฐ๋ฅ๋ณ ์์ผ ๋ถ๋ฆฌ
- ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ์ฉ์ดํ ๋ชจ๋๋ฌ ๊ตฌ์กฐ
- ๊ฐ ๋๋ฉ์ธ์ ๋ ๋ฆฝ์ ์ธ ํ์ฅ ๊ฐ๋ฅ
- ๋๋ฉ์ธ๋ณ ๊ฐ๋ณ ์ค์ผ์ผ๋ง ์ง์
์ด๋ฌํ ๊ณ์ธตํ ๊ตฌ์กฐ๋ ๋ณต์กํ ์ค์๊ฐ ๊ธฐ๋ฅ์ ๊ด๋ฆฌํ๊ธฐ ์ฝ๊ฒ ๋ง๋ค๋ฉฐ, ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๊ธฐ์กด ๊ธฐ๋ฅ ์์ ์ด ์ฉ์ดํ ํ์ฅ ๊ฐ๋ฅํ ์ํคํ ์ฒ๋ฅผ ์ ๊ณตํฉ๋๋ค.
์์ผ ์ค์ ์ ๋ด์ ๊ณ์ธต์ ๋๋ค.
// socket.config.ts
export const SOCKET_CONFIG = {
URL: import.meta.env.VITE_SOCKET_URL || '<http://localhost:3000>',
PATHS: {
[SocketNamespace.GAME]: '/game',
[SocketNamespace.DRAWING]: '/drawing',
[SocketNamespace.CHAT]: '/chat',
},
BASE_OPTIONS: {
autoConnect: false,
reconnection: true,
reconnectionAttempts: 5,
}
};
์ฌ์ฉํ ์์ผ ์ธ์คํด์ค์ ์ฐ๊ฒฐ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ด์ ๊ฐ ์์ผ์ ์ํ๋ฅผ ํ์ ํ๊ณ , ๊ณตํต ์์ผ ์ก์ ์ ์ ์ํ ๊ณ์ธต์ ๋๋ค.
// socket.store.ts
export const useSocketStore = create<SocketState>((set) => ({
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 socket = socketCreators[namespace](auth);
socket.connect();
set(state => ({
sockets: { ...state.sockets, [namespace]: socket }
}));
},
// ...
}
}));
๋๋ฉ์ธ๋ณ ์ํ ๊ด๋ฆฌ ๊ณ์ธต์ ๋๋ค. ๊ฒ์, ๋๋ก์, ์ฑํ ์์ผ์ ๋ถ์ฐ๋ฌผ์ธ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ณ์ธต์ ๋๋ค.
// gameSocket.store.ts
export const useGameSocketStore = create<GameState & GameActions>()(
devtools(
(set) => ({
room: null,
players: [],
actions: {
updateRoom: (room) => set({ room }),
updatePlayers: (players) => set({ players }),
}
}),
{ name: 'GameStore' }
)
);
์ฐ๊ฒฐ ๋ก์ง + ์๋ต ์ด๋ฒคํธ ๋ฑ๋ก์ ๋ด์ Custom Hooks์ ์์ฒญ ์ด๋ฒคํธ ํจ์๋ฅผ ๋ด์๋ธ Handlers๋ฅผ ๊ตฌํํ ๊ณ์ธต์ ๋๋ค.
์ปดํฌ๋ํธ ์์ค์ ์์ผ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํด ํธ์ถ๋ง์ผ๋ก ์ปดํฌ๋ํธ์ ์ฝ๊ฒ ์ฐ๋ํ๋๋ก ๊ณ์ธต์ ๊ตฌํํ์ต๋๋ค.
// useGameSocket.ts
export const useGameSocket = () => {
const { roomId } = useParams();
const { sockets, connected, actions: socketActions } = useSocketStore();
const { actions: gameActions } = useGameSocketStore();
useEffect(() => {
if (!roomId) return;
// 1. ์์ผ ์ฐ๊ฒฐ
socketActions.connect(SocketNamespace.GAME);
// 2. ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ค์
const handlers = {
joinedRoom: (response: JoinRoomResponse) => {
gameActions.updateRoom(response.room);
gameActions.updatePlayers(response.players);
},
// ...
};
Object.entries(handlers).forEach(([event, handler]) => {
socket.on(event, handler);
});
return () => {
// 3. ํด๋ฆฐ์
socketActions.disconnect(SocketNamespace.GAME);
};
}, [roomId]);
return {
isConnected: connected.game,
actions: gameActions,
};
};
import type { JoinRoomRequest, JoinRoomResponse, ReconnectRequest } from '@troublepainter/core';
import { useSocketStore } from '@/stores/socket/socket.store';
// socket ์์ฒญ๋ง ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ
export const gameSocketHandlers = {
joinRoom: (request: JoinRoomRequest): Promise<JoinRoomResponse> => {
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);
});
},
};
export type GameSocketHandlers = typeof gameSocketHandlers;
๋ ์ด์์์ ๊ณ์ธตํ ์ํคํ ์ฒ๋ฅผ ์ค์ ๋ก ์ ์ฉํด ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
// GameLayout.tsx
const GameLayout = () => {
const { isConnected } = useGameSocket();
// ์ฐ๊ฒฐ ์ํ์ ๋ฐ๋ฅธ UI ์ฒ๋ฆฌ
if (!isConnected) {
return <LoadingSpinner message="์ฐ๊ฒฐ ์ค..." />;
}
return (
<div className="flex min-h-screen flex-col">
<header>
<Logo variant="side" />
</header>
<main className="mx-auto">
<div className="flex">
{/* ํ๋ ์ด์ด ๋ชฉ๋ก */}
<PlayerList />
{/* ๊ฒ์ ์์ญ */}
<section className="flex-1">
<Outlet />
</section>
{/* ์ฑํ
์์ญ */}
<Chat />
</div>
</main>
</div>
);
};
-
์๋ช
์ฃผ๊ธฐ ๊ด๋ฆฌ ๋จ์ํ
- Layout ์์ค์์ ์์ผ ์ฐ๊ฒฐ ๊ด๋ฆฌ
- ํ์ ๋ผ์ฐํธ ์ ํ ์์๋ ์ฐ๊ฒฐ ์ ์ง
-
์ํ ๊ณต์ ์ต์ ํ
- ํ์ํ ์ปดํฌ๋ํธ๋ง ์ํ ๊ตฌ๋
- Props drilling ๋ฐฉ์ง
-
์ฌ์ฉ์ ๊ฒฝํ ํฅ์
- ์ฐ๊ฒฐ ์ํ์ ๋ฐ๋ฅธ ์ผ๊ด๋ UI
- ํ์ด์ง ์ ํ ์ ๋๊น์๋ ์ค์๊ฐ ๊ธฐ๋ฅ
์ด๋ฌํ ์ ๋ง์ ๊ณ์ธตํ ์ํคํ ์ฒ ์ค๊ณ์ ์ฃผ์ ์ด์ ์ ์๋์ ๊ฐ์ต๋๋ค.
- ์ ์ง๋ณด์์ฑ: ๊ฐ ๊ณ์ธต์ ์ญํ ์ด ๋ช ํํ๊ฒ ๋ถ๋ฆฌ๋์ด ์์ด ์ฝ๋ ๊ด๋ฆฌ๊ฐ ์ฉ์ด
- ์ฌ์ฌ์ฉ์ฑ: ์ปค์คํ ํ ์ ํตํด ์น์์ผ ๋ก์ง์ ์ฝ๊ฒ ์ฌ์ฌ์ฉ
- ํ์ฅ์ฑ: ์๋ก์ด ์์ผ ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ์ฉ์ดํ ๊ตฌ์กฐ
- ํ์ ์์ ์ฑ: TypeScript์ Socket.IO์ ํ์ ์์คํ ํ์ฉ
์ด ์ํคํ ์ฒ๋ ์ค์๊ฐ ๊ฒ์, ์ฑํ , ํ์ ๋๊ตฌ ๋ฑ WebSocket์ด ํ์ํ React ์ ํ๋ฆฌ์ผ์ด์ ์์ ํ์ฅ ๊ฐ๋ฅํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฝ๊ฒ ์๋๋ฅผ ๋ด์ ๋ง๋ค์์ต๋๋ค.
// โ ์ปดํฌ๋ํธ ๋ด ๋ก์ปฌ ์ํ ๊ด๋ฆฌ์ ๋ฌธ์ ์
const GameRoom = () => {
const [socket, setSocket] = useState<Socket | null>(null);
const [gameState, setGameState] = useState({});
const [connected, setConnected] = useState(false);
// ๋ฌธ์ ์ :
// 1. ์ํ ์ ํ๋ฅผ ์ํ Props Drilling
// 2. ์ปดํฌ๋ํธ ๊ฐ ์ํ ๋๊ธฐํ ์ด๋ ค์
// 3. ํ์ด์ง ์๋ก๊ณ ์นจ ์ ์ํ ์ ์ค
return <GameComponent socket={socket} gameState={gameState} />;
};
- Props Drilling: ์ฌ๋ฌ ๋จ๊ณ์ ์ปดํฌ๋ํธ๋ฅผ ๊ฑฐ์ณ ์์ผ๊ณผ ์ํ๋ฅผ ์ ๋ฌํด์ผ ํจ
- ์ํ ๋๊ธฐํ: ์ฌ๋ฌ ํ์ด์ง์์ ๋์ผํ ์์ผ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ ๋ ์ํ ๋๊ธฐํ๊ฐ ๋ณต์กํด์ง
- ์์์ฑ: ํ์ด์ง ์๋ก๊ณ ์นจ์ด๋ ๋ค๋น๊ฒ์ด์ ์ ์ํ ์ ์ง๊ฐ ์ด๋ ค์
- ๋๋ฒ๊น : ์ํ ๋ณํ๋ฅผ ์ถ์ ํ๊ณ ๋๋ฒ๊น ํ๊ธฐ ์ด๋ ค์
- ํ ์คํธ: ์ปดํฌ๋ํธ์ ์์ผ ๋ก์ง์ด ๊ฐํ๊ฒ ๊ฒฐํฉ๋์ด ํ ์คํธ๊ฐ ์ด๋ ค์
// โ Context API ์ฌ์ฉ ์์ ๋ฌธ์ ์
const SocketProvider = ({ children }) => {
const [gameState, setGameState] = useState({});
useEffect(() => {
// ๋ชจ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ํ ๊ณณ์ ์ง์ค
socket.on('gameStart', onGameStart);
socket.on('playerJoin', onPlayerJoin);
socket.on('drawing', onDrawing);
// ... ๊ณ์๋๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ค
}, []);
// ๋ฌธ์ ์ :
// 1. Provider๊ฐ ๋น๋ํด์ง
// 2. ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐ์
// 3. ๊ธฐ๋ฅ๋ณ ๋ถ๋ฆฌ๊ฐ ์ด๋ ค์
return (
<SocketContext.Provider value={{ socket, gameState }}>
{children}
</SocketContext.Provider>
);
};
- ์ด๋ฒคํธ ๊ด๋ฆฌ ๋ณต์ก์ฑ: ๋ง์ ์ด๋ฒคํธ๋ฅผ ํ ๊ณณ์์ ๊ด๋ฆฌํ๋ฉด์ ์ฝ๋๊ฐ ๋น๋ํด์ง
- ์ํ ์ ๋ฐ์ดํธ ์ต์ ํ: Context API ํน์ฑ์ Provider ์์์ ํน์ ์ด๋ฒคํธ๋ก ์ธํ ์ํ ์ ๋ฐ์ดํธ๊ฐ ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ์ ๋ฐ
- ์ฝ๋ ๋ถํ : ๊ธฐ๋ฅ๋ณ๋ก Context๋ฅผ ๋ถ๋ฆฌํ๋ฉด Provider ์ค์ฒฉ์ด ์ฌํด์ง
-
ํจ์จ์ ์ธ ์ํ ๊ตฌ๋ โ ย ํ์ํ ์ํ๋ง ์ ํ์ ์ผ๋ก ๊ตฌ๋
const Room = () => { // ํน์ ์ํ๋ง ๊ตฌ๋ ํ์ฌ ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง const room = useGameStore((state) => state.room); const updateRoom = useGameStore((state) => state.actions.updateRoom); useEffect(() => { socket.on('roomUpdate', updateRoom); }, []); };
-
๊ณ์ธตํ ๊ตฌ์กฐ์ ์ ํฉํ API โ ย ์คํ ์ด ๊ฐ ์ํ ๊ณต์ ๋ฐ ์กฐํฉ์ด ์์ ๋ก์
const useGameStore = create<GameState>()((set, get) => ({ room: null, players: [], actions: { // ๋ค๋ฅธ ์คํ ์ด์ ์ํ ์ฐธ์กฐ ๊ฐ๋ฅ updateRoom: (room) => { set({ room }); get().actions.syncWithSocket(room); } } }));
-
๊ฐ๋ฐ ์์ฐ์ฑ
- TypeScript ์ง์์ด ์ฐ์
- DevTools๋ฅผ ํตํ ์ํ ๋๋ฒ๊น
- ๋ฏธ๋ค์จ์ด๋ฅผ ํตํ ๊ธฐ๋ฅ ํ์ฅ
-
๋ฒ๋ค ํฌ๊ธฐ
- ์์ ๋ฒ๋ค ํฌ๊ธฐ (Redux: ~22KB, MobX: ~16KB, Zustand: ~1KB)
- ์ต์ํ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ
-
ํ์ต ๊ณก์ : โ ์ง๊ด์ ์ธ API
const useStore = create((set) => ({
socket: null,
connect: () => set({ socket: io() }),
disconnect: () => set({ socket: null })
}));
์ด๋ฌํ ์ด์ ๋ก Zustand๋ฅผ ์ฌ์ฉํ ๊ณ์ธตํ ์ํคํ ์ฒ๊ฐ WebSocket ํต์ ๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃฐ ์ ์๋ ์ต์ ์ ์ ํ์ด์์ต๋๋ค.
- 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 ์ฃผ๊ฐ ํ๊ณ