cd ..
published|

PixelForge — 브라우저에서 돌아가는 픽셀 아트 에디터

게임 개발자를 위한 웹 기반 픽셀 아트 에디터. React + TypeScript · Clean Architecture · TDD + ODD · 591+ 테스트 · 29일 개발.

브라우저에서 바로 돌아가는 픽셀 아트 에디터 · pixel-forge.gunggme.cc

에디터 전체 화면

한 줄 소개

게임 개발자를 위한 웹 기반 스프라이트 에디터. Aseprite에서 영감을 받아 React + TypeScript로 처음부터 다시 설계했습니다. 설치 없이, 가입 없이, 브라우저 탭 하나만 있으면 스프라이트를 만들고 애니메이션을 구성하고 11가지 포맷으로 내보낼 수 있습니다.

주요 기능

  • 🎨 18가지 드로잉 도구 — 펜슬, 채우기, 직선, 도형, 베지어, 그라디언트, 슬라이스, 타일 드로우
  • 🎞️ 레이어 + 프레임 애니메이션 — 블렌드 모드 19종, 어니언 스킨, 프레임 태그
  • 🧱 타일맵 시스템 — 게임 레벨 제작용 타일셋 관리 + 타일 드로우
  • 📤 11개 포맷 내보내기 — PNG · GIF · JPEG · WebP · BMP · TGA · ICO · SVG · PNG Sequence · .aseprite · 스프라이트 시트
  • 🔐 JWT 기반 클라우드 저장 — access 15분 + refresh 7일 · DB 기반 토큰 로테이션
  • 👤 게스트 모드 — 가입 없이 IndexedDB에 로컬 저장
  • ⌨️ 키보드 우선 설계 — 커스터마이즈 가능한 단축키 · 스크린 리더 지원
  • 📊 자체 구축 어드민 대시보드 — 사용량 · p50/p95 레이턴시 · 에러율 · 장애 감지

화면 소개

랜딩 페이지

랜딩

미니멀한 히어로 + 6개 기능 카드. 한국어/영어 토글 내장.

인증 플로우

로그인

이메일/비밀번호 로그인, 회원가입, 그리고 게스트 모드. 로컬 저장만 쓰는 사용자는 가입 없이 바로 그릴 수 있습니다.

새 스프라이트

새 스프라이트

8×8부터 256×256까지 6개 프리셋 + 커스텀 크기. 배경은 흰색 또는 투명.

에디터 메인

에디터

상단 메뉴바 · 툴 옵션 바 · 좌측 18개 툴 · 중앙 캔버스 · 우측 레이어/팔레트 · 하단 타임라인. 모든 패널은 리사이즈 가능하고 위치는 localStorage에 영구화됩니다.

픽셀 드로잉

드로잉

DB-16 팔레트로 그린 64×64 하트. 선택된 색상이 RGB/HSV/HSL 슬라이더와 Hex 코드에 실시간 반영됩니다.

레이어 시스템

레이어

다중 레이어 + 19개 블렌드 모드(Normal / Multiply / Screen / Overlay / Color Dodge / ... / Subtract / Divide). 레이어별 가시성과 잠금, 그룹핑 지원.

애니메이션 타임라인

타임라인

프레임 × 레이어 2D 그리드. 어니언 스킨, FPS 조정, 프레임 태그, 구간 플레이. requestAnimationFrame 기반으로 정확한 타이밍 보장.

스프라이트 시트 프리뷰

시트

모든 프레임을 수평/수직으로 나열한 실시간 프리뷰. 내보내기 전에 바로 확인 가능.

내보내기

내보내기

11개 포맷 중 선택. GIF는 gifenc로 인코딩, .asepritefflate로 압축 직렬화, 스프라이트 시트는 컬럼/패딩 커스터마이즈.

어드민 대시보드 — Usage

어드민 Usage

총 유저 · 프로젝트 수 · 7일 액티브 · 30일 가입 추이. 자체 구축한 페이지뷰 분석(Top paths / Top referrers) 포함. Google Analytics 없이 서버에서 직접 수집.

어드민 대시보드 — Engineering & Ops

어드민 Ops

p50/p95 레이턴시 · 에러율 · 요청 수 · 레이턴시 히스토그램. 5분 단위 슬라이딩 윈도우로 에러율 5% 초과 시 장애로 자동 기록. Storage 사용량도 실시간 계산.

어드민 — Users

유저 관리

이메일/이름 검색, Active/Deleted/All 필터, 소프트 삭제. Role + Subscription 관리.

기술 스택

영역스택
프론트엔드React 19 · Vite 8 · TypeScript strict · Zustand · CSS Modules
백엔드Express 5 · PostgreSQL (pg) · bcryptjs · jsonwebtoken
테스트Vitest · Testing Library · Playwright E2E · PGlite (in-process Postgres)
인프라Railway (Postgres) · Cloudflare (DNS · 엣지 SSL) · Docker
빌드Vite 모놀리식 (프론트 + 백엔드 단일 프로젝트)

아키텍처 — Clean Architecture를 프론트/백 모두에

┌─────────────────────────────────────────────────────┐
│  presentation/   React 컴포넌트 · Express 라우트     │
├─────────────────────────────────────────────────────┤
│  application/    Use Case · 오케스트레이션           │
├─────────────────────────────────────────────────────┤
│  domain/         순수 TS 엔티티 + Port(인터페이스)   │
├─────────────────────────────────────────────────────┤
│  infrastructure/ pg · bcrypt · jwt · HTTP 어댑터    │
└─────────────────────────────────────────────────────┘
      의존성은 안쪽으로만 — 바깥이 안쪽을 본다

모든 외부 의존성은 Port(인터페이스) 를 통해 도메인에 주입됩니다. 수동 DI 컨테이너로 11개 Use Case를 와이어링. 도메인 레이어에는 React도, Express도, pg도 들어오지 않습니다.

개발 방법론 — TDD + ODD

TDD (Test-Driven Development)

백엔드는 전부 Red → Green → Refactor. 도메인 엔티티, Use Case, API 컨트롤러는 예외 없이 TDD로 작성했습니다. 단순 CRUD와 DI 와이어링은 구현 후 타겟 테스트를 붙이는 식으로 실용적으로 조절.

ODD (Observability-Driven Development)

관측성을 Day 1부터 설계. 기능을 완성한 뒤 로깅을 붙이는 게 아니라, 기능과 관측성을 같이 설계합니다.

  • 구조화된 JSON 로그 (requestId · userId · latency · status)
  • 자체 구축 페이지뷰 분석 (서드파티 없음)
  • p50 / p95 요청 레이턴시 + 에러율
  • 5분 단위 슬라이딩 윈도우로 장애 자동 감지 (에러율 5% 초과 시)

오버엔지니어링 금지 원칙

  • 60초 화이트보드 룰 — 화이트보드에 60초 안에 설명 못 하면 복잡함
  • YAGNI — 가상의 요구사항을 위해 빌드하지 않음
  • 3번 룰 — 비슷한 코드 2번까지는 복제, 3번째부터 추상화
  • 레이어 2개부터 시작 — 필요할 때만 4개까지 성장 (절대 초과 금지)

기술적 도전 & 해결

1. putImageDataglobalAlpha를 무시한다

Canvas 2D의 putImageData는 합성 파이프라인을 우회합니다. 그래서 레이어 opacity가 아예 안 들어갔습니다. 해결: OffscreenCanvas에 레이어별 opacity로 그린 뒤 drawImage로 최종 합성. GC 부담을 피하려고 OffscreenCanvas 풀(최대 16개) 을 만들어 재사용.

2. 프레임 정체성 vs 배열 인덱스

프레임을 배열 인덱스로 참조했더니, 재정렬/삭제 시 cel 데이터가 엉뚱한 프레임으로 새어 들어가는 버그가 발생했습니다. 해결: 모듈 레벨 카운터로 stable frame ID를 발급하고, 모든 조회를 Map<frameId, cel>로 전환. 이 마이그레이션 하나로 196개 테스트를 재작성했습니다.

3. 어니언 스킨 성능

프레임을 바꿀 때마다 이전/다음 프레임을 실시간 합성했더니 60 FPS 메인 스레드가 죽었습니다. 해결: 프레임별 합성 결과를 OffscreenCanvas에 사전 캐시, 프레임 변경 시에만 무효화. 프레임당 CPU 시간: 8ms → 0.3ms.

4. Cloudflare 프록시 + Let's Encrypt 닭-달걀

Railway 커스텀 도메인에 인증서가 발급되지 않았습니다. 원인은 Cloudflare 프록시가 HTTP-01 챌린지를 엣지에서 가로채는 것. Let's Encrypt의 검증 요청이 오리진 서버까지 도달하지 못했습니다. 해결 순서: 프록시 OFF → Railway 자체 인증서 발급 → Cloudflare SSL 모드를 Full (strict)로 → 프록시 다시 ON. 결과적으로 엣지 인증서와 오리진 인증서 모두 정상 동작.

숫자로 보는 프로젝트

항목
개발 기간29일 (13 phase)
총 테스트591+ (단위 + 통합 + E2E)
드로잉 도구18개
내보내기 포맷11개
블렌드 모드19개 (17 네이티브 + 2 수동 구현)
팔레트 프리셋5개 (DB-16 · Pico-8 · Endesga-32 · CGA · Grayscale)
백엔드 엔드포인트15개
Use Case11개

배운 것

  • 관측성은 나중에 붙일 수 없다 — ODD로 설계 초기부터 메트릭 포인트를 심어두면 장애 대응이 10배 빠릅니다.
  • 테스트는 속도를 늦추지 않고 올려준다 — 591개 테스트 덕분에 12 phase를 자신감 있게 리팩터링할 수 있었습니다.
  • Clean Architecture는 Port 3개만 있어도 의미가 있다 — 프레임워크를 도메인 밖에 가두는 것만으로 단위 테스트 속도가 20배 빨라집니다.
  • 오버엔지니어링이 정말 무섭다 — Redis, 메시지 큐, 별도 GraphQL 게이트웨이 같은 유혹이 수십 번 있었지만, 모두 '측정된 병목이 나오면' 쪽으로 미뤘습니다. 결과적으로 29일 만에 591 테스트와 12 phase를 완성할 수 있었습니다.

링크