데스크탑 앱
ArtCraft는 이미지·영상 생성을 중심으로, 캔버스 편집과 3D 장면 구성, 여러 생성 제공자 연결을 묶은 데스크톱 창작 도구입니다. README는 이를 “예술가를 위한 IDE”라고 소개합니다. 개발자가 코드 편집기 안에서 여러 도구를 쓰듯, 창작자가 장면 구성과 생성 결과를 한 흐름 안에서 다루게 하려는 방향입니다.
이 저장소에서 배울 핵심은 AI 모델의 내부보다, 창작 화면과 외부 생성 작업을 안정적으로 연결하는 앱의 구조입니다. 기본 앱은 React 화면을 Tauri로 감싸고 Rust가 인증·네트워크·작업 추적을 담당합니다. 루트 Cargo.toml의 default-members도 crates/desktop/artcraft를 가리킵니다.
2D 캔버스는 콘티 보드, 3D 장면은 가상 세트장입니다. 창작자가 소품·배우·카메라를 배치하면, 앱이 그 의도를 생성 서비스에 전달합니다. 모델은 결과물을 만드는 제작팀이고, 작업 ID와 로컬 DB는 촬영 주문서와 진행 장부입니다.
감독석이 내 컴퓨터에 있다고 제작팀까지 모두 내 컴퓨터에서 일하는 것은 아닙니다. 현재 코드의 주요 생성 경로는 외부 API를 호출합니다. 화면 편집·미리보기와 모델 추론을 구분해야 하드웨어 요구와 개인정보 흐름도 이해할 수 있습니다.
| 사용자 행동 | 앱이 하는 일 | 읽어볼 위치 |
|---|---|---|
| 그림을 배치한다 | Konva의 레이어·선·이미지와 편집 상태를 관리합니다. | frontend/libs/components/pagedraw/ |
| 장면을 구성한다 | Three.js 장면과 카메라를 편집하고 UI에 상태를 알립니다. | frontend/libs/components/pagescene/ |
| 생성을 누른다 | Tauri 명령으로 요청을 전달하고 생성 API의 작업 ID를 받습니다. | crates/desktop/artcraft/src/core/commands/generate/ 아래의 Rust 코드 |
| 완료를 기다린다 | 원격 상태와 로컬 작업을 맞추고 결과 이벤트를 보냅니다. | crates/desktop/artcraft/src/services/storyteller/threads/ |
화면 편집은 프론트엔드 라이브러리에서, 계정과 작업 추적은 데스크톱 Rust 크레이트에서 찾을 수 있습니다. 한 기능의 양쪽 끝을 함께 읽으면 “누가 데이터를 가지고 있고, 누가 다음 행동을 결정하는가”가 드러납니다.
코드의 또 다른 입구는 artcraft-native/입니다. egui 기반의 별도 Rust 앱으로, 기본 Tauri 앱과 API 클라이언트 크레이트를 공유합니다. 같은 제품 영역을 웹 기술과 Rust UI로 각각 풀어낸 사례라 비교 학습에 좋습니다.
선정 기준은 TrendShift Monthly #5이며, 2026-10-23 GitHub 기준 스타는 35,748개입니다. 다만 순위와 누적 스타만으로 특정 기능이 유행의 직접 원인이라고 증명할 수는 없습니다. 아래는 README의 제품 방향과 소스 구조에서 읽을 수 있는 관심을 끌 만한 이유입니다.
pagedraw와 pagescene이라는 큰 편집 모듈이 분리되어 있습니다.artcraft_router에 제공자별 클라이언트와 요청 변환이 모여 있습니다. 계정·인증·지원 조합에 따른 제약도 코드에 드러납니다.ArtCraft의 설계적 매력은 모델 선택지를 늘리는 데 더해, 생성 전의 시각적 의도를 편집 가능한 상태로 만든다는 점에 있습니다. 다만 결과의 품질·속도가 다른 제품보다 높다는 비교 실험은 이 코드 분석으로 확인할 수 없습니다.
| 작업 방식 | 주로 직접 다루는 것 | ArtCraft에서 얻는 이점과 선택 기준 |
|---|---|---|
| 단일 모델의 입력 화면 | 문장·참조 이미지·생성 옵션 | 장면·캔버스와 생성 결과를 연결할 수 있습니다. 한 모델의 간단한 입력만 필요하면 통합 편집기가 과할 수도 있습니다. |
| ComfyUI | 노드 그래프로 표현한 생성 작업 흐름 | ArtCraft는 장면과 화면 조작을 출발점으로 삼습니다. 생성 파이프라인 자체를 조립하고 재사용하려면 ComfyUI의 접근도 함께 살펴볼 만합니다. |
| 직접 만든 API 연동 화면 | 한두 API의 요청·응답 | 제공자 변환, 작업 복구, 결과 알림처럼 운영 단계에서 필요한 설계를 한꺼번에 관찰할 수 있습니다. |
ComfyUI의 노드 그래프·재사용 작업 흐름 설명은 공식 README 기준입니다. 위 표는 작업 방식의 비교이며 기능 전수 조사나 성능 순위가 아닙니다. ComfyUI에도 클라우드 사용 경로가 있으므로 “ComfyUI는 로컬, ArtCraft는 클라우드”처럼 양분하면 부정확합니다.
파일 업로드, 외부 작업 접수, 상태 조회, 결과 표시를 처음부터 한 함수에 넣지 않는 방법을 관찰하세요. 채팅 앱의 첨부파일 처리나 보고서 생성 서비스에도 그대로 적용할 수 있는 설계 문제입니다.
README의 “62개 모델” 카탈로그에는 비활성·제한 표시가 섞여 있습니다. 실제 화면도 서버의 모델 목록을 받아 필터링합니다. 따라서 카탈로그 숫자를 “지금 누구나 실행할 수 있는 모델 수”로 읽어서는 안 됩니다.
이 저장소의 “백엔드”는 우선 데스크톱 앱 안의 Rust 코어를 뜻합니다. 운영 API 서버 전체가 들어 있는 것은 아닙니다. AGENTS.md와 _docs/dev_setup.md는 서버·회사 도구·웹사이트 빌드가 별도 artcraft-services 저장소에 있다고 설명합니다.
| 기술 | 실제 버전 근거 | 이 저장소에서 맡는 역할 |
|---|---|---|
| Tauri | 루트 Cargo.toml: 2.11.2Cargo.lock: 2.11.2 | Rust 코어와 시스템 WebView를 연결하고 명령·이벤트·플러그인을 제공합니다. |
| Tokio | 워크스페이스: 1.50.0데스크톱 크레이트: 1.42잠금 파일: 1.50.0 | HTTP 대기와 반복 상태 조회를 비동기로 처리합니다. 선언 문자열이 다르더라도 잠금 결과는 하나로 합쳐질 수 있습니다. |
| Serde | 루트: 1.0.228serde_json: 1.0.149 | Rust 타입과 JSON을 변환합니다. Omni 요청에서는 동적으로 추가되는 옵션도 보존합니다. |
| reqwest / wreq | 루트: 0.12.23 / 6.0.0-rc.28 | HTTP 클라이언트입니다. wreq 관련 네이티브 빌드 도구도 필요합니다. 잠금 파일에는 reqwest 0.13.2도 공존합니다. |
| SQLx + SQLite | crates/schema/database/sqlite_tasks/Cargo.toml: 0.7.4 | 파일 기반 로컬 작업 DB입니다. query!와 오프라인 쿼리 메타데이터를 사용합니다. |
| egui / eframe | artcraft-native/Cargo.toml: 0.36잠금 파일: 모두 0.36.2 | 별도 Rust UI 앱을 구성합니다. 기본 Tauri 앱의 UI 라이브러리는 아닙니다. |
데스크톱 Cargo.toml에는 Rust edition 2021과 rust-version = "1.77.2"가 남아 있습니다. 그러나 sqlite_tasks와 artcraft_router는 edition 2024입니다. 이 단일 필드를 저장소 전체의 최소 Rust 버전 보장으로 해석하면 안 됩니다. CI는 stable 툴체인을 사용합니다.
| 기술 | frontend/package.json 선언 | 소스에서 확인할 학습 주제 |
|---|---|---|
| React / React DOM | 각 18.3.1 | 편집 화면·모달·상태 구독을 컴포넌트로 조립합니다. |
| TypeScript | 5.8.3 | 앱과 재사용 UI 사이의 계약, API 요청 타입을 표현합니다. |
| Zustand | ^5.0.5잠금: 5.0.14 | 캔버스·장면 등 여러 화면 요소가 공유하는 편집 상태를 관리합니다. |
| Preact Signals | @preact/signals-react ^3.0.1 | 일부 인증·갤러리·화면 상태에 사용됩니다. 모든 상태가 Zustand 하나로 통일되어 있지는 않습니다. |
| Three.js | ^0.171.0잠금: 0.171.0 | 장면·카메라·조명·메시를 다룹니다. AI 추론과 별개로 로컬 그래픽 처리가 필요합니다. |
| Konva / react-konva | ^9.3.20 / 앱 선언 ^18.2.10 | 2D 그림·레이어·마스크와 조작 도구를 표현합니다. Konva 잠금 버전은 9.3.22입니다. |
| opencut-wasm | ^0.2.10 | 영상 편집기의 합성·효과 처리 경로에 연결됩니다. |
| Tailwind CSS | 3.4.3 | 화면 스타일을 구성합니다. 현재 문서의 스타일과는 별개의 앱 의존성입니다. |
Three.js의 기본 단위는 장면·카메라·렌더러입니다. 이 개념을 먼저 익히면 장면 편집 코드가 훨씬 쉬워집니다. 공식 장면 입문 문서는 이 세 요소의 관계를 설명합니다. ArtCraft 전체 엔진을 처음부터 읽기 전에 작은 장면 하나로 관계를 익히면 좋습니다.
^5.0.5 같은 선언은 허용 범위이고, 잠금 파일의 5.0.14는 의존성 해석 결과입니다. 잠금 파일을 읽었다고 설치·실행까지 검증한 것은 아닙니다. 모노레포에서는 하위 앱에 다른 버전이 따로 들어갈 수도 있습니다.| 구성 | 실제 설정 | 읽어야 할 의미 |
|---|---|---|
| Nx | 루트 선언 21.2.3 | 프론트엔드 앱과 라이브러리 작업을 관리합니다. |
| Vite | 루트 ^6.0.0, 앱 ^5.4.0잠금: 루트 6.4.3, 앱 내부 5.4.21 | 단순히 “Vite 6 앱”이라고 쓰면 버전 공존을 놓칩니다. 실제 실행 위치와 패키지 해석도 확인해야 합니다. |
| Tauri 빌드 연결 | beforeBuildCommand: npx nx run artcraft:build | 먼저 웹 자산을 만들고 frontendDist가 가리키는 결과물을 앱에 넣습니다. |
| SQLx 오프라인 구성 | .sqlx/, SQLX_OFFLINE=true | 쿼리 검사에 필요한 메타데이터를 사용합니다. 생성 서비스를 오프라인으로 대체하는 기능은 아닙니다. |
| GitHub Actions | macOS Universal / Windows 게시 워크플로 | 플랫폼 도구·인증서·프론트 빌드·패키징이 결합됩니다. Linux 배포가 같은 매트릭스에서 활성화된 것은 아닙니다. |
| SonarCloud | 공통 워크플로를 @main으로 호출 | 정적 분석 설정이 있습니다. 공유 워크플로 내부와 실제 분석 통과 여부는 이 저장소만으로 확정할 수 없습니다. |
frontend/apps/artcraft/vite.config.ts에는 wasm(), topLevelAwait(), 워크스페이스 소스 경로 해석과 React 중복 방지 설정이 있습니다. 라이브러리의 미리 빌드한 결과물 대신 소스를 연결하는 방식도 눈여겨보세요.
설정 근거: 루트 Cargo.toml, 프론트엔드 의존성, 앱 의존성, npm 잠금 파일, Cargo 잠금 파일.
먼저 실행 경계를 봅시다. 기본 앱의 main.rs는 artcraft_app_lib::run()을 호출합니다. crates/desktop/artcraft/src/lib.rs의 run()이 플러그인을 등록하고, .manage(...)로 상태를 보관하며, 명령 처리기를 연결합니다. 화면은 시스템 WebView 안에서 실행됩니다.
Tauri의 명령과 이벤트는 프로세스 사이의 통신 수단입니다. 화면에서 Rust를 부르는 것은 일반 함수처럼 보이지만, 실제로는 직렬화된 메시지가 경계를 통과합니다. 시스템 WebView와 코어의 역할은 공식 프로세스 모델과 IPC 문서를 참고하세요.
이 도식은 핵심 책임을 요약한 것입니다. 모든 HTTP 요청이 생성 명령을 통과하는 것은 아닙니다. frontend/libs/tauri-utils/src/lib/FetchProxy.ts는 데스크톱에서 Tauri HTTP 플러그인, 일반 브라우저에서는 기본 fetch를 선택합니다. 모델 목록·미디어 조회 등에는 별도의 HTTP 호출 경로도 있습니다.
crates/desktop/artcraft/src/core/commands/generate/generate_image/generate_image_command.rs의 generate_image_command()는 uses_artcraft()와 uses_legacy_image_endpoint()를 확인합니다. 일반 ArtCraft 요청은 omni::generate()로, 일부 기존 편집 모델과 다른 제공자는 기존 처리 경로로 보냅니다.
아래는 crates/desktop/artcraft/src/core/commands/generate/omni/request.rs의 핵심 구조입니다. 명시된 세 frontend_* 필드는 완료 결과를 받을 화면과 전달할 정보를 기록합니다. 이 필드들과 provider를 제외한 나머지는 JSON 맵에 모아 새 모델 옵션이 추가돼도 쉽게 탈락하지 않게 합니다.
// 핵심 구조 발췌: 로컬 메타데이터는 명시적인 필드
#[derive(Clone, Deserialize, Serialize)]
pub struct OmniRequest {
pub provider: Option<GenerationProvider>,
pub frontend_caller: Option<TauriCommandCaller>,
pub frontend_subscriber_id: Option<String>,
pub frontend_subscriber_payload: Option<String>,
#[serde(flatten)]
pub fields: Map<String, Value>,
}
OmniRequest::api_fields()는 API에 전달할 필드만 복사하고 이름을 정리합니다. 이미지의 batch_size를 image_batch_count로, 영상의 옛 image_media_token을 start_frame_image_media_token으로 바꾸는 식입니다. canonical_model_id()는 기존 모델 별칭을 새 ID로 바꾸고, 모르는 ID는 그대로 통과시킵니다.
// 학습용 입력: 외부 요청을 보내지 않고 변환 결과만 읽습니다.
{
"model": "future_image_v9",
"batch_size": 2,
"new_option": { "strength": 0.4 },
"frontend_subscriber_id": "canvas-tab-3"
}
// api_fields(Modality::Image)의 결과
{
"model": "future_image_v9",
"image_batch_count": 2,
"new_option": { "strength": 0.4 }
}
새 옵션 보존은 새 옵션의 유효성 보장과 다릅니다. 실제 지원 여부와 값 검증은 서버 응답까지 봐야 합니다. 파일 안의 future_model_and_options_survive_without_desktop_metadata 테스트는 이런 경계의 의도를 보여줍니다.
택배 상자 속 상품 설명은 보존하고, 창고에서만 쓰는 선반 번호는 떼고 보내는 과정과 같습니다. 서버가 이해할 새 상품 정보까지 앱이 미리 전부 열거할 필요는 없지만, 창고 내부 정보와 배송 정보는 구분해야 합니다.
여기에는 흥미로운 예외도 있습니다. uses_artcraft()는 grok_로 시작하는 모델이나 Grok 제공자 값도 ArtCraft 경로로 보냅니다. 파일 주석은 네이티브 Grok 경로를 의도적으로 비활성화했다고 설명합니다. 따라서 Grok 관련 디렉토리가 존재한다는 사실만으로 현재 기본 화면이 직접 Grok에 연결된다고 판단하면 안 됩니다.
crates/desktop/artcraft/src/core/commands/generate/omni/dispatch.rs의 generate()는 로그인 정보를 확인하고, 요청 필드를 정리한 뒤 /v1/omni_gen/generate/{종류}에 POST합니다. 서버 응답에서 받은 작업 ID를 로컬 DB에 기록하고 접수 이벤트를 보냅니다. 이 시점에 최종 이미지가 만들어진 것은 아닙니다.
idempotency_token이 없거나 null이면 UUID를 넣습니다.estimate_only는 실제 생성 요청에서 제거합니다.job_tokens()가 대표 ID와 all_job_tokens를 중복 없이 모읍니다.TaskEnqueueSuccess::insert_into_task_database_with_frontend_payload()로 기록합니다.생성 API의 접수 성공과 결과물의 완성은 서로 다른 사건이며, 작업 ID가 두 사건을 연결합니다. API를 부른 함수 하나가 끝날 때까지 화면 전체를 묶어두지 않는 것이 이 구조의 출발점입니다.
idempotency_token과 frontend_subscriber_id는 이름도 목적도 다릅니다.키가 자동 생성된다는 사실만으로 중복 실행을 완전히 막았다고 볼 수는 없습니다. 같은 논리적 요청을 다시 보낼 때 새 키를 만들면 서버는 별개의 요청으로 볼 수 있습니다. 재시도 동안 같은 키를 유지할지, 서버가 어떻게 보관하는지는 별도로 설계해야 합니다.
_database/sql/artcraft_migrations/20250708000000_create_tasks_table.sql에는 작업 상태·종류·제공자·원격 ID·화면 구독 정보·성공 결과·실패 이유가 함께 정의되어 있습니다. 이미지 파일 자체를 SQLite 안에 넣는 구조로 읽으면 안 됩니다.
-- 실제 스키마의 일부 필드만 추린 읽기용 예시
CREATE TABLE tasks (
id TEXT NOT NULL PRIMARY KEY,
task_status TEXT NOT NULL,
task_type TEXT NOT NULL,
provider TEXT,
provider_job_id TEXT,
frontend_subscriber_id TEXT,
frontend_subscriber_payload TEXT,
on_complete_primary_media_file_cdn_url TEXT,
on_failure_message TEXT
);
-- 원본에는 모델·배치·날짜·실패 유형 등의 필드도 있습니다.
crates/schema/database/sqlite_tasks/src/connection.rs의 run_migrations()는 SqliteJournalMode::Wal을 설정하고 마이그레이션을 바이너리에 포함합니다. crates/desktop/artcraft/src/core/state/task_database_version.rs의 버전은 7입니다.
crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread/storyteller_task_polling_thread.rs의 polling_loop()는 5,000ms를 기다린 뒤 list_session_jobs(..., States::All)을 호출합니다. 작업 ID로 로컬 레코드를 찾아 성공·실패 처리기로 넘깁니다. 실시간 서버 푸시가 이 경로의 중심은 아닙니다.
| 상황 | 실제 코드의 처리 | 학습할 지점 |
|---|---|---|
| 정상 상태 조회 | 내부 루프에서 매번 5초 대기 | 실제 조회 간격에는 HTTP 응답 대기와 작업 처리 시간도 더해집니다. |
| 조회 오류 | 오류를 반환하고 바깥 루프에서 30초 대기 | 장애가 났을 때 무한히 빠른 요청을 보내지 않습니다. |
| 요청 제한 응답 | 60초 추가 대기 후 오류 반환 | 30초 바깥 대기도 이어집니다. “항상 60초 후 재시도”가 아닙니다. |
| 이미 완료한 작업 | 로컬 CompleteSuccess 상태면 건너뜀 | 반복 조회에서 같은 작업을 계속 처리하는 일을 줄입니다. |
| 한 결과의 다운로드 실패 | 오류를 기록하고 다음 작업 계속 처리 | 한 항목의 실패가 다른 완료 작업을 지연시키지 않도록 합니다. |
handle_storyteller_successful_job.rs의 handle_successful_job()는 자동 다운로드 처리를 먼저 시도하고, DB 성공 상태를 갱신한 뒤 완료 이벤트를 보냅니다. 화면의 useGenerationCompleteEvent()는 generation-complete-event를 구독해 소리와 알림을 표시합니다.
이 순서는 유용하지만 정확히 한 번 전달을 보장하지는 않습니다. DB 갱신과 이벤트 전송이 하나의 원자적 트랜잭션은 아니기 때문입니다. DB 갱신 뒤 앱이 종료되면 그 이벤트가 다시 전송되지 않을 수 있습니다. 화면은 필요할 때 작업 목록을 다시 읽어 상태를 복원할 수 있어야 합니다.
외부 접수가 끝난 뒤 DB 저장에 실패하면 generate()는 오류 로그를 남기고 접수 성공 처리를 이어갑니다. 중복 생성을 부르는 재시도를 피하려는 선택이지만, 로컬 추적 기록이 빠질 수 있습니다. 또 connect_and_migrate()는 마이그레이션 실패 시 DB 파일을 삭제하고 재생성을 시도합니다. 이 DB를 영구 보관용 원장으로 간주해서는 안 됩니다.
crates/api_clients/artcraft/artcraft_router/src/client/router_client.rs의 RouterClient는 제공자별 클라이언트를 enum으로 표현합니다. MultiRouterClientBuilder는 사용할 클라이언트를 선택해 조립하고, 없는 클라이언트를 요청하면 ClientNotConfigured 오류를 돌려줍니다.
crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs의 GenerateImageRequestBuilder::build2()는 (provider, model) 조합을 보고 요청을 만듭니다. 미지원 조합은 명시적 오류가 됩니다. ImageGenerationRequest::send_request()는 실제 제공자 클라이언트로 전송합니다.
이 라우터의 지원 enum과 ArtCraft Omni의 동적 모델 경로는 같은 층이 아닙니다. ArtCraft Omni는 새 문자열 옵션을 전달하고, 라우터의 특정 경로는 알려진 제공자·모델 조합을 변환합니다. “모델을 하나 추가하면 모든 제공자에서 즉시 사용 가능”한 구조는 아닙니다.
화면에도 같은 원칙이 보입니다. frontend/libs/components/pagescene/src/lib/adapter.ts의 PageSceneAdapter는 생성·저장·업로드 등의 계약을 정의합니다. 앱 쪽 useTauriPageSceneAdapter.tsx가 Tauri·HTTP 연결을 채웁니다. 재사용 3D 편집기에 플랫폼 지식을 몰아넣지 않는 방식입니다.
frontend/libs/components/pagescene/src/lib/engine/events/EngineEventBus.ts의 EngineEventBus는 이벤트 생성자별로 리스너 집합을 보관합니다. subscribe()는 해제 함수를 반환하고, emit()은 해당 종류를 구독한 함수들만 호출합니다.
// EngineStoreBridge의 실제 연결 방식 발췌
bus.subscribe(SelectionChangedEvent, (e) =>
usePageSceneStore.getState().setSelectedObject(e.selection),
);
// 엔진의 선택 변경 → 화면의 선택 상태 변경
// 엔진 내부 전체가 React 컴포넌트를 알 필요는 없습니다.
EngineStoreBridge.ts의 생성자가 이러한 구독을 모읍니다. 장면 엔진이 객체를 선택했다고 알리면 브리지가 Zustand 저장소를 변경하고, 화면이 그 상태를 읽습니다. 이벤트 종류 검색은 맵으로 하지만 리스너 호출 비용은 해당 구독자 수에 비례합니다. “이벤트 처리가 전부 O(1)”이라고 확대하면 안 됩니다.
이 브라우저 내부 이벤트 버스와 Tauri 이벤트는 서로 다른 통신 수단입니다. 전자는 같은 JavaScript 실행 영역 안에서 엔진과 UI를 연결하고, 후자는 Rust 코어와 WebView의 경계를 넘습니다. “이벤트”라는 이름이 같아도 범위를 구분하세요.
artcraft-native/src/backend/mod.rs의 Backend::new()는 Tokio 작업 스레드 4개와 std::sync::mpsc::channel()을 만듭니다. Backend::drain()은 도착한 Event를 모아 UI에 넘깁니다. UI가 네트워크 응답을 직접 기다리지 않게 하는 역할은 비슷하지만, Tauri IPC를 사용하지 않습니다.
Native README는 이미지·영상·오디오 생성 화면을 구현했다고 설명하며, 다른 도구는 웹 앱으로 연결하는 준비 중 화면이라고 밝힙니다. 기본 Tauri 앱의 2D·3D 편집 기능이 Native에 모두 옮겨졌다고 볼 수 없습니다. 이 비교는 성능 우열보다 같은 비동기 경계를 다른 UI 환경에서 표현하는 방식에 초점을 맞추세요.
핵심 근거: Omni 생성·견적 처리, 요청 변환과 테스트, 엔진과 UI 연결.
| 위치 | 담는 것 | 처음 읽을 때의 질문 |
|---|---|---|
artcraft_api_defs | 서버와 주고받는 데이터의 형태 | 성공 응답에는 파일이 오는가, 작업 ID가 오는가? |
artcraft_client | 주소·인증·요청·응답 처리 | HTTP 오류가 어떤 Rust 오류로 바뀌는가? |
artcraft_router | 제공자 간 의미와 형식의 차이 | 같은 이미지 생성 의도를 각 제공자에 어떻게 맞추는가? |
tauri-api | 화면에서 Rust 명령을 부르는 함수 | 명령 이름과 전달하는 객체 모양이 일치하는가? |
tauri-events | Rust가 보낸 사건의 화면 구독 | 컴포넌트가 사라질 때 구독도 해제되는가? |
crates/vendor/는 모두 직접 작성한 코드라는 뜻이 아닙니다. 데스크톱 Cargo.toml 주석은 HTTP 플러그인의 쿠키 내부 접근 때문에 수정 사본을 사용한다고 설명합니다. 원본과 다른 책임이 생긴 만큼 업데이트 때 변경을 유지해야 하는 비용도 있습니다.
frontend/libs/tauri-api/src/lib/generate/GenerateImage.ts에서 요청의 입력 형태를 봅니다.generate_image_command()에서 경로 선택 조건을 읽습니다.OmniRequest와 generate()에서 필드 변환·원격 접수·로컬 기록을 찾습니다.storyteller_task_polling_thread.rs에서 완료를 발견하는 지점을 찾습니다.useGenerationCompleteEvent.ts로 돌아와 사용자에게 어떤 피드백이 보이는지 확인합니다.이 경로를 읽은 뒤 장면 편집이나 영상 합성으로 넓히면 됩니다. 수천 개 파일을 한꺼번에 이해하려 하기보다, 요청·응답·상태·이벤트 네 가지를 표시한 지도를 먼저 그리세요.
이 코드를 공부할 때는 정상 동작을 따라가는 데서 한 걸음 더 나아가, 응답이 늦거나 순서가 뒤집힐 때를 질문해 보세요. 비동기 앱의 어려움은 요청을 보내는 한 줄보다, 오래된 응답과 사라진 화면을 다루는 데 있습니다.
RouterClient는 가능한 제공자를 enum으로, MultiRouterClient는 설정되지 않은 클라이언트를 Option으로 표현합니다. getter는 Result를 반환하므로 “없음”이 조용히 넘어가지 않습니다.
실습 아이디어: 가짜 제공자 두 개를 가진 작은 enum을 만들고, 없는 클라이언트를 요청할 때 ClientNotConfigured에 해당하는 오류를 돌려주세요. 문자열 비교를 여러 곳에 흩뿌린 구현과 읽기 편의를 비교합니다.
OmniRequest의 메타데이터는 고정 타입이고 나머지는 Map<String, Value>입니다. 무엇을 컴파일 단계에서 제한하고, 무엇을 서버의 계약에 맡길지 선택한 사례입니다.
실습 아이디어: 새 모델 ID·새 옵션을 가진 JSON에 로컬 구독 ID도 넣으세요. 변환 뒤 새 옵션은 남고 구독 ID는 API 필드에서 빠지는지 표로 확인합니다. 필드가 null일 때와 아예 없을 때도 나눠 봅니다.
Tauri 앱은 비동기 명령과 폴링 루프를 사용하고, Native 앱은 Backend::drain()으로 채널에 도착한 이벤트를 모읍니다. 화면이 네트워크 응답을 기다리며 멈추지 않게 하는 두 가지 표현입니다.
실습 아이디어: 가짜 생성 작업이 몇 초 뒤 이벤트를 보내는 작은 프로그램을 설계하세요. 기다리는 동안 카운터나 선택 상태가 바뀌도록 해 “작업 중”과 “앱 정지”의 차이를 체감합니다.
create_task()의 SQL은 원격 ID와 화면 구독 정보를 함께 저장합니다. 생성 파일의 보관 위치와 작업의 상태 기록은 별개의 문제입니다. query!와 .sqlx/는 쿼리 형태의 검증을 돕지만, 원격 작업과 DB 사이의 원자성까지 보장하지는 않습니다.
실습 아이디어: “서버 접수 성공 → DB 쓰기 실패”와 “DB 완료 → 이벤트 전송 실패”를 각각 그려보세요. 사용자가 재시도 버튼을 누르면 어떤 중복이나 누락이 생길지 설명합니다.
frontend/libs/components/pagedraw/src/lib/stores/SceneState.ts의 setBaseImageInfo()는 baseImageLoadGen을 증가시키고, 이미지 로드 콜백에서 현재 세대와 비교합니다. 이전 선택의 이미지가 늦게 도착해 새 선택을 덮어쓰는 경쟁 상태를 막습니다.
실습 아이디어: A 이미지는 800ms, B 이미지는 100ms 뒤에 로드되게 모의 구현하세요. A를 고른 직후 B를 골라도 최종 화면이 B인지 확인합니다. 단순히 응답이 온 순서대로 상태를 바꾸면 왜 실패하는지 설명해 보세요.
// SceneState의 세대 번호 방식을 축약한 학습용 예시
let generation = 0;
async function selectImage(id: string) {
const myGeneration = ++generation;
const image = await loadMockImage(id);
if (myGeneration !== generation) return;
showImage(image);
}
// loadMockImage와 showImage는 별도 학습용 함수입니다.
frontend/libs/components/pagedraw/src/lib/hooks/useUpdateSnapshot.ts의 captureStageEditsBitmap()는 조작 핸들을 숨기고 필요한 레이어를 캡처한 뒤 finally에서 좌표·배율을 되돌리고 모든 레이어·핸들을 표시합니다. 기존의 숨김 상태까지 복원하는 것은 아닙니다. 저장할 그림과 사용자가 조작 중인 화면은 같지 않습니다.
frontend/libs/components/pagedraw/src/lib/components/Model3DOverlay.tsx는 정리 단계에서 애니메이션 프레임을 취소하고 controls·장면 객체·renderer를 해제합니다. 컴포넌트가 사라졌다고 그래픽 자원이 자동으로 전부 정리된다고 가정하지 않는 코드입니다.
실습 아이디어: 사각형과 선택 테두리를 함께 보여주는 작은 캔버스를 만들고, 내보내기에는 테두리가 포함되지 않게 하세요. 내보내기 오류가 나도 편집 화면이 복구되도록 설계합니다.
frontend/libs/components/video-editor/src/lib/services/renderer/compositor/wasm-compositor.ts의 WasmCompositor는 텍스처 캐시를 관리하며, 소스·내용 해시와 크기를 비교해 불필요한 재업로드를 줄입니다. frontend/libs/components/video-editor/src/lib/services/renderer/gpu-renderer.ts는 GPU 초기화 실패를 기록하고 사용 가능 여부를 별도로 보관합니다.
실습 아이디어: 고정 배경과 움직이는 자막을 나눠, 매 프레임 바뀐 레이어만 다시 계산하는 구조를 종이에 그려보세요. 성능은 언어 이름보다 재계산·복사·업로드 횟수에 크게 영향을 받습니다.
처음 공부할 때는 위 주제 중 하나만 골라도 충분합니다. 예를 들어 React 경험이 있다면 이미지 선택의 경쟁 상태부터, Rust를 배우고 있다면 Option과 Result를 쓰는 제공자 접근부터 시작하세요.
원격 모델을 호출하는 데 필요한 장비와 3D 장면을 부드럽게 편집하는 데 필요한 장비는 서로 다른 문제입니다. 주요 생성 명령이 외부 API로 향하므로, 이 앱을 사용한다는 이유만으로 로컬 대형 모델용 VRAM을 요구한다고 볼 근거는 없습니다. 반면 장면·영상 편집에는 로컬 그래픽·메모리 자원이 사용됩니다.
| 목적 | 확인되는 조건 | 판단할 때 주의할 점 |
|---|---|---|
| 소스 학습 | 텍스트 편집기와 파일 검색 도구 | API 계정·GPU·전체 빌드가 없어도 요청·스키마·이벤트 구조를 읽을 수 있습니다. |
| 지원 운영체제 | README: Windows·macOS 안정 버전, Linux 소스 빌드 안내 | 게시 CI는 macOS·Windows가 활성화되어 있습니다. OS별 최소 버전은 별도 배포 조건을 확인해야 합니다. |
| Node.js | 공통 사전 검사: 20+ | 앱 manifest의 >=18.0.0보다 현재 실행 스크립트의 조건이 엄격합니다. |
| Rust·Tauri CLI | CI: Rust stable 개발 문서 예시: CLI 2.10.0 | 앱 의존성의 Tauri 2.11.2와 CLI 버전은 서로 다른 항목입니다. |
| macOS 빌드 도구 | 루트 Cargo 주석: cmake, pkg-config, llvm | wreq 네이티브 의존성과 Tauri 플랫폼 도구가 함께 필요합니다. |
| Windows 빌드 도구 | 게시 CI: CMake·Strawberry Perl·pkgconfiglite·LLVM·NASM | 추가로 Rust와 Tauri가 요구하는 플랫폼 환경을 갖춰야 합니다. |
| Linux 환경 | Tauri의 WebKitGTK 계열 시스템 의존성 | 배포판별 패키지명이 다릅니다. Unix 실행기에 WebKit 관련 우회 설정도 있습니다. |
| 네트워크·계정 | 생성 API·인증·미디어 다운로드 경로 | 코드 공개와 서비스 무료 이용은 별개입니다. 제공자 계정·권한·크레딧 조건이 영향을 줍니다. |
README와 개발 환경 문서에는 제품 전체의 공식 최소 RAM·VRAM·디스크 용량 표가 없습니다. 따라서 임의의 “최소 8GB”나 “VRAM 12GB 필수”를 공식 요구사항으로 제시할 수 없습니다. Rust 빌드 캐시와 npm 의존성, 편집 미디어가 모두 디스크를 차지하므로 소스 폴더 크기를 설치 용량으로 쓰면 안 됩니다.
Windows 게시 워크플로는 프론트 빌드에 NODE_OPTIONS=--max-old-space-size=8192를 설정합니다. 이는 Node 프로세스의 힙 한도를 조정한 값이며, 제품이 RAM 8GB 컴퓨터에서 충분히 동작한다는 측정 결과가 아닙니다.
tauri.conf.json의 기본 개발 주소는 localhost:5173이지만, 결합 Unix 실행기는 127.0.0.1의 5193부터 빈 포트를 찾습니다. 설정 파일 한 줄만 보고 실제 포트를 단정하지 마세요.frontend/scripts/unix-dev.mjs는 기본으로 SQLX_OFFLINE=true를 전달합니다. 이는 쿼리 검사 환경을 위한 설정이며 서비스 인증·모델 목록·생성을 대신하지 않습니다.wry 의존성이 있습니다. “주 UI가 egui”라는 사실을 “어떤 WebView도 사용하지 않는다”로 확대하면 안 됩니다.플랫폼 의존성의 구체적인 설치 조건은 Tauri 공식 사전 요구사항, 앱의 현재 개발 흐름은 ArtCraft 개발 문서를 참고하세요.
아래 과제는 원본 소스를 읽고 별도의 학습용 프로젝트에 작은 모형을 만드는 방식입니다. API 키나 실제 계정 대신 가짜 응답을 사용합니다. 실제 모델 호출을 모의 응답으로 바꾸면, 비용과 생성 시간에 흔들리지 않고 앱의 상태 설계에 집중할 수 있습니다.
목표: 화면의 함수 호출이 원격 접수와 완료 이벤트로 이어지는 과정을 설명합니다.
GenerateImage.ts에서 invoke의 명령 이름을 찾습니다.dispatch.rs에서 작업 ID를 꺼내는 곳과 DB 기록 지점을 표시합니다.완료 기준: “함수 반환”과 “이미지 완성”을 서로 다른 점으로 표시하고, frontend_subscriber_id가 필요한 이유를 한 문단으로 설명합니다.
확장 질문: 접수 직후 앱이 종료되면 어떤 정보가 남아야 다시 추적할 수 있을까요?
목표: 입력을 바꾸지 않고 API용 필드를 만드는 함수를 구현합니다. OmniRequest::api_fields()의 동작을 참고하되 HTTP 호출은 넣지 않습니다.
frontend_caller / frontend_subscriber_id / frontend_subscriber_payload)와 provider를 API 결과에서 제외합니다.batch_size를 image_batch_count로 옮깁니다.// 가짜 입력과 기대 출력: 실제 API 요청이 아닙니다.
const input = {
model: "future_image_v9",
batch_size: 2,
image_batch_count: 3,
new_option: true,
frontend_subscriber_id: "tab-7",
};
const expected = {
model: "future_image_v9",
image_batch_count: 3,
new_option: true,
};
완료 기준: 원본 입력이 그대로이고, 충돌하는 새 필드·null·알 수 없는 옵션의 처리 결과를 설명할 수 있습니다.
목표: 생성 접수와 결과 표시를 분리합니다. 실제 SQLite를 연결하기 전에는 JSON이나 브라우저 저장소로 시작해도 됩니다.
job_demo_1 같은 가짜 ID를 만듭니다.pending으로 저장하고 화면에 대기 카드를 표시합니다.완료 기준: 작업 두 개 중 하나가 실패해도 다른 작업은 완료됩니다. 완료 결과에는 가짜 파일 주소나 텍스트만 사용해도 충분합니다.
생각할 점: 폴링 타이머만 중지하는 것과 원격 작업 자체를 취소하는 것은 다릅니다. 취소 버튼의 의미를 명확히 정의하세요.
목표: 작은 사각형 편집 화면에 엔진·이벤트·저장소의 세 경계를 만듭니다.
완료 기준: 엔진 코드에 React 컴포넌트나 Tauri import가 없습니다. 같은 이벤트를 콘솔 기록기와 상태 브리지 두 곳에서 구독할 수 있습니다.
확장 질문: 모든 마우스 이동을 전역 상태에 보내는 대신, 어떤 값만 UI로 전달해야 할까요?
목표: dispatch.rs의 접수 후 DB 쓰기 실패 상황을 모델링하고 복구 정책을 설계합니다. 실제 서비스를 호출하지 않고 가짜 서버와 임시 학습 DB만 사용합니다.
완료 기준: 요청 직전 종료·접수 직후 종료·DB 저장 실패·완료 알림 실패의 네 상황에서 중복 생성과 상태 누락이 어떻게 처리되는지 표로 설명합니다.
이 과제는 현재 구현을 그대로 재현하는 단계에서 더 나아간 설계 확장입니다. 서버에 같은 키의 조회·중복 방지 계약이 없으면 클라이언트만으로 모든 문제를 해결할 수 없다는 제약까지 적으세요.
JavaScript 함수·배열·객체와 기본 HTML을 아는 독자를 기준으로 잡았습니다. Rust가 처음이라면 소유권·빌림·Option·Result를 익히는 준비 기간을 추가하세요. 각 주차의 결과물은 같은 작은 가짜 생성 앱에 쌓아갑니다.
| 주차 | 배울 것과 코드 연결 | 남길 결과물 |
|---|---|---|
| 1주차 | TypeScript 객체·유니온·비동기 함수GenerateImage.ts 요청 타입 읽기 | 입력 폼과 모의 접수 응답, 요청의 왕복 지도 |
| 2주차 | React 상태·이펙트 정리·경쟁 상태SceneState.ts의 세대 번호 읽기 | 오래된 응답이 최신 선택을 덮어쓰지 않는 화면 |
| 3주차 | Rust enum·Result·SerdeOmniRequest 변환 읽기 | 로컬 메타데이터와 API 필드를 분리하는 순수 함수 |
| 4주차 | Tauri 명령·이벤트와 상태lib.rs, FetchProxy.ts 읽기 | 가짜 생성 명령을 보내고 완료 이벤트를 받는 데스크톱 모형 |
| 5주차 | SQLite·SQLx·폴링·멱등성sqlite_tasks와 완료 처리기 읽기 | 새로고침·앱 재시작을 고려한 작업 목록과 실패 표 |
| 6주차 | Konva 또는 Three.js 중 하나PageSceneAdapter, EngineStoreBridge 읽기 | 편집 화면에서 모의 생성 결과까지 연결되는 작은 창작 도구 |
학습의 완성 기준은 라이브러리 이름을 많이 아는 것이 아니라, 요청 하나의 성공·실패·재시작 경로를 설명하고 구현하는 것입니다. 6주차까지도 외부 모델이 꼭 필요하지 않습니다. 화면 조작과 작업 수명이 연결되면 핵심 설계는 충분히 연습한 셈입니다.
artcraft-native의 채널과 Event 처리를 Tauri 앱과 비교합니다. 인증·API 클라이언트는 공유하면서 UI 경계는 달라지는 점을 찾아보세요.기능 설명 하나, 흐름도 하나, 실패 상황 하나를 남기세요. 예를 들어 “이미지 선택은 세대 번호로 오래된 응답을 무시한다”를 코드 위치와 함께 설명할 수 있다면, 이미 다른 앱에도 옮겨 쓸 수 있는 지식을 얻은 것입니다.
| 용어 | 쉬운 설명 |
|---|---|
| IDE | 여러 작업 도구를 하나의 흐름으로 묶은 개발 환경입니다. ArtCraft는 이를 창작 도구의 비유로 사용합니다. |
| 모노레포 | 여러 앱·라이브러리를 한 저장소에서 관리하는 구조입니다. ArtCraft에는 Rust 크레이트와 프론트엔드 라이브러리가 함께 있습니다. |
| 크레이트 / 워크스페이스 | Rust의 컴파일 단위와, 여러 단위를 함께 관리하는 구성입니다. 루트 Cargo.toml에서 연결 관계를 볼 수 있습니다. |
| WebView | 앱 안에 넣은 웹 화면 실행기입니다. 기본 Tauri 앱은 그 안에 React 화면을 표시합니다. |
| IPC | 서로 다른 프로세스가 메시지를 주고받는 통신입니다. Tauri 명령·이벤트가 이 경계를 연결합니다. |
| 직렬화 / 역직렬화 | 메모리의 값을 전송·저장 가능한 형태로 바꾸고 다시 읽는 일입니다. Serde가 Rust와 JSON 사이의 변환을 돕습니다. |
| 모달리티 | 이미지·영상·오디오 같은 데이터 종류입니다. Modality는 생성 API의 경로와 작업 종류를 결정합니다. |
| 어댑터 | 서로 다른 인터페이스 사이를 연결하는 변환기입니다. 제공자 API의 차이나 플랫폼의 차이를 한곳에 모읍니다. |
| 폴링 | 주기적으로 상태를 물어보는 방식입니다. 이 앱의 주요 작업 조회 루프는 매 조회 전에 5초를 기다립니다. 실제 간격에는 요청·처리 시간도 더해집니다. |
| 백오프 | 실패·요청 제한이 생기면 재시도 전에 더 기다리는 정책입니다. 이 코드에서는 상황별 고정 대기 시간이 보입니다. |
| 멱등성 키 | 같은 논리적 작업의 중복 접수를 구분하는 표식입니다. 효과를 내려면 재시도 시 키 유지와 서버의 처리 계약이 필요합니다. |
| 상관관계 ID | 돌아온 결과를 원래 요청·화면과 연결하는 번호입니다. 중복 방지 키와 목적이 다릅니다. |
| 마이그레이션 | DB 테이블 구조를 만들거나 바꾸는 절차입니다. ArtCraft는 로컬 작업 스키마를 앱에 포함합니다. |
| WAL | SQLite가 변경 내용을 별도 로그에 기록하는 방식입니다. 동시 읽기·쓰기 운용에 도움을 주지만 모든 장애를 해결하지는 않습니다. |
| 이벤트 버스 | 사건을 발행하는 쪽과 듣는 쪽을 연결하는 통로입니다. 엔진 내부 버스와 Tauri 이벤트는 통신 범위가 다릅니다. |
| 경쟁 상태 | 처리 순서에 따라 결과가 바뀌는 상황입니다. 먼저 선택한 이미지가 나중에 도착해 새 선택을 덮어쓰는 문제가 한 예입니다. |
| WASM | 웹 실행 환경에서 사용할 수 있는 바이너리 형식입니다. 영상 편집기의 합성·효과 모듈 연결에 사용됩니다. |
| Gaussian splat | 공간을 많은 점 모양의 분포로 표현해 장면을 보여주는 방식입니다. 삼각형 표면을 잇는 메시와 데이터 표현이 다릅니다. |
| CDN / 미디어 토큰 | CDN은 결과 파일을 전달하는 분산 네트워크이고, 미디어 토큰은 서비스가 파일을 식별하는 ID입니다. 파일 주소와 식별자는 같은 개념이 아닙니다. |
| 항목 | 확인 값과 해석 |
|---|---|
| 선정 근거 | TrendShift Monthly #5 |
| 스타 / 포크 | 35,748 / 5,656 · 2026-10-23 GitHub 기준 |
| 생성일 | 2022-03-31 · 저장소 생성 시점이며 현재 제품 형태의 출시일과는 다릅니다. |
| 주 언어 | Rust · 화면에는 TypeScript·TSX도 대규모로 사용됩니다. |
| 최신 릴리스 | artcraft-v0.41.0 · 2026-09-26 |
| 최근 push | 2026-10-22 · 저장소 갱신 시점이며 아래 분석 커밋의 날짜와 구분합니다. |
| 분석 기준 커밋 | 800217e · 2026-10-21ci: add sonar-project.properties for CI-based analysis |
| 소스의 앱 버전 | tauri.conf.json: 0.42.0데스크톱 Cargo 패키지: 0.0.1 · 공개 릴리스 태그와 별도 표기입니다. |
| Rust 규모 | .rs 3,261파일 · 272,400줄이 중 artcraft-native/ 47파일 · 13,958줄 |
| TypeScript 규모 | .ts 1,207파일 · 114,304줄.tsx 452파일 · 92,639줄합계 206,943줄 |
| 보조 언어 | JS·MJS·CJS 합계 4,673줄, CSS 1,625줄, Python 1,337줄, SQL 127줄 |
| 규모 산정 범위 | 커밋 800217e의 Git 추적 파일 5,824개가 기준입니다. 위 언어별 값은 확장자별 물리 줄 수이며 빈 줄·주석·테스트·생성 코드·vendor 코드도 포함합니다. 바이너리 자산의 크기는 줄 수에 반영하지 않습니다. |
| 라이선스 | GitHub 대표 표기: Apache-2.0 README·데스크톱 Cargo.toml·NOTICE: MIT 또는 Apache-2.0 상표와 일부 제3자 자산은 별도 고지 대상입니다. |
줄 수는 기능의 완성도나 직접 작성한 코드의 양을 뜻하지 않습니다. 특히 frontend/apps/artcraft/app/public/의 자산·보조 코드와 crates/vendor/까지 포함한 값입니다. 실제 학습은 전체 크기보다 한 기능의 호출 경로를 기준으로 나누는 편이 좋습니다.
이 글은 커밋 800217e의 정적 코드 분석입니다. 운영 서버의 내부 구현, 계정별 모델 가용성, 실제 생성 품질·지연 시간, OS별 실동작을 보증하지 않습니다. README의 모델 카탈로그에는 비활성·제한 항목이 있고 서버 목록도 바뀔 수 있습니다.
로드맵의 “ArtCraft 호스팅 서비스 의존성 제거”는 목표입니다. 현재 주요 생성 경로에는 ArtCraft API·인증·미디어 업로드 의존성이 보입니다. 완전한 오프라인 생성 도구나 운영 서버 전체를 포함한 자체 호스팅 묶음으로 이해하면 안 됩니다.
코드의 라이선스와 번들 자산의 권리는 동일하지 않습니다. NOTICE는 상표·제3자 라이선스와 별도 권리 자산을 구분합니다. 소스를 가져와 제품을 만들 때는 사용할 코드와 자산의 고지를 각각 확인해야 합니다.
ArtCraft를 통해 가져갈 가장 큰 배움은, 복잡한 창작 기능을 요청·상태·저장·이벤트의 경계로 나누어 읽는 방법입니다. 작은 모의 생성 앱에서 이 경계를 먼저 구현하면, 큰 저장소도 기능 단위로 차근차근 이해할 수 있습니다.