Xprite 작동 방식
Xprite는 Aseprite 에디터를 브라우저로 옮겨 왔습니다. 설치할 것도, 만들 계정도, 작품을 저장하는 서버도 없습니다. 이 글은 브라우저에서 .aseprite를 읽고 쓰는 방법, Canvas로 선명한 픽셀을 그리는 방법, 작품이 저장되는 곳, 인터넷 연결 없이 에디터가 열리는 방식, 그리고 마우스용으로 만든 데스크톱 화면을 손가락과 펜, 휴대폰 화면에 맞추는 방법을 기술적인 관점에서 설명합니다.
전체 구조
에디터의 모든 작업은 브라우저 안에서 이루어집니다. 무거운 작업은 Web Worker에서 실행되고, 저장에는 브라우저 자체의 IndexedDB와 OPFS를 사용합니다. 네트워크는 페이지를 불러오고, 익명 분석을 보내고, 피드백을 제출할 때만 씁니다.
브라우저에서 .aseprite 읽고 쓰기
파일 형식
.aseprite는 리틀 엔디언 바이너리 형식으로, 128바이트 헤더 뒤에 여러 청크로 이루어진 프레임이 이어집니다. 헤더의 매직 넘버는 0xA5E0, 각 프레임의 매직 넘버는 0xF1FA입니다. 디코더는 다음 청크를 처리합니다.
| 청크 유형 | 내용 |
|---|---|
0x2004 |
레이어: 일반 레이어, 그룹, 타일맵 레이어 |
0x2005 |
셀: 원본 픽셀, 연결된 셀, zlib 압축 픽셀, 압축 타일맵 |
0x2006 |
셀 추가 데이터(정확한 위치와 크기) |
0x2007 |
색 프로필 |
0x2008 |
외부 파일 참조 |
0x2018 |
태그(애니메이션 범위) |
0x2019 |
팔레트 |
0x2020 |
사용자 데이터(텍스트, 색, 속성) |
0x2022 |
슬라이스 |
0x2023 |
타일셋 |
예전의 0x0004, 0x000B 팔레트 청크도 읽으며, 더 이상 쓰이지 않는 마스크(0x2016)와 경로(0x2017) 청크도 인식합니다.
먼저 검사하고 디코딩하기
브라우저에서 여는 파일은 어디서 왔는지 알 수 없으므로 디코딩은 두 단계로 진행합니다. 첫 단계에서는 파일 전체를 훑으며 모든 청크의 길이, 오프셋, 개수가 파일 안에 있는지 확인합니다. 실제 디코딩은 그다음에 시작됩니다. 손상된 파일이나 제한을 넘는 파일은 도중에 실패하는 대신 검사 단계에서 걸러집니다. 현재 제한은 다음과 같습니다.
| 항목 | 제한 |
|---|---|
| 캔버스 한 변 | 32,768픽셀 |
| 이미지 한 장 | 6,400만 픽셀 |
| 프레임 | 4,096개 |
| 레이어 | 256개 |
Worker에서 디코딩하기
수십 개 프레임의 압축을 푸는 데는 시간이 걸리고, 메인 스레드에서 하면 화면이 멈춥니다. 파일을 열면 전용 디코딩 Worker에 넘기고, 디코딩한 픽셀 버퍼는 Transferable 객체로 복사 없이 메인 스레드에 돌려줍니다. 브라우저가 Worker를 지원하지 않거나 Worker가 실패하면 메인 스레드에서 디코딩합니다.
픽셀 데이터는 zlib으로 압축되어 있습니다. 브라우저 기본 DecompressionStream("deflate")를 우선 사용하고, 압축 폭탄을 막기 위해 출력을 청크 헤더에 선언된 크기로 제한합니다. 지원하지 않으면 fflate를 씁니다. 다른 압축 해제 경로는 선언된 크기와 정확히 같은 출력 버퍼를 할당하고 Adler-32 체크섬을 확인합니다.
필요한 픽셀만 압축 풀기
파일을 열 때 RGB 셀은 검증하지만 전부 압축을 풀지는 않습니다. 원래의 압축 바이트를 메모리에 두고, 각 프레임은 필요할 때 압축을 풉니다. 큰 프로젝트도 한꺼번에 메모리에 풀 필요가 없습니다.
바꾸지 않은 내용은 그대로 다시 쓰기
.aseprite로 저장할 때 인코더는 손대지 않은 내용을 그대로 둡니다.
- 바뀌지 않은 셀은 열 때부터 보관한 압축 바이트로 다시 압축하지 않고 씁니다.
- 알 수 없는 청크는 그대로 다시 쓰므로, 새 버전 Aseprite로 만든 파일을 Xprite에서 저장해도 데이터가 사라지지 않습니다.
- 다시 압축한 셀은 압축본이 원본 데이터보다 작을 때만 압축본을 씁니다.
- 압축에는 브라우저 기본
CompressionStream을 씁니다. 지원하지 않으면 압축하지 않고 저장합니다. 용량은 커지지만 Aseprite에서 열 수 있습니다.
Canvas 2D만으로 캔버스 그리기
Xprite는 WebGL을 쓰지 않습니다. 픽셀 아트 캔버스는 보통 작고, 어려운 점은 모든 픽셀을 선명하게 유지하는 것입니다. 어떤 확대 배율과 화면 밀도에서도 픽셀은 브라우저가 흐릿하게 뭉갠 모습이 아니라 가장자리가 또렷한 정사각형으로 보여야 합니다.
레이어에서 화면까지
그리는 동안 에디터는 스트로크마다 바뀐 사각형을 기록하고, 렌더링할 때는 매번 이미지 전체를 다시 보내는 대신 그 영역만 이미지 캔버스에 씁니다. 완성된 뷰(픽셀, 체크무늬, 격자)는 기본 캔버스에 캐시합니다. 캐시 키에는 픽셀 버전, 뷰 위치, 표시 설정 등이 들어갑니다. 커서를 움직여도 키가 바뀌지 않으므로 기본 캔버스를 재사용하고 브러시 커서 미리 보기만 다시 그립니다.
정수 배율과 소수 배율
마지막 단계에서 뷰를 화면에 그립니다. 뷰가 화면에 정확한 정수 배율로 대응하면 이미지 스무딩을 끄고 브라우저의 drawImage에 맡깁니다.
if (Number.isInteger(scaleX) && Number.isInteger(scaleY) /* and no offset */) {
presentation.imageSmoothingEnabled = false;
presentation.drawImage(sourceCanvas, 0, 0, sourceWidth, sourceHeight,
0, 0, canvas.width, canvas.height);
return;
}
presentation.putImageData(renderer.render(context.getImageData(...)), 0, 0);
화면 밀도 1.5나 브라우저 확대 110% 같은 소수 배율에서는 브라우저마다 drawImage 샘플링 방식이 다릅니다. 픽셀 열의 너비가 고르지 않거나 이미지 전체가 반 픽셀 밀립니다. 이럴 때는 최근접 이웃 샘플링을 직접 합니다. 대상 크기에 대해 각 열과 행이 어느 원본 픽셀에 대응하는지 미리 계산해 두 개의 Int32Array 조회 테이블에 저장합니다. 샘플링은 각 픽셀을 Uint32 하나로 복사하고, 같은 행이 이어지는 부분은 copyWithin으로 통째로 복사합니다.
인터페이스도 같은 방식으로, 모든 컨트롤을 논리 픽셀당 2샘플로 그립니다. 브라우저 창 크기를 바꾸면 작업 공간 크기만 바뀌고 버튼과 메뉴의 크기는 그대로입니다.
이미지 가져오기와 내보내기
PNG, JPEG, WebP 등 이미지의 인코딩과 디코딩은 브라우저가 합니다.
- 디코딩은
createImageBitmap을 우선 쓰고<img>요소를 대체 경로로 씁니다. - 인코딩은
OffscreenCanvas.convertToBlob을 우선 쓰고<canvas>의toBlob을 대체 경로로 씁니다. - 브라우저가 형식을 지원하지 않으면
toBlob은 오류 없이 조용히 PNG를 돌려줍니다. 그래서 인코딩 뒤에 결과의 실제 유형을 확인하고, 내보내기 전에 1×1 테스트 이미지를 인코딩해 브라우저가 어떤 형식을 내보낼 수 있는지 알아냅니다.
작품이 저장되는 곳
프로젝트를 저장하는 곳은 두 가지이며, 다른 이름으로 저장의 저장 위치에서 고릅니다.

| 위치 | 사용하는 브라우저 기능 | 사용할 수 없을 때 |
|---|---|---|
| 파일 관리자 | File System Access API | 대신 파일을 다운로드 |
| 브라우저 | IndexedDB + OPFS | 프로젝트 데이터를 대신 IndexedDB에 저장 |
브라우저 프로젝트는 이 기기의 이 브라우저에만 있고 동기화되지 않습니다. 사이트 데이터를 지우면 함께 삭제됩니다. 사용 가이드를 참고하세요.
파일 관리자: 원본 파일에 바로 다시 쓰기
File System Access API를 지원하는 브라우저(데스크톱 Chrome, Edge 등)에서는 열기와 저장이 시스템 파일 대화 상자를 띄웁니다. 받는 것은 파일 사본이 아니라 파일 핸들입니다.
- 파일 핸들은 IndexedDB에 저장되므로, 다음 저장 때는 위치를 묻지 않고 같은 파일에 다시 씁니다.
- 쓰기 전에 읽기·쓰기 권한을 요청한 다음
createWritable()로 씁니다. - 브라우저는 파일 대화 상자가 사용자 클릭에서 바로 열리기를 요구하므로, 클릭 직후 다른 비동기 작업 없이
showSaveFilePicker()를 호출합니다. - 페이지가 다른 사이트의 iframe(예: 게임 포털)에 삽입되어 있으면 브라우저가 파일 대화 상자를 허용하지 않습니다. 이때 Xprite는 지원하지 않는 브라우저에서처럼 파일을 다운로드합니다.
브라우저: IndexedDB 목록과 OPFS 데이터
브라우저에 저장한 프로젝트의 목록(이름, 크기, 수정 시각, 현재 버전)은 항상 IndexedDB에 있습니다. 프로젝트 데이터는 가능하면 OPFS(오리진 비공개 파일 시스템. 브라우저가 사이트마다 주는 비공개 디스크 영역)에 저장합니다. OPFS는 브라우저가 Worker, navigator.storage.getDirectory(), Web Locks, 쓰기 가능한 파일 핸들을 지원할 때만 씁니다. 저장 방식은 프로젝트를 만들 때 정해지고 바뀌지 않으므로, 브라우저 지원이 달라져도 예전 프로젝트가 사라지지 않습니다.
OPFS 읽기·쓰기는 전용 Worker에서 이루어지며, 동기 접근 핸들(createSyncAccessHandle)로 쓰기, 자르기, 플러시를 합니다. 저장할 때마다 다음 단계를 거칩니다.
데이터 손상을 막는 규칙이 몇 가지 있습니다.
- 쓴 데이터는 추가만 합니다. 각 키는 한 번만 쓸 수 있고, 도중에 실패한 파일은 삭제합니다.
- 현재 버전을 바꾸는 것은 마지막 목록 트랜잭션뿐이며, 그것도 다른 탭이 먼저 현재 버전을 바꾸지 않았을 때만 합니다(비교 후 교체).
- Xprite가 여러 탭에서 열려 있으면 Web Locks로 쓰기를 서로 배제합니다.
.aseprite청크 경계를 따라 나누므로 바뀌지 않은 프레임과 레이어는 같은 세그먼트에 남습니다. 프레임 하나를 편집하면 바뀐 세그먼트만 씁니다.
자동 복구
자동 복구는 브라우저 저장과 같은 저장 경로를 씁니다. 편집하는 동안 Xprite는 기본적으로 2분마다 복구 데이터를 저장하고 7일 동안 보관합니다. 간격과 일수는 편집 → 환경설정 → 파일에서 바꿀 수 있습니다. 브라우저가 갑자기 닫혔다면 홈에서 파일 복구를 선택해 작업을 되찾으세요.
공유 링크: 스프라이트는 # 뒤에 있습니다
파일 → 공유는 아무것도 업로드하지 않습니다. 스프라이트는 압축되어 링크의 #share= 뒷부분(URL 프래그먼트)에 들어갑니다. 브라우저는 # 뒤의 내용을 서버에 보내지 않으므로 공유할 때 서버를 거치지 않습니다.

- 묶기, 압축, 디코딩은 모두 Worker에서 실행됩니다. Zstd는 WebAssembly로 실행되며, 먼저 레벨 19로 압축하고 1MiB보다 작은 결과는 더 높은 레벨로 다시 압축합니다. WebAssembly를 불러오지 못하면 DEFLATE만 쓰며, 링크는 그래도 열립니다.
- QR 코드의 영숫자 모드는 글자당 5.5비트만 써서 바이트 모드보다 촘촘하므로, QR 코드에는 이 모드의 문자로만 이루어진 Base43 인코딩을 씁니다.
- 링크를 열면 에디터는
#share=뒤의 데이터를 읽고, 곧바로history.replaceState로 주소 표시줄에서 지운 다음 Worker에 넘겨 디코딩합니다. - 1,800자가 넘는 링크에는 일부 채팅 앱에서 잘릴 수 있다는 경고가 붙습니다. 8,192자가 넘는 링크는 만들지 않습니다.
링크에는 편집 가능한 프로젝트 전체가 들어 있습니다. 전체 링크를 가진 사람은 누구나 열 수 있고, 링크는 만료되지 않으며 취소할 수도 없습니다. 링크나 QR 코드를 채팅 앱으로 보내면 그 앱도 작품 데이터를 받게 됩니다.
오프라인 사용: 한 번에 완전한 버전 하나
Xprite를 바탕 화면이나 홈 화면에 추가하면 Service Worker가 에디터를 캐시해 인터넷 없이도 열립니다(캐시하려면 먼저 온라인에서 에디터를 한 번 열어야 합니다. 방법은 사용 가이드를 참고하세요). 캐시 전략은 버전 일관성을 지키는 데 맞춰져 있습니다.
- 빌드마다 해당 버전의 리소스 매니페스트를 만들고, 모든 파일에 SRI 해시를 붙입니다. 설치할 때
cache.addAll로 매니페스트 전체를 한 번에 캐시합니다. 파일 하나라도 다운로드에 실패하거나 해시가 맞지 않으면 그 버전은 적용되지 않으므로, 새 파일과 옛 파일이 섞이지 않습니다. - 모두 캐시하면 "준비 완료" 표시를 쓰고, 오프라인 실행은 이 표시가 있는 버전만 씁니다.
- 새 버전은 설치되자마자 적용되지만, 이미 열린 페이지는 예전 코드로 계속 실행됩니다. 해시가 붙은 파일 이름의 옛 에셋은 남겨 두어 그 페이지들도 불러올 수 있습니다.
- 빌드 결과물만 캐시합니다. 분석, 피드백 엔드포인트, 웹사이트 페이지는 캐시를 거치지 않습니다.
- 옛 버전의 캐시는 모든 에디터 창이 닫힌 뒤에만 삭제합니다.
지원하는 브라우저에서는 영구 저장소(navigator.storage.persist())도 요청해, 공간이 부족할 때 브라우저가 작품을 지울 가능성을 낮춥니다.
터치스크린과 펜
마우스 포인터는 하나지만, 터치스크린에는 펜과 여러 손가락이 동시에 닿을 수 있습니다. 입력 계층은 다음 규칙으로 처리합니다.
| 상황 | 동작 |
|---|---|
| 펜과 손가락이 동시에 닿음 | 펜이 우선이며 손가락 동작을 중단 |
| 펜을 감지한 뒤 | 손바닥 오입력을 막기 위해 기본적으로 한 손가락은 캔버스 이동 |
| 두 번째 손가락이 닿음 | 현재 스트로크를 취소하고 두 손가락 제스처 시작 |
| 두 손가락 핀치 | 캔버스 확대/축소 |
| 두·세 손가락 탭 | 실행 취소 또는 다시 실행 |
| 손가락이 닿은 뒤 | 탭인지 드래그인지 먼저 판단하고 편집 시작 |
드래그 기준값도 포인터 유형에 따라 다릅니다. 손가락은 8 CSS 픽셀 넘게 움직여야 드래그로 보지만, 마우스나 펜은 1픽셀이면 충분합니다.
더 섬세한 스트로크
브라우저는 처리를 줄이려고 한 프레임 안의 여러 pointermove 이벤트를 하나로 합칩니다. 펜으로 그릴 때는 getCoalescedEvents()에서 합쳐진 샘플을 모두 꺼내고 각각의 필압 값도 씁니다. 브라우저는 예측 샘플도 제공하지만 쓰지 않습니다. 예측한 점은 실제 스트로크와 다를 수 있고, 실행 취소 기록에 들어가면 안 되기 때문입니다.
/** Real samples only: predicted input must never reach document history. */
const coalesced = event.getCoalescedEvents?.() ?? [];
const actual = coalesced.filter(
(point) => point.pointerId === event.pointerId && point.pointerType === "pen",
);
아이패드의 Apple Pencil
아이패드 Safari에서는 Apple Pencil로 닿으면 기본적으로 텍스트 선택이나 손글씨 입력(Scribble) 같은 시스템 동작이 일어납니다. React의 터치 리스너는 패시브라서 기본 동작을 막을 수 없으므로, 캔버스와 끌 수 있는 컨트롤에 패시브가 아닌 네이티브 touchstart 리스너를 직접 등록하고, touchType이 stylus일 때만 preventDefault()를 호출합니다. 텍스트 입력란 같은 편집 가능한 영역에는 영향이 없어서 그곳에서는 여전히 손글씨로 입력할 수 있습니다.
레이아웃: 데스크톱 창에서 세로 휴대폰까지
Xprite의 인터페이스는 Aseprite 1.3을 재현합니다. 대화 상자 크기와 컨트롤 위치는 Aseprite 1.3.18을 측정한 값이며, 테스트로 일치를 유지합니다. 테마 그래픽은 Aseprite의 픽셀 스타일을 따르고, 중국어·일본어·한국어 인터페이스에는 Fusion Pixel 글꼴을 씁니다.
넓은 화면에서는 위에 메뉴 막대, 왼쪽에 팔레트, 오른쪽에 도구, 아래에 타임라인이 있는 익숙한 데스크톱 레이아웃이 됩니다.

이 레이아웃은 세로로 든 휴대폰에는 맞지 않습니다. 작업 공간 레이아웃이 자동(기본값)이면 쓸 수 있는 공간에 맞춰 레이아웃을 고릅니다. 넓은 레이아웃이나 좁은 레이아웃으로 고정할 수도 있습니다. 사용 가이드를 참고하세요. 규칙은 다음과 같습니다.
| 기준 | 조건 | 결과 |
|---|---|---|
| 쓸 수 있는 영역의 가로세로 비율 | 1.2 이상 | 넓은 레이아웃: 패널을 가장자리에 배치 |
| 쓸 수 있는 영역의 가로세로 비율 | 1.2 미만 | 좁은 레이아웃: 도구를 위에 한 줄로, 팔레트와 타임라인은 캔버스 아래 탭으로 |
| 쓸 수 있는 영역의 너비 | 768 CSS 픽셀 미만 | 인터페이스를 촘촘한 밀도로 전환 |
| 터치 지점이나 포인터 정밀도 | 터치 지점이 있거나 (pointer: coarse) |
터치 조작 모드 |
좁은 레이아웃에서는 메뉴 막대가 왼쪽 위 ☰ 버튼으로 접히고, 인터페이스는 노치나 둥근 모서리 같은 안전 영역을 피해 배치됩니다.

레이아웃과 조작 모드는 따로 정해집니다. 가로로 든 아이패드는 넓은 레이아웃과 터치 조작을 함께 씁니다. 데스크톱 브라우저 창을 좁히면 좁은 레이아웃으로 바뀌지만 마우스 조작은 그대로입니다.
Xprite가 보내는 데이터
편집, 저장, 공유에는 네트워크를 쓰지 않습니다. 보내는 데이터는 두 가지뿐입니다.
| 데이터 | 보내는 때 | 포함 | 미포함 |
|---|---|---|---|
| 사용 분석 | 공식 웹 버전을 쓸 때 | 페이지 방문, 기능 사용, 오류 유형, 브라우저 기능, 대략적인 위치 | 작품 픽셀, 문서 이름, 로컬 파일 경로 |
| 피드백 | 문제 및 제안을 제출할 때 | 피드백 유형, 메시지, 선택 사항인 이메일 주소, 현재 방문 정보 | — |
사용 분석은 PostHog Cloud(미국)로 갑니다.
- 방문자 ID는 브라우저의
localStorage에 저장됩니다. - 원래 IP 주소는 대략적인 위치를 더한 뒤 버립니다.
- 세션 녹화와 자동 클릭 수집은 모두 꺼져 있습니다.
- 브라우저에서 추적 안 함(Do Not Track)을 켜고 Xprite를 다시 열면 더 이상 사용 데이터를 보내지 않습니다.
자세한 내용은 개인정보 안내를 참고하세요.
오픈 소스
여기서 설명한 모든 소스 코드는 GitHub에서 볼 수 있습니다. 에디터 코드는 GPL-2.0-only 라이선스입니다.