90년대 다마고치를 브라우저용으로 재구축하며 — 어떤 튜토리얼보다 상태 머신(State Machines)에 대해 더 많이 배우게 된 과정
요약
TypeScript를 사용하여 90년대 다마고치를 브라우저용 오픈 소스로 재구축한 과정을 다룹니다. 복잡한 상태 머신(State Machines), 게임 루프, 감성 디자인을 구현하며 얻은 기술적 통찰을 공유합니다.
핵심 포인트
- TypeScript 기반의 의존성 없는 브라우저 구현
- 복잡한 상태 머신과 조건부 전이 설계의 중요성
- 실시간 데이터 감소를 처리하는 게임 루프 메커니즘
- 기술적 완성도만큼 중요한 감성 디자인의 가치
1996년, Bandai는 8,200만 개의 Tamagotchi를 판매했습니다. 아이들은 계란 모양의 플라스틱 키링을 어디든 들고 다녔고, 수학 시간에 무시했다가는 말 그대로 죽어버리는 픽셀 덩어리에게 먹이를 주고, 청소해주고, 놀아주기 위해 세 개의 버튼을 미친 듯이 눌러댔습니다.
수백만 명의 사람들이 소프트웨어 한 조각에 진정한 정서적 애착을 느낀 첫 번째 사례였습니다.
30년이 지난 지금, 저는 그 모든 경험을 TypeScript를 사용하여 의존성 없이(zero dependencies), 완전히 오픈 소스로, 브라우저에서 재구축했습니다.
앱 스토어도, 다운로드도, 설치도 필요 없습니다. 그저 탭을 열고 당신의 반려동물을 입양하기만 하면 됩니다.
그리고 그 과정에서, 저는 그 어떤 컴퓨터 과학 강의가 가르쳐준 것보다 상태 머신(State Machines), 게임 루프(Game Loops), 그리고 감성 디자인(Emotional Design)에 대해 더 많은 것을 배웠습니다.
2025년에 왜 가상 반려동물을 만드는가?
세 가지 이유가 있습니다:
1. 향수는 배포를 위한 해킹(Distribution Hack)이다
사람들은 어린 시절의 기억을 자극하는 것을 공유합니다. 이는 이성적인 것이 아니라 감성적인 것입니다. 브라우저 기반의 Tamagotchi는 그 어떤 할 일 목록(Todo) 앱이나 대시보드도 건드릴 수 없는 신경을 자극합니다.
제가 초기 프로토타입을 공유했을 때, 반응은 "멋진 기술 스택이네요"가 아니었습니다. 다음과 같았습니다:
"세상에, 저 초등학교 2학년 때 제 다마고치가 죽었을 때 울었던 기억이 나요"
그러한 감정적 반응은 그 어떤 Product Hunt 출시보다 가치가 있습니다.
2. 게임 상태 머신(Game State Machines)은 지나치게 과소평가되어 있다
모든 튜토리얼은 신호등이나 토글 버튼을 통해 상태 머신을 가르칩니다. 지루하고, 쓸모없으며, 잊히기 쉽습니다.
가상 반려동물은 수십 개의 상호 연결된 상태(Interconnected States), 실시간 감소(Real-time decay), 진화 경로, 조건부 전이(Conditional transitions), 그리고 상태 아키텍처에 대해 실제로 '생각'하게 만드는 예외 상황(Edge cases)을 가지고 있습니다. 이것을 구축한 후에는, 실제 프로덕션 앱에서 복잡한 UI 흐름을 구현하는 것이 사소하게 느껴졌습니다.
3. 모든 것이 SaaS일 필요는 없다
인디 개발자 세계는 "수익을 창출하는 사이드 프로젝트"에 집착합니다. 때로는 순수하게 사람들을 미소 짓게 만들기 위해 무언가를 만들어야 합니다. 최고의 프로젝트는 다른 사람이 아무도 없더라도 당신이 직접 사용할 프로젝트입니다.
당신의 새로운 반려동물을 만나보세요
Tamagochi를 열면 알을 받게 됩니다. 알이 부화합니다. 아주 작은 픽셀 형태의 생명체가 나타납니다. 이 생명체에게는 욕구(needs)가 있습니다. 그 욕구를 충족시켜 주면 생명체는 번성합니다. 무시한다면... 음, gameOver는 유효한 상태(state)입니다.
당신의 반려동물은 실시간으로 감소하는 네 가지 핵심 욕구를 가지고 있습니다:
interface PetState {
name: string;
hunger: number; // 0 = 배부름, 100 = 굶주림
...
몇 초마다 통계치(stats)가 감소합니다. 반려동물은 점점 더 배고파지고, 슬퍼지고, 더러워지며, 더 피곤해집니다. 시계는 결코 멈추지 않습니다.
당신은 다음과 같은 간단한 행동을 통해 상호작용합니다:
- 먹이 주기 (Feed) — 허기(hunger)를 감소시킵니다 (하지만 너무 많이 먹이면 병이 듭니다)
- 놀아주기 (Play) — 행복도(happiness)를 높입니다 (하지만 에너지가 소모됩니다)
- 청소하기 (Clean) — 위생(hygiene)을 회복합니다
- 잠자기 (Sleep) — 에너지를 회복합니다 (하지만 자는 동안에도 허기는 계속 증가합니다)
- 약 먹이기 (Medicine) — 병을 치료합니다 (아플 때만 가능합니다)
단순한 입력. 복잡한 창발적 행동(emergent behavior).
게임 루프 (The Game Loop): 실시간 감소
모든 가상 반려동물 게임의 핵심은 **틱 (tick)**입니다. 이는 통계치를 감소시키고 상태 전이(state transitions)를 평가하는 주기적인 함수입니다:
const TICK_INTERVAL = 3000; // 3초마다
const DECAY_RATES = {
...
이것은 3초마다 실행됩니다. 10분 동안의 세션 동안, 당신의 반려동물은 약 200번의 상태 전이를 거칩니다. 탭을 한 시간 동안 열어둔다면요? 그것은 1,200번의 틱입니다. 반려동물의 일생 전체가 눈앞을 스쳐 지나갑니다.
상태 머신 (The State Machine): 진화 및 생애 단계
여기서부터 흥미로워집니다. 반려동물은 단순히 존재하는 것이 아니라, 생애 단계를 통해 **진화 (evolves)**하며, _당신이 얼마나 잘 돌보느냐_에 따라 어떤 경로를 걷게 될지가 결정됩니다:
[Egg]
│
(hatch: 5 ticks)
...
이러한 분기(branching)는 **재플레이 가능성 (replayability)**을 만들어냅니다. 한 번의 플레이로는 모든 진화 경로를 볼 수 없습니다. 방치하면 다른 (더 슬픈) 변종이 생겨납니다. 완벽하게 돌보면 희귀한 단계가 해금됩니다.
function evaluateEvolution(pet: PetState): PetStage {
const avgMood = getAverageMoodScore(pet.moodHistory);
...
탭 닫기 문제 (내가 배포한 가장 어려운 버그)
이틀 동안 제 머리를 터지게 만들었던 질문이 하나 있습니다:
누군가 8시간 동안 탭을 닫아두면 어떤 일이 발생할까요?
옵션 A: 탭이 닫혀 있는 동안 반려동물이 정지합니다. 문제점: 긴장감이 완전히 사라집니다. 상황이 어려워질 때마다 그냥 탭을 닫아버리면 그만이니까요.
옵션 B: 너무 오래 자리를 비우면 반려동물이 즉시 죽습니다. 문제점: 잔인합니다. 죽음이 보장되어 있다면 아무도 다시 돌아오지 않을 것입니다.
옵션 C: 추적 계산 (Catch-up calculation). 탭이 다시 포커스를 얻었을 때, 부재 기간 동안 발생했을 법한 일들을 계산하되, 자비로운 상한선 (mercy cap)을 적용합니다.
저는 옵션 C를 선택했습니다:
function calculateCatchUp(pet: PetState, lastTickTime: number): PetState {
const now = Date.now();
const elapsed = now - lastTickTime;
...
자비로운 상한선 (mercy cap)은 매우 중요합니다. 이것이 없다면, 몇 시간 이상의 부재는 곧 = 확실한 죽음 = 아무도 두 번 이상 플레이하지 않음을 의미합니다. 상한선이 있다면, 절망감 없이 긴박함을 느낄 수 있습니다.
CSS 전용 렌더링 (Canvas 없음, WebGL 없음)
제가 스스로에게 설정한 제약 조건은 다음과 같습니다: <canvas> 사용 금지, WebGL 사용 금지, 스프라이트 이미지 (sprite images) 사용 금지. 반려동물 전체를 순수 CSS로만 렌더링합니다.
이유는 다음과 같습니다:
- 번들 크기를 매우 작게 유지할 수 있습니다 (총 약 15KB).
- Canvas 핑거프린팅 (canvas fingerprinting)을 차단하는 브라우저를 포함하여 어디서든 작동합니다.
- CSS 애니메이션은 무료로 GPU 가속을 받습니다.
- 재미있는 도전 과제입니다.
Box Shadow를 이용한 픽셀 아트
반려동물은 거대한 box-shadow 선언을 가진 단 하나의 <div>를 사용하여 그려집니다. 각 그림자가 하나의 "픽셀" 역할을 합니다:
.pet-sprite {
width: 4px;
height: 4px;
...
다양한 감정 상태에 따라 서로 다른 box-shadow 세트로 교체됩니다:
function getSpriteForMood(mood: string): string {
switch (mood) {
case 'happy': return sprites.happy; // 눈을 ^_^ 모양의 호(arc)로 표현
...
@keyframes를 이용한 애니메이션
반려동물이 살아있는 것처럼 느껴지게 만드는 작은 위아래 움직임인 유휴 상태 바운스 (idle bounce)는 순수 CSS입니다:
@keyframes idle-bounce {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
...
핵심 통찰: 애니메이션 속도가 감정을 전달합니다. 행복함 = 빠른 튀어오름. 슬픔 = 느린 처짐. 졸림 = 빙하처럼 느린 호흡. 플레이어들은 이러한 신호를 무의식적으로 읽습니다.
감성 디자인 (Emotional Design): 사람들이 Div를 신경 쓰게 만드는 법
이 프로젝트에서 가장 어려웠던 부분은 코드가 아니었습니다. 플레이어들이 CSS 픽셀의 집합체에 대해 진심으로 애착을 갖게 만드는 것이었습니다.
감성 디자인에 대해 제가 배운 점은 다음과 같습니다:
1. 반려동물이 당신을 바라보게 하라
대기 상태일 때, 반려동물의 "눈"(두 개의 픽셀 점)이 마우스 커서를 향해 천천히 움직입니다. 단 5줄의 JavaScript 추가만으로 사람들은 "아, 나를 보고 있어!" 라고 말하게 됩니다.
document.addEventListener('mousemove', (e) => {
const petRect = petElement.getBoundingClientRect();
const petCenter = {
...
2. 반응은 즉각적이고 과장되어야 한다
반려동물에게 먹이를 줄 때:
- 단순히 숫자가 업데이트되는 것에 그치지 않습니다.
- 살짝 점프를 합니다.
- 작은 하트 파티클 (particle)이 나타납니다.
- 눈이 1초 동안 행복한 호 모양으로 찌그러집니다.
- 그 후 정상 상태로 돌아옵니다.
이 과정은 200ms가 소요되지만, 상호작용을 보상감이 느껴지도록 만듭니다. 이 과정이 없다면 먹이를 주는 것은 버튼을 클릭하는 것과 같습니다. 이 과정이 있다면 먹이를 주는 것은 살아있는 무언가를 돌보는 것처럼 느껴집니다.
3. 죽음은 (약간) 아파야 한다
반려동물이 죽었을 때, 화면에는 단순히 "Game Over"라고 표시되지 않습니다. 대신 다음과 같은 일이 일어납니다:
- 반려동물 스프라이트 (sprite)를 천천히 회색조 (grayscale)로 페이드 아웃 시킵니다 (2초).
- 사라지기 전 반려동물 위에 작은 "..."를 표시합니다.
- (사운드가 활성화된 경우) 조용하고 단조로운 톤을 한 번 재생합니다.
- 반려동물의 나이, 얼마나 오래 살렸는지, 그리고 가장 좋았던 기분 상태를 보여줍니다.
- "다시 시작하기 (Restart)"가 아닌 "다시 입양하기 (Adopt again)"를 제안합니다 (언어 선택이 중요합니다).
목표는 누구에게 트라우마를 주는 것이 아닙니다. 다음에 더 잘하고 싶게 만드는 것입니다.
4. 반려동물의 이름을 지어주라
가장 먼저 하는 일은 반려동물의 이름을 짓는 것입니다. 이는 의도적인 설계입니다. 이름을 짓는 것은 애착을 형성합니다. "내 반려동물이 죽었다"는 "스프링클즈(Sprinkles)가 죽었다"와는 다르게 다가옵니다.
내가 배운 것 (실제 엔지니어링에도 적용 가능한 점)
1. 상태 머신 (State Machines)은 UI 버그의 80%에 대한 해답이다
여러분이 로딩 스피너(loading spinner)와 에러 메시지가 동시에 뜨거나, 폼(form)이 제출된 상태이면서 동시에 수정 가능한 상태인 것과 같은 "불가능한 상태(impossible state)"의 UI를 본 적이 있다면, 그것은 바로 상태 머신 (State Machines)의 문제입니다.
명시적인 상태(explicit states)와 유효한 전이(valid transitions)를 사용하여 다마고치를 구축한 후, 저는 실제 서비스(production)의 UI 버그를 다른 시각으로 보기 시작했습니다. 해결책은 거의 항상 다음과 같습니다: "상태를 명시적으로 정의하고, 유효하지 않은 전이가 불가능하도록 만드세요."
// 나쁜 예: 불리언 수프 (boolean soup)
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
...
2. 시간 기반 시스템은 생각보다 까다롭다
"3초마다 숫자를 감소시킨다"는 말은 사소해 보입니다. 하지만 다음과 같은 상황들을 마주하게 됩니다:
- 탭이 비활성화됨 (브라우저가
setInterval의 속도를 제한함) - 기기가 절전 모드로 전환됨
- 시간대 (Timezone) 변경
- 일광 절약 시간제 (Daylight saving time) 적용으로 인한 시간 점프
- 사용자가 시스템 시계를 조작함
세션(session)이 유지되는 실시간 게임은 시간을 상수가 아닌 **신뢰할 수 없는 입력값 (unreliable input)**으로 생각해야 합니다.
3. 완성도를 높이는 10%의 작업에 90%의 시간이 소요된다
핵심 게임 루프(game loop)를 만드는 데는 주말 한 번이면 충분했습니다. 하지만 "기분 좋게" 만드는 데는 3주가 더 걸렸습니다:
- 게임이 몰입감을 주면서도 스트레스를 주지 않도록 감소율 (decay rates) 조정
- 애니메이션이 기계적이지 않고 유기적으로 느껴지도록 구현
- 죽음 시퀀스 (death sequence)가 감정적이되 조작적으로 느껴지지 않게 만들기
- 캐주얼 플레이어는 생존할 수 있고, 몰입하는 플레이어는 도전 과제를 느낄 수 있도록 난이도 조절
이 비율 (10% 기능, 90% 느낌)은 모든 제품에서 동일합니다. 기능은 하루 만에 작동하지만, 사용자가 그것을 "사랑하게" 만드는 데는 한 달이 걸립니다.
전체 기능 목록
| 기능 | 상태 |
|---|---|
| 실시간 능력치 감소 (Real-time stat decay) | 완료 |
| ... |
지금 바로 체험해보세요
온라인에서 플레이하기
클릭 한 번으로 직접 배포하거나, 클론(clone)하여 로컬에서 실행하세요:
git clone https://github.com/Tech-aficionado/Tamagochi---Open-Source-Game.git
cd Tamagochi---Open-Source-Game
npm install
...
localhost:3000을 여세요. 반려동물의 이름을 지어주세요. 제가 처음 했을 때보다 더 오래 살려보려고 노력해보세요 (맹세컨대, 저는 죽음 시퀀스를 테스트하느라 20분도 못 버텼습니다).
직접 배포하기
다음 단계 (What's Next)
- 다양한 종 (Multiple species) — 고양이, 강아지, 용, 로봇, 외계인 중 선택 (각기 다른 능력치 감소율 적용)
- 업적 시스템 (Achievement system) — "반려동물을 30일 동안 살려두기", "위엄 있는 성체로 진화하기", "1000번 먹이 주기"
- 멀티플레이어 펫 파크 (Multiplayer pet park) — 공유 공간에서 친구의 반려동물 방문하기
- 커스텀 스킨 (Custom skins) — 인앱 에디터로 자신만의 픽셀 반려동물 디자인하기
- PWA + 푸시 알림 (PWA + push notifications) — 휴대폰에 설치하여 "반려동물이 배고파해요!" 알림 받기
- 교배 (Breeding) — 능력치가 최대치인 성체 두 마리가 형질을 물려받은 아기를 생성
- 시즌 이벤트 (Seasonal events) — 할로윈 코스튬, 겨울 눈, 생일 축하
소스 (The Source)
- GitHub: github.com/Tech-aficionado/Tamagochi---Open-Source-Game
- 라이선스 (License): MIT — 채택하고, 포크(fork)하고, 번식시키세요
- 스택 (Stack): TypeScript (70.7%) + CSS (27.2%) + HTML (1.9%) + JavaScript (0.2%)
- 의존성 (Dependencies): 없음. 순수 TypeScript + CSS. 프레임워크 없음. 라이브러리 없음.
- 번들 크기 (Bundle size): 총 약 15KB
마지막 생각 (Final Thought)
우리는 하루 종일 생산성을 위한 도구들을 만듭니다. 작업 관리자, 대시보드, 분석 도구, 최적화 엔진 등. 모든 것이 _출력(output)_에 관한 것입니다.
때로는 누군가를 5분 동안 미소 짓게 만드는 것 외에는 아무런 목적이 없는 일에 가장 인상적인 엔지니어링이 투입되기도 합니다.
가상 반려동물은 당신을 승진시켜 주지 않습니다. 월간 반복 매출(MRR)을 창출하지도 않습니다. 벤처 캐피털(VC)을 감동시키지도 못합니다.
하지만 만약 당신이 누군가로 하여금 200바이트의 CSS box-shadow에 진정한 정서적 유대감을 느끼게 할 수 있다면 — 그것은 매우 의미 있는 종류의 엔지니어링입니다.
쓸모없는 것을 만드세요. 그것을 아름답게 만드세요. 그러면 어떤 튜토리얼이 가르쳐주는 것보다 더 많은 것을 배우게 될 것입니다.
간단한 투표: 당신의 첫 번째 가상 반려동물은 무엇이었나요? 다마고치? 네오펫(Neopets)? 닌텐독스(Nintendogs)? 아니면 눈알 스티커를 붙인 돌멩이였나요? 댓글로 남겨주세요 — 여러분이 어느 세대인지 알고 싶습니다.
Shivansh Goel 제작 — 최고의 코드는 사람들에게 무언가를 느끼게 해준다고 믿는 AI 및 풀스택 개발자 (Full Stack Developer).
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기