cd ..
published|

PixelForge에 모바일/태블릿 풀 편집 지원 추가

데스크톱 전용이던 픽셀 아트 에디터에 폰/태블릿 풀 편집 지원을 추가했습니다. 핀치 줌, 두 손가락 팬, 시스템 공유, 모바일 협업까지 4개 커밋으로 묶어 디자인 문서부터 배포까지 한 세션에 끝낸 과정을 공유합니다.

데스크톱 전용이던 픽셀 아트 에디터를 폰과 태블릿에서도 풀 편집 가능하게 만들었습니다. · pixel-forge.gunggme.cc

모바일 편집 화면

한 줄 소개

PixelForge는 원래 1024px 미만 뷰포트에서 "Viewport Too Small" 차단 화면을 띄우던 데스크톱 전용 도구였습니다. 이번 작업으로 같은 코드베이스 안에서 폰(<640px)은 별도 모바일 셸로, 태블릿(640–1023px)은 압축된 데스크톱 레이아웃으로 라우팅합니다. 핀치 줌, 두 손가락 팬, 시스템 공유, 모바일 협업까지 4개 커밋으로 이어붙였습니다.

주요 기능

  • 📱 폰 전용 레이아웃 — 캔버스 65% 확보, 상단 AppBar(undo/redo/오버플로우 메뉴), 가로 스크롤 ToolBar, 하단 탭바(Color/Layers/Frames)
  • 📄 BottomSheet 프리미티브 — 드래그로 닫기, 백드롭, focus trap, safe-area-inset 적용한 재사용 가능한 시트
  • 🤏 멀티터치 제스처 — 핀치 줌(1.18 ratio threshold), 두 손가락 팬, 두 번째 손가락 닿으면 진행 중이던 그리기 자동 중단
  • 📐 태블릿 적응 — 사이드바 200px로 캡, 타임라인 기본 접힘, 툴바 56px(터치 친화)
  • 🔗 시스템 공유navigator.share 지원 시 OS 공유 시트, 미지원 시 기존 ShareDialog 폴백
  • 🔌 협업 끊김 알림 — 인증 실패/연결 끊김 시 sticky 배너 (데스크톱+모바일 공통)
  • 🎨 터치 친화 ColorPickerDialog — MouseEvent → PointerEvent 마이그레이션, setPointerCapture로 캔버스 밖 드래그 추적
  • 🏗 데스크톱 회귀 0건 — 기존 528개 테스트(useViewport+EditorRouter 추가 후 522개) 전부 통과

화면 소개

폰 — 새 스프라이트 다이얼로그

폰 새 스프라이트

처음 진입 시 데스크톱과 동일한 New Sprite 모달이 폰 화면에 맞게 렌더됩니다. 1024px 차단 화면이 사라지고 폰에서도 바로 시작 가능합니다.

폰 — 메인 편집

폰 메인 편집

캔버스가 화면의 약 65%를 차지하고, 상단에 AppBar, 그 아래 ToolOptions, 하단에 ToolBar(가로 스크롤)와 BottomTabBar 순으로 쌓입니다. 우측 하단 status chip은 좌표·줌 비율을 컴팩트하게 표시합니다.

폰 — 색상 & 팔레트 시트

Color Sheet

하단 탭바의 🎨 Color 누르면 RGB/HSV/HSL 슬라이더, hex 입력, DB-16 팔레트, foreground/background 스왑까지 한 시트에 펼쳐집니다.

폰 — 레이어 시트

Layers Sheet

블렌드 모드 + opacity 슬라이더 + 레이어 리스트 + 레이어 추가/삭제/병합/이동 버튼 묶음. 데스크톱 LayerPanel을 그대로 시트에 임베드 — 코드 재사용.

폰 — 프레임 & 애니메이션 시트

Frames Sheet

Timeline / Sheet / Preview 3탭, 프레임 편집 버튼, FPS 입력, 어니언 스킨 토글까지 전부 폰에서 동작.

폰 — 오버플로우 메뉴 (⋯)

Overflow Menu

데스크톱 MenuBar(File/Edit/Select/Sprite/Layer/Frame/View/Help — 8개)를 핵심 액션 5개(New, Export, Share, Preferences, Help)로 압축. 폰에서 자주 쓰는 항목만 남김.

태블릿 — 압축된 데스크톱 레이아웃

Tablet Editor

태블릿 뷰포트(640–1023px)에서는 데스크톱 그리드를 유지하되: 사이드바 200px로 캡, 타임라인 기본 접힘, 툴바 컬럼 56px로 확장. 별도 셸을 만들지 않아도 터치 환경에서 자연스럽게 동작.

기술 스택

영역기술
뷰포트 분기matchMedia 기반 useViewport() 훅 (SSR 안전)
멀티터치Pointer Events + setPointerCapture
시트자체 구현 BottomSheet (드래그 hit threshold 80px)
시스템 공유navigator.share (Web Share API)
안전 영역env(safe-area-inset-*) CSS
테스트Vitest + matchMedia stub + EditorRouter render test

아키텍처 — 라우팅 분기

App.tsx
  └── Route /editor → EditorRouter
                          │
                          ├── viewport === 'phone'   → MobileEditorLayout
                          │                              ├── MobileAppBar
                          │                              ├── CanvasArea
                          │                              ├── ToolOptionsBar
                          │                              ├── MobileToolBar
                          │                              ├── MobileBottomTabBar
                          │                              └── BottomSheet × 3 (Color/Layers/Frames)
                          │
                          └── viewport === 'tablet' or 'desktop'
                                                       → EditorLayout (viewport-aware grid)
                                                          ├── 'tablet' → 사이드바 200px max,
                                                          │              타임라인 접힘, 툴바 56px
                                                          └── 'desktop' → 현행 그대로

핵심 원칙: 데스크톱 레이아웃은 1픽셀도 바꾸지 않는다. 폰은 별도 셸, 태블릿은 같은 그리드의 viewport-aware 변형.

개발 방법론

디자인 우선

코드를 짜기 전 docs/mobile-editor-plan.md에 6단계(M1–M6) 롤아웃을 적었습니다. 폰 가로/세로 ASCII 와이어프레임, 제스처 매핑 표, 구현 순서, 열린 질문, Phase 종료 검증 기준까지. 이 문서가 결과적으로 4개 커밋의 가이드 역할을 했습니다.

Anti-over-engineering 적용

CLAUDE.md의 7가지 원칙(60초 룰, YAGNI, 최소 시작, 한 도구씩, 인터페이스는 필요할 때만, 조기 추상화 금지, 측정 후 최적화)을 곳곳에 적용:

  • 별도 useCanvasGestures 훅 만들지 않고 기존 useCanvas에 멀티터치 로직을 합쳤습니다. 두 훅이 똑같은 renderer ref를 공유해야 하므로 분리는 조기 추상화.
  • MOBILE_PINNED_TOOLS 같은 상수도 toolList.ts 한 파일에 평면적으로 둠. 별도 모듈 만들지 않음.
  • Long-press 스포이드는 첫 픽셀 commit과 충돌하는 미묘한 UX라 M3에서 의도적으로 보류 — 실기기 테스트 후 결정으로 미뤘습니다.

TDD는 도구

useViewport 훅과 EditorRouter 컴포넌트는 단위 테스트부터 작성. 단, 시각적 컴포넌트(MobileAppBar, BottomSheet)는 E2E/실기기 검증 영역이라 강제 TDD를 적용하지 않았습니다.

기술적 도전 & 해결

1. 핀치 줌 도중 의도치 않은 드로잉

폰에서 두 손가락으로 줌하려 했는데 첫 번째 손가락이 그리기 stroke를 시작해버리는 문제. 해결:

if (e.pointerType === 'touch') {
  activePointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
  if (activePointers.current.size >= 2) {
    gestureActive.current = true;
    isDrawing.current = false;  // 드로잉 즉시 중단
    // ...baseline distance/midpoint 기록
  }
}

두 번째 포인터가 닿는 순간 isDrawing 플래그를 false로 강제 해제. 추가로 pointerup 시에도 gesture 모드면 tool finalize 호출을 건너뜀.

2. 1024px 하드 차단 → 미디어 쿼리 게이팅

기존 EditorLayout.module.cssmin-width: 1024px가 박혀 있어 태블릿/폰에서 가로 스크롤이 발생. 단순히 제거하면 데스크톱에서도 작아질 수 있음. 해결:

.layout {
  min-height: 480px;   /* 항상 적용 */
}

@media (min-width: 1024px) {
  .layout {
    min-width: 1024px;  /* 데스크톱 한정 */
  }
}

미디어 쿼리로 데스크톱 한정 가드. 같은 그리드 정의가 폰/태블릿/데스크톱 모두에서 살아있음.

3. ColorPicker가 터치에서 동작 안 함

색상 휠/슬라이더가 onMouseDown/onMouseMove로 짜여 있어 터치에서 무반응. 해결:

const handleSVPointer = useCallback((e: React.PointerEvent<HTMLCanvasElement>) => {
  if (e.buttons !== 1) return;
  if (e.type === 'pointerdown') e.currentTarget.setPointerCapture(e.pointerId);
  // ...
}, [hsv.h, alpha, updateColor]);

React.MouseEventReact.PointerEvent로 통합, setPointerCapture로 캔버스 밖으로 끌어내도 추적 유지, CSS touch-action: none 추가로 브라우저 기본 제스처 차단.

4. Sheet 백드롭이 BottomTabBar 클릭 가로채는 이슈

스크린샷 작업 중 발견: BottomSheet가 열려있을 때 다른 탭(예: Color → Layers)을 누르면 backdrop(z-index 900)이 클릭을 흡수해서 sheet 변경이 안 됨. 사용자는 X 버튼이나 backdrop 탭으로 닫고 다시 열어야 함. 현재는 의도된 모달 동작이지만 모바일 UX 관점에서는 시트 간 직접 전환이 자연스러움. M6 실기기 QA 후 BottomTabBar의 z-index를 backdrop보다 높게 올리는 보정을 검토 예정.

숫자로 보는

항목
작업 기간1세션 (계획 + 구현 + 4커밋)
신규 파일12개 (useViewport, EditorRouter, MobileEditorLayout, MobileAppBar, MobileToolBar, MobileBottomTabBar, BottomSheet, CollabBanner, toolList, mobile plan doc)
수정 파일6개 (App.tsx, EditorLayout.tsx/css, ColorPickerDialog.tsx/css, useCanvas.ts, editorStore.ts, ShareDialog.tsx, ToolBar.tsx/css)
커밋4개 (foundation / phone shell / gestures / tablet+share+banner)
테스트522개 통과 (기존 519 + 신규 useViewport 5 + EditorRouter 3, 회귀 0)
빌드Vite 205 modules, 502KB → gzip 150KB
디자인 문서docs/mobile-editor-plan.md (M1–M6 6단계, 와이어프레임 + 제스처 표)

배운 것

  1. 종이 위에서 끝낸 설계가 코드 작성을 빠르게 한다. ASCII 와이어프레임과 제스처 매핑 표를 미리 그리니 4개 커밋이 막힘없이 이어졌습니다. 디자인 문서가 PR 설명을 자동으로 만들어준 셈.
  2. Pointer Events는 마우스+터치+펜의 통합 인터페이스. setPointerCapture 하나로 마우스 드래그도, 터치 슬라이드도, 펜 입력도 같은 코드 경로. ColorPicker와 캔버스 둘 다 이 패턴으로 통일.
  3. viewport-aware는 별도 셸 vs 같은 그리드 변형 두 갈래. 폰은 데스크톱과 멘탈 모델이 너무 달라서 별도 컴포넌트, 태블릿은 그리드 파라미터만 조정. 1픽셀도 데스크톱 회귀 없이.
  4. 모달 시트의 backdrop은 양날의 검. 포커스를 가두는 데는 좋지만 같은 카테고리의 다른 시트 사이 전환을 막음. 모바일 네이티브 앱들이 BottomTabBar를 항상 위에 두는 이유를 직접 부딪혀 알게 됨.

링크