FC 프로클럽 커뮤니티 & 매칭 플랫폼
FootballSquare는 FC24 프로클럽 유저를 위한 온라인 축구팀 매칭 및 커뮤니티 플랫폼입니다. 팀 구성, 경기 매칭, 대회 운영, 커뮤니티 활동을 하나의 서비스에서 제공합니다.
FC 프로클럽은 FIFA 시리즈의 온라인 모드로, 실제 사용자들이 각자의 포지션을 맡아 팀을 이루어 경기하는 게임입니다. 이 플랫폼은 게임 경험을 웹으로 확장하여, 게임처럼 빠르고 반응적인 UI를 목표로 했습니다.
- 매치 생성·삭제·스코어 수정 시 0-latency 반응
- 실시간 채팅과 즉각적인 UI 피드백
- 토너먼트 브라켓의 인터랙티브한 시각화
- 팀 생성 및 프로필 관리
- 선수 로스터 구성
- 팀원 역할 및 권한 설정
- 팀 통계 및 전적 관리
- 자유 매치: 실시간 매칭 로비에서 상대팀 찾기
- 팀 매치: 일정 기반 경기 예약
- 경기 결과 및 선수별 스탯 기록
- 매치 히스토리 및 통계 분석
- 리그 및 토너먼트 형식 지원
- 인터랙티브 토너먼트 브라켓 (16강, 8강, 4강, 결승)
- 실시간 스코어 업데이트
- 대회 통계 및 순위표
- 커뮤니티 허브 생성 및 운영
- 자유게시판, 팀 게시판, 커뮤니티 게시판
- 댓글 및 좋아요 시스템
- 커뮤니티 스태프 관리
- Socket.io 기반 실시간 채팅
- 팀 채팅방
- 읽지 않은 메시지 알림
- 이메일 회원가입/로그인
- Discord OAuth 연동
- 자동 토큰 리프레시
- 영구 로그인 (Remember Me)
- React 18.3.1 - UI 라이브러리
- TypeScript 5.5.3 - 정적 타입 시스템
- Vite 5.4.1 - 빌드 도구
- Zustand 5.0.3 - 전역 상태 관리
- Axios 1.8.1 - HTTP 클라이언트
- Socket.io Client 4.8.1 - 실시간 통신
- Tailwind CSS 4.0.6 - 유틸리티 기반 스타일링
- React Hook Form 7.54.2 - 폼 상태 관리
- Yup 1.6.1 - 스키마 기반 유효성 검증
- React Router 7.1.5 - 클라이언트 사이드 라우팅
- react-cookie 7.2.2 - 쿠키 기반 인증
- crypto-js 4.2.0 - 로컬 스토리지 암호화
src/
├── 0_App/ # 앱 전역 설정
│ ├── index.tsx # 메인 앱 컴포넌트
│ ├── ui/Nav/ # 네비게이션 바
│ └── style/ # Tailwind 테마
│
├── 1_Page/ # 페이지 레벨 (라우트별)
│ ├── Championship/ # 대회 대시보드
│ ├── Team/ # 팀 프로필
│ ├── FreeMatch/ # 자유매치 로비
│ ├── Boards/ # 게시판 목록
│ └── Chat/ # 채팅
│
├── 2_Widget/ # 재사용 가능한 UI 위젯
│ ├── ChatWidget/ # 실시간 채팅 위젯
│ ├── MatchModal/ # 매치 상세 모달
│ ├── TeamSummaryCard/ # 팀 요약 카드
│ └── PlayerCard/ # 플레이어 카드
│
├── 3_Entity/ # 비즈니스 도메인 (API 레이어)
│ ├── Account/ # 인증 (21개 훅)
│ ├── Match/ # 매치 (19개 훅)
│ ├── Team/ # 팀 (18개 훅)
│ ├── Championship/ # 대회 (10개 훅)
│ ├── Board/ # 게시판 (12개 훅)
│ └── Chat/ # 채팅 (1개 훅)
│
└── 4_Shared/ # 공유 유틸리티
├── util/apiUtil.ts # 중앙 API 클라이언트
├── lib/useMyInfo.ts # 인증 상태 관리
├── zustand/ # Zustand 스토어
├── hookForm/ # 폼 컴포넌트
└── components/ # 기본 UI 컴포넌트
Page (비즈니스 로직 오케스트레이션)
↓
Widget (재사용 UI 컴포넌트)
↓
Entity (API 호출 훅)
↓
Shared (공통 유틸리티)
매치 생성·삭제·수정 시 즉각적인 UI 반영과 실패 시 자동 롤백을 구현했습니다.
const useGetChampionshipMatchListHandler = () => {
const originalListRef = useRef(matchList);
const backupRef = useRef(new Map());
const [optimisticMatchList, setOptimisticMatchList] = useState([]);
const handleDeleteMatch = (matchIdx) => {
backupRef.current.set(matchIdx, originalItem);
setOptimisticMatchList(prev => prev.filter(m => m.id !== matchIdx));
};
const handleRollback = (matchIdx) => {
const backup = backupRef.current.get(matchIdx);
setOptimisticMatchList(prev => /* 원래 위치에 삽입 */);
};
return { optimisticMatchList, handleDeleteMatch, handleRollback };
};Map을 사용해 개별 항목을 백업하고, 실패 시 원래 위치에 정확히 복원합니다. 사용자는 0ms 지연으로 즉각적인 피드백을 받습니다.
Socket.io 기반 실시간 채팅을 구현했습니다.
const useSocketHandler = () => {
const socketRef = useRef(null);
const [chatLog, setChatLog] = useState([]);
useEffect(() => {
const socket = io(API_URL, {
transports: ['websocket'],
auth: { token: accessToken }
});
socket.on('message', (data) => {
setChatLog(prev => [...prev, messageWithTimestamp]);
});
return () => socket.disconnect();
}, []);
const sendMessage = (content) => {
socketRef.current?.emit('message', { content });
};
return { chatLog, sendMessage };
};WebSocket 기반 양방향 통신으로 실시간 메시지를 주고받고, 연결 상태를 모니터링하며 자동 재연결을 처리합니다.
프로젝트를 시작하면서 의도적으로 React Query를 배제했습니다. React Query가 해결하는 문제를 직접 경험하고, 상태 관리와 캐싱의 근본적인 개념을 체득하기 위해서였습니다.
| 구현 내용 | 직접 구현 | React Query |
|---|---|---|
| 기본 데이터 페칭 | useFetchData (143줄) | useQuery (1줄) |
| Optimistic Update | useRef + Map + 동기화 (236줄) | onMutate (15줄) |
| 토큰 리프레시 | 전역 큐 + 인터셉터 (80줄) | 자동 retry |
| 캐시 무효화 | window.location.reload() | invalidateQueries (1줄) |
| 총 코드량 | 약 2000줄 | 약 100줄 |
React Query를 단순히 사용만 했다면 "편하네!"로 끝났을 것입니다. 하지만 직접 구현하면서 내부 동작 원리를 이해하게 되었습니다.
Optimistic UI의 원리: 백업 → 즉시 반영 → 동기화 → 롤백 사이클을 직접 구현하면서, React Query의 onMutate와 onError가 정확히 이 패턴을 추상화한 것임을 깨달았습니다.
토큰 관리의 복잡성: 동시 다발적 요청에서 토큰이 만료되면, 모든 요청이 401을 받습니다. 이를 해결하려면 Queue 패턴이 필요합니다. React Query는 이를 자동으로 처리합니다.
보일러플레이트의 고통: 80개의 훅을 작성하면서 같은 패턴을 반복했습니다. 로딩 상태 관리, 에러 처리, 재시도 로직 등이 모든 훅에 중복되었습니다.
캐시 무효화의 어려움: 데이터가 변경되면 관련된 모든 컴포넌트를 업데이트해야 합니다. 직접 구현할 때는 window.location.reload()로 해결했지만, React Query의 invalidateQueries가 얼마나 우아한 해결책인지 알게 되었습니다.
"도구를 진정으로 이해하는 방법은 그것 없이 살아보는 것이다."
직접 구현한 2000줄의 코드는 React Query 도입 후 100줄로 줄어들 것입니다. 하지만 이 과정이 없었다면, React Query가 왜 그렇게 동작하는지 진정으로 이해하지 못했을 것입니다.
React Query는 단순한 편의 라이브러리가 아닙니다. 서버 상태 관리의 근본적인 문제들을 해결하는 검증된 패턴의 집합입니다. 이를 직접 구현해보면서, 라이브러리의 진짜 가치를 이해하게 되었습니다.
# 의존성 설치
npm install
# 개발 서버 실행
npm run dev# 프로덕션 빌드
npm run build
# 빌드 미리보기
npm run preview
# 린트 검사
npm run lintVITE_SERVER_URL=your_api_server_url
VITE_API_URL=your_socket_server_url- React Query 도입: 검증된 패턴으로 코드 간소화
- 테스트 커버리지: Jest + React Testing Library
- 성능 최적화: Code splitting, lazy loading
- PWA 지원: 오프라인 사용 및 푸시 알림
이 프로젝트는 "라이브러리 없이 직접 구현해보기"라는 학습 여정의 결과물입니다. React Query의 진가를 이해하기 위해 돌아간 길이었지만, 그 과정에서 얻은 깊은 이해는 어떤 도구를 사용하든 더 나은 개발자가 되는 데 도움이 되었습니다.