bobob.app
개발2026-04-1411분 읽기

가벼운 웹 게임을 정적으로 배포하는 구조

bobob.app의 작은 Play를 서버 없이 JSON 콘텐츠, 공통 엔진, 안정적인 QA hook, sitemap/feed 연결, 공개 전 점검 기록으로 운영하는 구조를 정리했습니다.

버그 티켓 접수선 바로 하기

서버를 붙이지 않기로 한 이유

작은 웹게임을 만들 때 가장 먼저 유혹되는 것은 저장, 랭킹, 계정, 댓글이다. 붙이면 그럴듯해 보인다. 그런데 bobob.app의 Play는 아직 그런 장치가 필요하지 않다. 목표는 오래 키우는 게임 서비스가 아니라, 글에서 말한 판단을 손으로 한 번 확인하게 만드는 짧은 상호작용이다. 그래서 첫 구조는 서버 없이 간다.

서버가 없으면 포기해야 하는 것도 있다. 유저별 저장도 없고, 랭킹도 없고, 공유 결과를 서버에 남기지도 않는다. 대신 얻는 것이 있다. 배포가 단순하고, 개인정보 경계가 선명하고, 한 Play를 고칠 때 데이터베이스 마이그레이션을 생각하지 않아도 된다. 작은 사이트에는 이 단순함이 꽤 중요하다.

지금 구조는 콘텐츠와 엔진을 나눠 둔다. Play의 제목, 설명, 결과 문장, 관련 글, 관련 Play, 계획 메모는 `content/play/*.json`에 둔다. 실제 조작은 `tap-game`, `sort-match-game`, `micro-sim`, `arcade-game` 계열 엔진이 담당한다. 새 Play를 만들 때 매번 페이지 전체를 새로 만들기보다, 이미 있는 엔진에 맞는 콘텐츠를 넣고 필요한 경우 엔진을 조금만 확장한다.

이 선택은 "나중에 서버를 붙이면 된다"는 미룸이 아니다. 공개 표면을 작게 유지하겠다는 운영 기준이다. 지금 단계에서 필요한 것은 계정 기능보다 한 페이지가 완성된 콘텐츠인지, 게임이 모바일과 데스크톱에서 실제로 끝까지 움직이는지, 결과 뒤에 읽을 글과 다음 Play가 자연스럽게 보이는지다. 그 세 가지를 안정적으로 만들기 전에는 저장 기능이 오히려 흐름을 흐린다.

정적 구조의 경계

서버 없는 구조라고 해서 모든 것이 한 파일에 들어가는 것은 아니다. 오히려 경계를 더 분명하게 둬야 한다. 콘텐츠, 화면, 조작, 발견 경로, 검증이 섞이면 한 Play를 고칠 때 다른 Play까지 같이 흔들린다.

경계맡는 파일과 표면공개 전에 보는 것지금 일부러 두지 않은 것
콘텐츠`content/play/*.json`, 관련 Blog frontmatter제목, 설명, 결과 문장, 관련 글, 관련 Play가 한 판단으로 이어지는가서버 저장용 사용자 상태
엔진`apps/main/src/features/play/*-engine.tsx`마우스, 터치, 키보드 중 최소 한 흐름이 끝까지 작동하는가게임별 임시 페이지 복사
발견`/play`, `/play/{slug}`, `/search?q=`, feed, sitemap새 Play가 목록, 검색, 관련 링크에서 같은 방향으로 노출되는가숨은 실험 페이지의 무제한 노출
검증`data-play-*` hook, route smoke, interaction harness실제 클릭 후 결과와 공유, 관련 링크가 보이는가"눈으로 한 번 봤다" 수준의 수동 확인
개인정보브라우저 안의 일시 상태결과 공유가 서버 기록 없이 가능한가로그인, 랭킹, 개인 저장소

이 표는 코드 구조이기도 하지만, 공개 기준이기도 하다. Play가 재미있어 보여도 관련 글이 없거나, 결과 뒤 링크가 비어 있거나, 모바일에서 조작이 막히면 아직 공개할 준비가 안 된 것으로 본다. 반대로 서버가 없어도 이 경계가 맞으면 짧은 Play 하나는 충분히 완성된 콘텐츠가 된다.

지금 쓰는 형식

형식쓰는 상황좋은 점조심할 점
`tap-game`빠르게 맞는 신호와 아닌 신호를 누르는 게임규칙을 바로 이해한다단순 퀴즈처럼 보이지 않게 피드백을 줘야 한다
`sort-match-game`항목을 몇 개의 상자에 나눠 넣는 게임판단 기준을 비교하기 좋다텍스트 카드만 많아지면 작업표처럼 보인다
`micro-sim`몇 번의 선택이 상태를 바꾸는 흐름글에서 말한 판단 순서를 작은 이야기로 만든다결과가 설교처럼 끝나지 않아야 한다
`arcade-game`키보드, 마우스, 터치로 바로 움직이는 캔버스 게임체류와 손맛이 가장 분명하다화면 신호와 모바일 조작을 실제로 확인해야 한다

처음에는 선택형이나 분류형이 만들기 쉬웠다. 하지만 Play가 계속 늘어나면 단순 텍스트 선택만으로는 약하다. 그래서 최근에는 캔버스 기반의 `arcade-game` 쪽을 더 늘리고 있다. 스네이크, 스태커, 지뢰 찾기, 숫자 금고, 합 10 드래그처럼 직접 조작이 있는 Play가 사이트의 성격을 더 잘 보여준다.

엔진을 고를 때는 구현 편의보다 사용자가 손으로 느끼는 판단이 먼저다. `priority-sorter`처럼 오늘 할 일과 밖으로 보낼 일을 나누는 표면은 `sort-match-game`이 맞다. `bug-clicker`처럼 증상과 소음을 빠르게 구분해야 하는 표면은 캔버스에 뜨는 항목을 직접 잡는 쪽이 더 낫다. `meeting-escape`처럼 작은 선택이 하루 흐름을 바꾸는 이야기는 `micro-sim`이 자연스럽다. 같은 글을 붙여도 엔진 선택이 빗나가면 게임은 얇은 버튼 묶음처럼 보인다.

콘텐츠 파일이 맡는 일

각 Play JSON은 단순한 제목 묶음이 아니다. 적어도 아래 역할을 해야 한다.

  • - 첫 5초 안에 무엇을 해야 하는지 설명한다.
  • - 결과 문장이 게임의 판단 기준을 다시 말해준다.
  • - 관련 Blog로 돌아가는 길을 만든다.
  • - 다른 Play로 이어지는 다음 행동을 둔다.
  • - `planningBrief`로 왜 이 규칙과 화면을 선택했는지 남긴다.

`planningBrief`는 특히 중요하다. 얇은 게임은 "점수 올리기"로 끝난다. 좋은 작은 Play는 왜 이 조작이 이 글과 연결되는지 설명할 수 있어야 한다. 복권형이면 확률, 기대값, 손실 회복을 생각해야 하고, 성장형이면 자원과 곡선을 봐야 한다. 액션형이면 거리, 위험 신호, 실패 원인을 봐야 한다. 이 질문이 없으면 게임 표면만 바뀌고 속은 같아진다.

그래서 콘텐츠 파일에는 공개 문장과 내부 계획이 함께 들어간다. 공개 문장은 처음 온 사람이 읽는다. 내부 계획은 다음에 내가 다시 고칠 때 읽는다. 둘을 분리해 두면 공개 페이지에는 필요한 말만 남기고, 왜 그런 게임 규칙을 골랐는지는 코드 리뷰와 하네스에서 확인할 수 있다.

한 Play를 공개하기 전 보는 질문

질문통과 기준실패하면 하는 일
글과 Play가 같은 판단을 말하는가결과 문장이 관련 Blog의 핵심 판단을 다시 말한다관련 글을 바꾸거나 Play 규칙을 줄인다
첫 화면에서 조작이 보이는가시작 버튼, 캔버스, 카드, 입력 표면이 스크롤 없이 이해된다설명을 늘리기보다 화면 신호를 바꾼다
실패 이유가 보이는가틀린 선택 뒤에 왜 틀렸는지 짧게 남는다점수만 보여주는 결과를 피드백 문장으로 바꾼다
다음 행동이 있는가관련 Blog와 관련 Play가 결과 근처에 보인다결과 링크와 reciprocal metadata를 맞춘다
자동 검증이 누를 수 있는가`data-play-action`에서 결과까지 도달한다버튼 이름이 아니라 stable hook을 먼저 고친다

엔진이 맡는 일

엔진은 화면과 조작을 안정적으로 제공해야 한다. 특히 bobob.app은 Play마다 QA hook이 있어야 한다. 자동 검증이 실제로 게임을 누르고 결과 상태까지 갈 수 있어야, 새 콘텐츠를 추가해도 "페이지는 뜨지만 게임은 끝나지 않는" 상태를 막을 수 있다.

지금 Play 표면에서 계속 유지하려는 hook은 이런 것들이다.

  • - `data-play-action`: 사용자가 실제로 누르거나 조작하는 시작점
  • - `data-play-result`: 결과 상태가 렌더링되는 지점
  • - `data-play-share`: 결과 공유 동작
  • - `data-play-result-links`: 결과 후 이어지는 링크 묶음
  • - `data-play-related-play`: 다른 Play로 이어지는 링크
  • - `data-play-related-blog`: 관련 글로 돌아가는 링크

이런 hook은 사용자가 보지 않는다. 하지만 운영자에게는 중요하다. Play가 늘어나면 눈으로 전부 확인하기 어렵다. 하네스가 모바일과 데스크톱에서 실제 클릭/키보드 흐름을 타고 결과를 확인해야 한다. 정적 사이트라도 상호작용은 깨질 수 있기 때문이다.

특히 캔버스 Play는 정적 HTML처럼 보여도 실제로는 클라이언트 런타임 품질에 많이 기대고 있다. 키보드 이벤트가 브라우저 스크롤과 충돌하지 않는지, 터치 좌표가 캔버스 비율에 맞게 환산되는지, 결과 패널이 hydration 뒤에도 같은 위치에 남는지 봐야 한다. 서버가 없다고 해서 테스트가 줄어드는 것이 아니라, 테스트 위치가 API에서 화면 조작으로 옮겨간다.

서버 없이도 깨지는 지점

지점실제 위험확인 방법
hydration처음 HTML은 보이지만 버튼이 반응하지 않을 수 있다Playwright로 시작 동작과 결과 hook을 누른다
캔버스 좌표모바일에서 터치 위치와 실제 타깃이 어긋날 수 있다모바일 viewport로 주요 조작을 한 번 끝까지 탄다
결과 링크게임은 끝나도 관련 Blog/Play가 비어 있을 수 있다`data-play-result-links`와 reciprocal metadata를 확인한다
sitemap/feed새 Play가 페이지로는 있으나 발견 표면에 빠질 수 있다live discovery와 submitted URL health를 같이 본다
공개 문장개발 메모가 그대로 노출되어 사용자에게 변명처럼 보일 수 있다Blog/Play 문장을 읽고 내부 용어를 줄인다

배포와 발견 경로

정적 Play라고 해서 콘텐츠 발견을 대충 둬도 되는 것은 아니다. Play가 추가되면 적어도 아래 표면이 같이 움직여야 한다.

  • - `/play` 목록
  • - `/play/{slug}` 상세
  • - 관련 Blog 카드와 결과 링크
  • - `/sitemaps/en`
  • - `/feed.xml`, `/atom.xml`, `/feed.json`
  • - `/llms.txt`
  • - 전역 `/search?q=`

이 중 하나라도 빠지면 사용자는 찾기 어렵고, 검색 시스템도 현재 사이트 구조를 덜 이해한다. 그래서 Play 작업은 "파일 하나 추가"가 아니라 "콘텐츠 등록, 결과 링크, 검색/발견 표면, 실제 조작 검증"까지 한 묶음으로 본다.

여기서 중요한 것은 모든 글과 모든 실험을 한꺼번에 제출하지 않는 것이다. 짧은 개발 메모는 보관할 수 있지만, 대표 글처럼 목록과 sitemap에 밀어 넣으면 사이트가 덜 완성되어 보인다. 그래서 제출 표면은 대표 Blog, Play, category hub, trust page 중심으로 작게 유지한다. 짧은 노트는 직접 URL로는 남겨도 public list, feed, sitemap에서는 빼고, 대표 build log 안에서 어떤 메모를 흡수했는지 이름으로 남긴다. 이 방식이면 시행착오를 숨기지 않으면서도 처음 보는 사람에게는 완성된 읽을거리가 먼저 보인다.

지금 구조의 한계

서버가 없어서 좋은 점만 있는 것은 아니다. 랭킹이나 저장이 없으니 장기 몰입은 약하다. 결과 공유도 브라우저의 복사 동작에 기대야 한다. 또 모든 Play가 같은 엔진 위에 있으면 화면이 비슷해질 수 있다. 그래서 엔진은 공유하되, 각 Play의 시각적 은유는 다르게 가져가려고 한다.

예를 들어 숫자 금고는 증거판과 키패드가 어울리고, 합 10 드래그는 낡은 사과상자와 손으로 훑는 느낌이 맞다. 스태커는 레일과 착지선이 중요하고, 지뢰 찾기는 닫힌 지도와 깃발 보류가 중심이다. 같은 `arcade-game`이어도 화면이 같은 퀴즈판처럼 보이면 실패다.

또 하나의 한계는 "운영자가 직접 만든 콘텐츠"라는 신호를 계속 남겨야 한다는 점이다. 서버 기능이 없으면 사용자의 누적 기록이 대신 말해주지 않는다. 그래서 각 Play는 build log, 관련 글, 결과 문장, 검증 기록으로 자기 맥락을 가져야 한다. 단순히 게임 수를 늘리는 것은 쉽지만, 그렇게 하면 사이트가 샘플 모음처럼 보인다. 지금 필요한 것은 수량보다 연결된 제작 판단이다.

유지할 체크리스트

  • - 새 Play는 기존 엔진으로 가능한지 먼저 본다.
  • - 직접 조작이 필요한 경우 캔버스와 버튼 fallback을 같이 확인한다.
  • - 결과 화면에서 관련 Blog와 관련 Play가 실제로 보이는지 본다.
  • - `planningBrief`가 장르별 질문을 담는지 확인한다.
  • - `/sitemaps/en`에는 대표 Blog와 Play 중심으로만 제출한다.
  • - `harness:play-planning`, `harness:blog-play-mvp`, `harness:play-interaction`을 같이 본다.

지금 유지하는 공개 기준

정적 Play 하나를 추가하거나 고칠 때 최종 판단은 아래 순서로 한다.

1. Play가 글 없이도 첫 화면에서 할 일을 이해시키는가. 2. 관련 Blog가 게임 규칙의 이유를 설명하는가. 3. 결과 화면이 다음 글이나 다음 Play로 이어지는가. 4. 짧은 개발 메모가 대표 글 안에서 흡수되어 있는가. 5. sitemap, feed, search, route smoke가 같은 공개 표면을 보고 있는가. 6. 모바일과 데스크톱에서 같은 결과 hook까지 도달하는가.

이 구조가 대단한 게임 플랫폼은 아니다. 하지만 작은 사이트가 계속 고치고 배포하기에는 충분하다. 서버를 붙이지 않아서 가능한 속도와 단순함을 먼저 얻고, 손으로 해볼 수 있는 원본 Play를 꾸준히 남기는 쪽이 지금 bobob.app에는 맞다.

바로 해보기

버그 티켓 접수선과 오늘 일감 세 칸 보드는 같은 정적 콘텐츠 구조 위에 있다. 하나는 실제 증상과 소음을 가르는 캔버스 게임이고, 하나는 오늘 볼 일과 판 밖으로 보낼 일을 나누는 분류 게임이다. 둘 다 서버 없이 움직이지만, 관련 글과 결과 링크, sitemap, feed까지 이어져야 비로소 하나의 공개 콘텐츠가 된다.

읽고 나서 해볼 것

가벼운 웹 게임을 정적으로 배포하는 구조 - bobob.app | bobob.app