React와 Recoil을 사용하여 실시간 게임 화면 및 채팅 UI/UX를 구현했습니다.
프론트엔드의 가장 큰 도전 과제는 Pong 게임의 상태(공의 위치, 플레이어의 점수 등)와 채팅 메시지를 여러 클라이언트 간에 실시간으로 동기화하는 것이었습니다. 웹소켓을 통해 서버로부터 수신되는 데이터를 지연 없이 UI에 반영하고, 모든 사용자가 일관된 화면을 보도록 보장해야 했습니다. 이 과정에서 발생하는 성능 저하와 상태 불일치 문제를 해결하는 것이 핵심이었습니다.
전역 상태 관리를 위해 Recoil을 도입하여 게임 상태와 채팅 상태를 원자적(Atomic)으로 관리했습니다. Socket.io 클라이언트를 통해 서버 이벤트를 수신하는 중앙 핸들러를 구현하고, 이 핸들러가 직접 Recoil의 atom 상태를 업데이트하도록 설계했습니다. 이 아키텍처를 통해 데이터의 흐름을 단방향으로 유지하고, 상태 변경이 필요한 컴포넌트만 효율적으로 리렌더링할 수 있었습니다.
[그림 1] Socket.io와 Recoil을 연동한 프론트엔드 데이터 흐름
JWT 인증을 통해 소켓 연결을 관리하고, `useEffect` 훅 내에서 `roomList`, `offerBattle`, `acceptBattle` 등 다양한 게임 이벤트를 수신하여 Recoil 상태를 업데이트하는 커스텀 훅(`useGameSocket`)을 구현했습니다. 이를 통해 복잡한 실시간 이벤트 처리 로직을 중앙에서 관리하고 컴포넌트의 복잡도를 낮췄습니다.
// 예시: Socket 이벤트 수신 및 Recoil 상태 업데이트 훅import * as ioClient from "socket.io-client";export const gameSocket = ioClient.io(/backend/game, {autoConnect: false,reconnection: true,reconnectionAttempts: 5,reconnectionDelay: 1000,reconnectionDelayMax: 5000,});export const gameSocketConnect = (jwt: string) => {if (gameSocket.disconnected) {gameSocket.io.opts.extraHeaders = {Authorization: Bearer accessToken,};gameSocket.connect();}};import { useEffect } from 'react';import { useSetRecoilState } from 'recoil';import { gameStateAtom } from '../atoms/gameState';import { socket } from '../socket';const useGameSocket = () => {useEffect(() => {if (!jwt) {gameSocket.disconnect();} else {// console.log("gamesocket connected");gameSocket.off("roomList");gameSocket.on("roomList", (data: GameRoomInfoType[]) => {setGameRoomList(data);if (data.find((room) => room.roomURL === gameRoomURL) !== undefined) {setGameRoomInfo((prevInfo) => ({...prevInfo,...data.find((room) => room.roomURL === gameRoomURL),}));}});gameSocket.off("offerBattle");gameSocket.on("offerBattle", (data: OfferGameType) => {console.log(data, userData);if (data.awayUser.id === userData.id) {setBattleActionModal({battleActionModal: true,awayUser: data.myData,gameRoomURL: data.gameRoomURL,});}});gameSocket.off("acceptBattle");gameSocket.on("acceptBattle", (data) => {console.log("acceptBattle", data, userData);if (data.user1Id === userData.id || data.user2Id === userData.id) {setGameRoomInfo(data.gameRoom);setGameRoomURL(data.gameRoomURL);setGameRoomChatList([]);window.location.href = /game/gameRoomURL;}});gameSocket.off("rejectBattle");gameSocket.on("rejectBattle", (data) => {if (data.awayUserId === userData.id) {setGameAlertModal({gameAlertModal: true,gameAlertModalMessage: "상대방이 대전 신청을 거절했습니다.",shouldRedirect: false,shouldInitInfo: true,});setGameRoomChatList([]);setGameRoomInfo(data.gameRoom);}});gameSocket.on("startGame", (data) => {if (data.gameRoomURL !== gameRoomURL) return;setGameModal({gameMap: "NORMAL" as GameMapType,});});gameSocketConnect(jwt);}};
Recoil 기반의 상태 관리 아키텍처를 통해 지연 시간이 짧은 실시간 멀티플레이어 게임 경험을 성공적으로 구현했습니다. 상태 관리 로직과 UI 컴포넌트를 명확하게 분리함으로써 코드의 재사용성과 유지보수성을 높였습니다. 이 프로젝트를 통해 복잡한 실시간 웹 애플리케이션의 프론트엔드 구조를 설계하고 최적화하는 실전 역량을 확보했습니다.