때는 바야흐로 12월 3일, 스핔의 시대가 도래하고 있다.
여러 영상을 보며 깔깔 웃던 트릭컬 유저로서, 수학여행 중 문득 이런 생각이 들었다.
한번 비슷한 거 만들어 보고 싶은데?
그렇게 3일간의 개발이 시작됐다.
게임명: 네르지마세요
개발 기간: 3일
기술 스택:
플레이 영상:
https://youtu.be/udM6HxwW4rs?si=SgUpNyP3-SaZ_Ir2

Firebase Realtime Database로 최대 100명 동시 플레이.
onDisconnect)requestAnimationFrame으로 60fps 안정화.
다른 플레이어 클릭 시 픽셀 왜곡 애니메이션.
Top 5 플레이어 실시간 표시.
TypeScript 대신 JavaScript를 선택한 이유:
React를 선택한 이유:
사실 뭐 걍 팬게임이라서 묵직하게는 안들어갈려고 했다.
자체 서버 vs Firebase:
| 항목 | 자체 서버 | Firebase |
|---|---|---|
| 개발 시간 | 2-3일 추가 | 즉시 |
| 비용 | $5~10/월 | 무료~$5/월 |
| 확장성 | 직접 관리 | 자동 |
| 실시간 | Socket.io 구현 | 내장 |
결정: 3일 프로젝트에 Firebase가 최적.
이게 인기가 많아지면 Firebase 말고 지금 서버를 파던지 유료플랜을 바꾸던지 이런식으로 만들면 될것같다
문제 발견:
useEffect(() => {
// Canvas 렌더링
}, [position, otherPlayers, zone, squishPlayers]); // 😱 과도한 의존성
모든 상태 변경마다 렌더링 → 440fps 폭주.
해결:
// 1. ref로 state 접근
const positionRef = useRef(position);
// 2. requestAnimationFrame
useEffect(() => {
const render = () => {
ctx.drawImage(img, positionRef.current.x, ...);
requestAnimationFrame(render);
};
render();
}, []); // ✅ 빈 의존성 → 60fps 안정
결과: 86% 성능 향상 (440fps → 60fps)
문제:
픽셀 단위 왜곡 계산 (200x200 = 40,000번) → 렉 발생.
해결: Map 캐싱
const squishCache = new Map();
// 같은 위치 클릭 시 캐시 재사용
if (squishCache.has(cacheKey)) {
return cachedCanvas;
}
// 최대 20개 유지
if (squishCache.size > 20) {
const firstKey = squishCache.keys().next().value;
squishCache.delete(firstKey);
}
결과: 반복 클릭 시 즉시 렌더링.
문제:
왜곡 효과 공유 시 타이밍 문제 - 저장 후 0.5초 만에 삭제 → 다른 클라이언트가 못 받음.
해결:
// 삭제 시간 증가
setTimeout(() => {
remove(squishRef);
}, 1000); // 500ms → 1000ms
문제:
serviceAccountKey.json 커밋 → GitHub Push Protection 차단.
해결:
# 1. 커밋 히스토리에서 제거
git reset --soft HEAD~1
git rm --cached serviceAccountKey.json
# 2. .gitignore 추가
echo "serviceAccountKey.json" >> .gitignore
# 3. 재커밋
git commit -m "..."
git push --force
교훈: 민감 정보는 .env + .gitignore 필수!
이론으로만 알던 Canvas를 실제 게임에 적용하면서:
requestAnimationFrame의 중요성 체감가장 어려웠던 부분:
좌표 계산. 월드 좌표 → 스크린 좌표 → 캐릭터 중심점 계산이 계속 헷갈렸다.
const screenX = player.x - cameraX;
const screenY = player.y - cameraY;
이 한 줄을 이해하는 데 생각보다 오래 걸렸다.
생각보다 쉬웠던 것:
onValue로 실시간 구독생각보다 어려웠던 것:
onDisconnect 타이밍처음엔 Rules를 전부 열어놨다가, 나중에 제대로 설정했다.
{
"players": {
"$uid": {
".write": "auth.uid == $uid" // 자기 데이터만 수정
}
}
}
useEffect 의존성의 중요성:
아무 생각 없이 넣었던 의존성 배열이 440fps를 만들었다.
// ❌ 이렇게 하면 안 됨
useEffect(() => {
// 렌더링
}, [position, zone, players]); // 모든 변화마다 실행
// ✅ 이렇게 해야 함
useEffect(() => {
const render = () => {
// positionRef.current로 접근
requestAnimationFrame(render);
};
render();
}, []); // 한 번만 실행
useRef의 힘:
state는 변경되면 리렌더링하지만, ref는 그렇지 않다는 걸 제대로 이해했다.
동시성은 어렵다:
특히 왜곡 효과 공유:
이런 문제를 겪으면서 "동기화"가 얼마나 어려운지 알았다.
localhost에서만 돌리다가 실제 배포하니:
"일단 돌아가게" 만드는 것과 "실제 사용 가능하게" 만드는 건 다르다는 걸 배웠다.
3일 만에 100명이 플레이할 수 있는 게임을 만들었다는 자신감.
처음엔 "이거 내가 할 수 있나?" 싶었는데, 하나씩 해결하다 보니 완성됐다.
특히 성능 최적화 (440fps → 60fps)를 직접 해결한 게 뿌듯했다.
클로드의 도움을 받지 않았다면 효율적으로 코드를 생산해 내지 못했을것이다.
고마워요 클로드!pro샀는데 이정도는 해야지 암
3일간의 개발 회고:
수학여행 중 떠올린 아이디어가 실제 게임이 됐다.
처음엔 단순히 "재미있겠다" 싶어서 시작했지만, 막상 만들다 보니:
생각보다 많은 문제를 마주했다.
하지만 하나씩 해결하면서 성장했고, 결과물이 나왔다는 게 뿌듯하다.
좀 바이브를 돌리긴 했지만
앞으로의 계획:
링크:
피드백 환영합니다!
버그 제보나 개선 아이디어가 있다면 GitHub Issues나 댓글로 남겨주세요.
"일단 만들어보자"가 가장 중요한 것 같다.
수학여행이면 고등학생인가요?? 대단하네요 잘 보고 갑니다!