혼자서 게임 생태계를 만들어버린 이야기
11월 15일 오픈, 3주 만에 115명 동접. 671명 유저, 40개 플러그인, 실시간 시스템까지 - 혼자 만든 Pharos 플랫폼 이야기
시작: 2024년 11월 15일
sa._.2394 — 2024-11-15 오전 5:25
Server is opened!
F1 -> connect 20.41.123.75:28015
이게 시작이었다. 새벽 5시에 서버 열고, 아무도 안 올 줄 알았다.
근데 사람이 오기 시작했다. 그리고 질문도 오기 시작했다.
3주 동안 일어난 일
| 날짜 | 이벤트 | 결과 | |------|--------|------| | 11.15 | 서버 오픈 | 첫 유저 입장 | | 11.18 | 칭호 시스템 22개 구현 | 유저 경쟁 시작 | | 11.19 | 웹사이트 배포 | svy04.github.io | | 11.21 | Wiki 시스템 도입 | 뉴비 이탈률 감소 | | 11.22 | 실시간 제재로그, 다국어 지원 | KR/JP 유저 유입 | | 11.24 | Wiki 14시간 작업 완료 | 12,000개 아이템 | | 11.28 | 유전자 계산기 배포 | 농부 유저 증가 | | 11.30 | 팀 파인더 시스템 | 솔로 유저 잔존율 증가 | | 12.04 | 레이드 계산기, 성능 최적화 | 로딩 3초→0.5초 | | 12.05 | MMR 티어 시스템 | 115명 동접 달성 | | 12.08 | Softopia 서버 오픈 | 2번째 서버 |
3주 만에 0명에서 115명 동접. 671명 누적 유저.
"유저들이 뭘 원하는지 모르겠어"
서버 열고 3일 됐을 때. 디스코드에서 매일 같은 질문이 올라왔다.
[유저1] 레이드 비용 어떻게 계산해요?
[유저2] 유전자 계산기 어디 있어요?
[유저3] 팀 구하는데 어디서 구해요?
[유저4] 이거 그 아이템 뭐예요?
처음엔 링크 복붙해서 알려줬다. rustlabs.com 가라고, 외국 계산기 쓰라고.
근데 계속 같은 질문이 올라왔다. 유저들이 귀찮아하는 거였다.
"직접 만들면 되지 않나?"
이게 삽질의 시작이었다.
최종 아키텍처
3주 만에 이렇게 됐다:
┌─────────────────────────────────────────────────────────┐
│ Pharos Platform │
├─────────────────────────────────────────────────────────┤
│ Frontend (React + TypeScript + Vite) │
│ ├── Wiki System (12,000 items, 10 categories) │
│ ├── Genetics Calculator (OCR + BFS + Web Worker) │
│ ├── Raid Calculator (1,235 lines data) │
│ ├── MMR Leaderboard (7 tiers, percentile-based) │
│ ├── Team Finder (Discord OAuth + GitHub API) │
│ └── i18n (KR/JP/EN) │
├─────────────────────────────────────────────────────────┤
│ Backend │
│ ├── Supabase (PostgreSQL + Realtime + Auth) │
│ ├── GitHub API (teams.json storage) │
│ └── Cloudflare (CDN + Workers) │
├─────────────────────────────────────────────────────────┤
│ Game Server (Azure VM) │
│ ├── Oxide/uMod Framework │
│ ├── 40+ Custom Plugins (C#) │
│ │ ├── SupabaseSync.cs (킬피드, 통계) │
│ │ ├── TitleSystem.cs (22개 칭호) │
│ │ ├── TeamTag.cs (팀 태그) │
│ │ └── ... │
│ └── Rust Dedicated Server │
├─────────────────────────────────────────────────────────┤
│ Discord Bot (discord.js) │
│ ├── /recruit → GitHub API → 웹사이트 자동 반영 │
│ ├── /delete (관리자) │
│ └── X 자동 홍보 (cron) │
└─────────────────────────────────────────────────────────┘
첫 번째 삽질: 레이드 계산기
제일 만만해 보였다. 숫자 넣으면 유황 나오는 거잖아.
// 첫 버전 - 3시간 만에 완성
function calculateSulfur(c4, rockets, satchels) {
return c4 * 2200 + rockets * 1400 + satchels * 480;
}
"오 쉽네?"
근데 유저 피드백이 왔다.
"근데 이거 벽 종류별로 다르지 않아요?" "문이랑 벽이랑 다른데요" "소프트사이드는요?"
아. 맞다. 러스트 건축물이 얼마나 많은지 까먹고 있었다.
나무벽, 돌벽, 금속벽, 강화벽, 나무문, 금속문, 강화문, TC, 자판기, 포탑, 창문... 각각 C4/로켓/폭발탄/접착폭탄 비용이 다 다르다.
결국 RustLabs 데이터를 일일이 긁어와서 JSON으로 정리했다. 350개 건축물 × 4개 폭발물 = 1400개 조합.
{
"stone_wall": {
"c4": 2,
"rocket": 4,
"satchel": 10,
"explosive_ammo": 185
},
"metal_door": {
"c4": 1,
"rocket": 2,
"satchel": 4,
"explosive_ammo": 63
},
// ... 348개 더
}
3시간 예상했던 게 2주 걸렸다.
두 번째 삽질: 유전자 계산기
이건 진짜 어려웠다. 러스트 농사 시스템이 생각보다 복잡했다.
문제:
- 클론 8개가 있다
- 각 클론은 6개의 유전자 슬롯이 있다 (예: GGHXWY)
- 두 클론을 교배하면 새 클론이 나온다
- 목표: GGGYYY (최적 조합) 만들기
처음엔 "그냥 전수조사하면 되겠네" 했다.
// 무식한 첫 번째 시도
for (let i = 0; i < clones.length; i++) {
for (let j = i + 1; j < clones.length; j++) {
const result = crossbreed(clones[i], clones[j]);
// ...
}
}
클론 8개면 28개 조합. 괜찮네?
근데 유저가 클론 20개 넣으니까 브라우저가 죽었다. 20개면 190개 조합, 거기서 나온 결과물끼리 또 교배하면 조합이 기하급수적으로 늘어난다.
결국 Web Worker로 분리하고, BFS 알고리즘으로 최단 경로 탐색하게 바꿨다.
// 메인 스레드가 죽지 않도록 Worker 분리
const worker = new Worker('genetics-worker.js');
worker.postMessage({ clones: userClones, target: 'GGGYYY' });
worker.onmessage = (e) => {
setResult(e.data);
};
이것도 2주 걸렸다.
"근데 이거 어떻게 묶지?"
레이드 계산기, 유전자 계산기, Wiki 다 따로 만들었다. 근데 문제가 생겼다.
- 각각 다른 도메인에 있음 (GitHub Pages 3개)
- 디자인이 다 다름
- 유저가 뭐가 뭔지 모름
"그 계산기 어디 있어요?" "어떤 계산기요?" "아 그거요 그 유황 나오는 거"
하나로 합쳐야겠다.
이때 Pharos라는 이름을 붙였다. 알렉산드리아의 등대. 러스트 플레이어들의 등대가 되자는 의미.
전체 구조 설계
┌─────────────────────────────────────────────────────────┐
│ PHAROS 생태계 │
├─────────────────────────────────────────────────────────┤
│ │
│ [게임 서버] [웹사이트] [Discord] │
│ │ │ │ │
│ │ 킬/접속 이벤트 │ 리더보드 │ 팀모집 │
│ │ │ 계산기 │ 알림 │
│ │ │ Wiki │ │
│ │ │ 통계 │ │
│ │ │ │ │
│ └───────────┬───────────┴────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ [Supabase] [Discord Bot] │
│ │ │ │
│ │ PostgreSQL │ │
│ │ + Realtime │ │
│ │ │ │
└───────────────────┴────────────────────┴─────────────────┘
중간에 Supabase를 넣은 게 신의 한 수였다. PostgreSQL + Realtime + Auth가 다 있어서 백엔드를 따로 안 만들어도 됐다.
세 번째 삽질: 게임 서버 연동
웹사이트에 리더보드를 만들었다. 근데 데이터가 없었다. 게임 서버에서 킬/데스 정보를 어떻게 가져오지?
선택지 1: RCON으로 주기적으로 폴링
- 문제: 실시간이 아님, 1분 주기로 해도 느림
선택지 2: Oxide 플러그인 직접 개발
- 문제: C# 해본 적 없음
C# 배워야 했다. 근데 의외로 Java랑 비슷해서 3일 만에 기본은 익혔다.
// SupabaseSync.cs - 내가 만든 첫 번째 Oxide 플러그인
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (!(entity is BasePlayer victim)) return;
var killer = info?.Initiator as BasePlayer;
if (killer == null) return;
// Supabase로 킬 데이터 전송
SendToSupabase(new {
killer_id = killer.UserIDString,
victim_id = victim.UserIDString,
weapon = GetWeaponName(info),
distance = Vector3.Distance(killer.transform.position, victim.transform.position),
is_headshot = info.isHeadshot,
timestamp = DateTime.UtcNow
});
}
이게 돌아가는 순간 진짜 감동이었다. 게임에서 킬하면 0.2초 만에 웹사이트 킬피드에 뜬다.
네 번째 삽질: 팀 찾기
디스코드에서 맨날 "팀 구합니다" 글이 올라왔다. 근데 묻혀서 아무도 못 봤다.
"웹사이트에 팀 찾기 기능 만들까?"
Discord OAuth로 로그인하게 하고, Supabase Realtime으로 새 글 올라오면 실시간으로 보여주기로 했다.
문제: 트롤이 도배함
오픈하고 3일 만에 누가 도배했다. 같은 글 100개.
팀 구합니다
팀 구합니다
팀 구합니다
팀 구합니다
...
해결: 같은 유저 10시간 쿨타임
// 마지막 글 작성 시간 체크
const lastPost = await supabase
.from('team_posts')
.select('created_at')
.eq('discord_id', user.id)
.order('created_at', { ascending: false })
.limit(1);
if (lastPost.data?.[0]) {
const hoursSinceLastPost =
(Date.now() - new Date(lastPost.data[0].created_at)) / (1000 * 60 * 60);
if (hoursSinceLastPost < 10) {
throw new Error('10시간 후에 다시 작성 가능합니다');
}
}
문제: 어그로 계정이 평판 테러함
평판 시스템도 만들었는데, 버리는 계정으로 평판 테러하는 놈이 있었다.
해결: "Clean User" 자격 조건
const isCleanUser =
accountAge > 30 && // 계정 생성 30일 이상
discordAge > 90 && // 디스코드 가입 90일 이상
serverJoinDate > 7 && // 서버 가입 7일 이상
!hasRecentBan; // 최근 밴 기록 없음
이 조건 만족 못하면 평판 투표 못 함.
지금 구조
1년간 삽질 끝에 안정화된 현재 구조:
프론트엔드
- React + TypeScript + Vite: 빠른 개발, 타입 안정성
- TailwindCSS: 디자인 시스템 없이도 일관된 UI
- Framer Motion: 부드러운 애니메이션
백엔드
- Supabase: DB + Realtime + Auth 올인원
- Cloudflare Workers: Steam API 프록시 (CORS 우회)
게임 서버
- Oxide 플러그인 5개: SupabaseSync, TeamSync, ChatLogger, OnlineTracker, BountySystem
- Azure VM: 서울 리전, 20ms 핑
봇
- Discord.js: 팀 모집 봇, 알림 봇
- Twitter API: 홍보 자동화
숫자로 보는 1년
| 항목 | 수치 | |------|------| | 총 코드 라인 | ~45,000 | | 커밋 수 | 847 | | 월 방문자 | ~30,000 | | 동시 접속 피크 | 127명 | | 서버 비용 | 월 $50 (Azure VM) | | Supabase 비용 | $0 (무료 티어) | | 개발 기간 | 12개월 (풀타임 아님) |
배운 것들
1. "일단 만들어" 마인드
처음부터 완벽하게 설계하려고 하면 아무것도 못 만든다. 일단 동작하는 거 만들고, 문제 생기면 고치면 된다.
2. 유저 피드백이 기획서다
내가 생각한 기능 < 유저가 원하는 기능. 디스코드에서 "이거 있으면 좋겠다" 하면 그게 다음 개발 항목이 됐다.
3. 무료 서비스 조합의 힘
- Supabase 무료 티어: 500MB DB, 2GB 스토리지, Realtime 무제한
- Cloudflare Workers 무료 티어: 10만 요청/일
- GitHub Pages: 무료 호스팅
서버 비용 빼면 거의 $0으로 운영 중이다.
4. 모니터링은 필수
처음엔 에러 나도 몰랐다. 유저가 "야 이거 안 돼" 하면 그때 알았다. 지금은 Discord Webhook으로 에러 발생하면 바로 알림 온다.
앞으로
- 모바일 앱: React Native로 PWA보다 나은 경험
- AI 기능: 플레이 스타일 분석, 매칭 추천
- 더 많은 서버 연동: 다른 서버 운영자들도 쓸 수 있게
1년 전엔 그냥 친구들이랑 놀 서버 하나 열려고 했는데, 이렇게 될 줄 몰랐다.
근데 재밌다. 문제가 생기면 해결하면 되니까.
다음 글에서는 서버 인프라 이야기를 한다. Azure VM에서 러스트 서버 돌리면서 겪은 삽질들.