@kood/claude-code 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -1
- package/package.json +1 -1
- package/templates/.claude/agents/analyst.md +5 -0
- package/templates/.claude/agents/architect.md +5 -0
- package/templates/.claude/agents/build-fixer.md +1 -0
- package/templates/.claude/agents/code-reviewer.md +1 -0
- package/templates/.claude/agents/critic.md +4 -0
- package/templates/.claude/agents/deep-executor.md +1 -0
- package/templates/.claude/agents/dependency-manager.md +2 -0
- package/templates/.claude/agents/deployment-validator.md +2 -0
- package/templates/.claude/agents/designer.md +2 -0
- package/templates/.claude/agents/document-writer.md +3 -0
- package/templates/.claude/agents/explore.md +1 -0
- package/templates/.claude/agents/git-operator.md +2 -0
- package/templates/.claude/agents/implementation-executor.md +2 -0
- package/templates/.claude/agents/ko-to-en-translator.md +3 -0
- package/templates/.claude/agents/lint-fixer.md +2 -0
- package/templates/.claude/agents/planner.md +3 -0
- package/templates/.claude/agents/pm.md +349 -0
- package/templates/.claude/agents/qa-tester.md +1 -0
- package/templates/.claude/agents/refactor-advisor.md +4 -0
- package/templates/.claude/agents/researcher.md +9 -1
- package/templates/.claude/agents/scientist.md +1 -0
- package/templates/.claude/agents/security-reviewer.md +1 -0
- package/templates/.claude/agents/tdd-guide.md +1 -0
- package/templates/.claude/agents/vision.md +1 -0
- package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +376 -0
- package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
- package/templates/.claude/scripts/agent-teams/check-availability.sh +238 -0
- package/templates/.claude/scripts/agent-teams/setup-tmux.sh +125 -0
- package/templates/.claude/skills/agent-teams-setup/SKILL.md +460 -0
- package/templates/.claude/skills/brainstorm/SKILL.md +1 -0
- package/templates/.claude/skills/bug-fix/SKILL.md +1 -0
- package/templates/.claude/skills/crawler/SKILL.md +2 -0
- package/templates/.claude/skills/docs-creator/SKILL.md +1 -0
- package/templates/.claude/skills/docs-fetch/SKILL.md +6 -4
- package/templates/.claude/skills/docs-refactor/SKILL.md +1 -0
- package/templates/.claude/skills/elon-musk/SKILL.md +1 -0
- package/templates/.claude/skills/execute/SKILL.md +1 -0
- package/templates/.claude/skills/feedback/SKILL.md +1 -0
- package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
- package/templates/.claude/skills/genius-thinking/SKILL.md +1 -0
- package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/plan/SKILL.md +1 -0
- package/templates/.claude/skills/prd/SKILL.md +1 -0
- package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
- package/templates/.claude/skills/project-optimizer/SKILL.md +375 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-config-centralize.md +66 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-hot-path.md +35 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-interface-segregation.md +51 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-module-boundary.md +42 -0
- package/templates/.claude/skills/project-optimizer/rules/build-cache.md +57 -0
- package/templates/.claude/skills/project-optimizer/rules/build-code-split.md +56 -0
- package/templates/.claude/skills/project-optimizer/rules/build-incremental.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/build-minify.md +61 -0
- package/templates/.claude/skills/project-optimizer/rules/build-tree-shake.md +60 -0
- package/templates/.claude/skills/project-optimizer/rules/code-complexity.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/code-dead-elimination.md +32 -0
- package/templates/.claude/skills/project-optimizer/rules/code-duplication.md +54 -0
- package/templates/.claude/skills/project-optimizer/rules/code-error-handling.md +75 -0
- package/templates/.claude/skills/project-optimizer/rules/code-naming.md +52 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-defer-await.md +54 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-parallel.md +90 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-pipeline.md +68 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-pool.md +68 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-lightweight-alt.md +37 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-peer-align.md +44 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-security-audit.md +45 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-unused-removal.md +25 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-version-pin.md +40 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-ci-speed.md +47 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-dev-server.md +35 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-lint-config.md +36 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-test-coverage.md +34 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-type-safety.md +49 -0
- package/templates/.claude/skills/project-optimizer/rules/io-batch-queries.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-cache-layer.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-connection-reuse.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-serialize-minimal.md +61 -0
- package/templates/.claude/skills/project-optimizer/rules/io-stream.md +75 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-bounded-cache.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-large-data.md +64 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-lazy-init.md +78 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-leak-prevention.md +79 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-pool-reuse.md +70 -0
- package/templates/.claude/skills/ralph/SKILL.md +1 -0
- package/templates/.claude/skills/refactor/SKILL.md +1 -0
- package/templates/.claude/skills/research/SKILL.md +1 -0
- package/templates/.claude/skills/sql-optimizer/SKILL.md +438 -0
- package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -0
- package/templates/.claude/skills/startup-validator/SKILL.md +1 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/AGENTS.md +53 -14
- package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +94 -27
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/bundle-defer-third-party.md +42 -19
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-optimistic-updates.md +109 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md +74 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-use-hook.md +81 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/rerender-react-compiler.md +81 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-beforeload-auth.md +121 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-file-conventions.md +104 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-link-navigation.md +119 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-nested-layouts.md +155 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-path-params.md +89 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-pending-component.md +110 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-preload-strategy.md +91 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-router-context.md +120 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-search-params.md +114 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-deferred-data.md +1 -1
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-error-boundaries.md +79 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-middleware.md +85 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-serialization.md +56 -21
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-streaming.md +84 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-validator.md +71 -0
- package/templates/.claude/skills/tauri-react-best-practices/AGENTS.md +527 -0
- package/templates/.claude/skills/tauri-react-best-practices/SKILL.md +571 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-barrel-imports.md +140 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-cargo-profile.md +96 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-frontend-treeshake.md +242 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-lazy-components.md +255 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-remove-unused-commands.md +160 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-ci-pipeline.md +269 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-signing.md +207 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-updater.md +226 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-async-commands.md +172 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-batch-commands.md +133 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-binary-response.md +198 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-channel-streaming.md +186 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-error-handling.md +250 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-type-safe.md +227 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-derived-state.md +231 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-functional-setstate.md +191 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-index-maps.md +276 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-lazy-state-init.md +196 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-lifecycle.md +265 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-mobile-compat.md +199 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-permission-scope.md +193 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-error-boundary.md +239 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-event-listener.md +151 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-file-src.md +155 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-invoke-hook.md +139 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-optimistic-update.md +211 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-capability-split.md +205 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-csp.md +207 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-least-privilege.md +106 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-no-wildcard.md +253 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-scope-paths.md +160 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-async-mutex.md +270 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-mutex-pattern.md +265 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-react-sync.md +375 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-single-container.md +275 -0
- package/templates/tanstack-start/docs/architecture.md +238 -167
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +777 -38
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +549 -37
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +895 -111
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +641 -43
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +889 -38
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +891 -29
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +972 -36
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +1525 -881
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +1099 -20
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +796 -30
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +953 -35
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +371 -15
- package/templates/tauri/CLAUDE.md +189 -0
- package/templates/tauri/docs/guides/distribution.md +261 -0
- package/templates/tauri/docs/guides/getting-started.md +302 -0
- package/templates/tauri/docs/guides/mobile.md +288 -0
- package/templates/tauri/docs/library/tauri/index.md +510 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Response for Binary Data to Bypass JSON Serialization
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: 3-5x faster, no size overhead for binary data
|
|
5
|
+
tags: ipc, performance, binary, response, tauri-v2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Response로 바이너리 데이터 직렬화 우회
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
바이너리 데이터를 `Vec<u8>`로 반환하면 JSON으로 직렬화되어 크기가 증가하고 속도가 느립니다. `tauri::ipc::Response`를 사용하면 JSON 직렬화를 우회하여 raw 바이너리를 직접 전송할 수 있습니다.
|
|
13
|
+
|
|
14
|
+
**영향도:**
|
|
15
|
+
- 속도: 3-5배 향상
|
|
16
|
+
- 크기: 30-40% 감소 (Base64 인코딩 없음)
|
|
17
|
+
- 메모리: 복사 최소화
|
|
18
|
+
|
|
19
|
+
## ❌ 잘못된 패턴
|
|
20
|
+
|
|
21
|
+
**Vec<u8> 반환 (JSON 직렬화됨):**
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
use std::fs;
|
|
25
|
+
|
|
26
|
+
// ❌ Vec<u8>는 JSON 배열로 직렬화됨
|
|
27
|
+
#[tauri::command]
|
|
28
|
+
fn read_image(path: String) -> Result<Vec<u8>, String> {
|
|
29
|
+
fs::read(&path).map_err(|e| e.to_string())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 결과: [255, 216, 255, 224, ...] 형태로 직렬화
|
|
33
|
+
// 1MB 이미지 → 1.3MB JSON 배열
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// ❌ 프론트엔드에서 다시 Uint8Array로 변환 필요
|
|
38
|
+
const imageBytes = await invoke<number[]>('read_image', { path });
|
|
39
|
+
const blob = new Blob([new Uint8Array(imageBytes)]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**문제점:**
|
|
43
|
+
- JSON 직렬화 오버헤드
|
|
44
|
+
- 크기 증가 (각 바이트가 숫자로 표현)
|
|
45
|
+
- 메모리 복사 증가
|
|
46
|
+
|
|
47
|
+
## ✅ 올바른 패턴
|
|
48
|
+
|
|
49
|
+
**Response로 raw 바이너리 반환:**
|
|
50
|
+
|
|
51
|
+
```rust
|
|
52
|
+
use tauri::ipc::Response;
|
|
53
|
+
use std::fs;
|
|
54
|
+
|
|
55
|
+
// ✅ Response로 JSON 직렬화 우회
|
|
56
|
+
#[tauri::command]
|
|
57
|
+
fn read_image(path: String) -> Result<Response, String> {
|
|
58
|
+
let bytes = fs::read(&path).map_err(|e| e.to_string())?;
|
|
59
|
+
Ok(Response::new(bytes))
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ✅ 프론트엔드: 자동으로 Uint8Array로 수신
|
|
65
|
+
const imageBytes = await invoke<Uint8Array>('read_image', { path });
|
|
66
|
+
const blob = new Blob([imageBytes], { type: 'image/png' });
|
|
67
|
+
const url = URL.createObjectURL(blob);
|
|
68
|
+
|
|
69
|
+
// 이미지 표시
|
|
70
|
+
<img src={url} alt="Loaded from Rust" />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**스트리밍 대용량 파일:**
|
|
74
|
+
|
|
75
|
+
```rust
|
|
76
|
+
use tauri::ipc::{Channel, Response};
|
|
77
|
+
use tokio::fs::File;
|
|
78
|
+
use tokio::io::AsyncReadExt;
|
|
79
|
+
|
|
80
|
+
// ✅ 작은 파일: Response 직접 반환
|
|
81
|
+
#[tauri::command]
|
|
82
|
+
async fn read_small_file(path: String) -> Result<Response, String> {
|
|
83
|
+
let bytes = tokio::fs::read(&path)
|
|
84
|
+
.await
|
|
85
|
+
.map_err(|e| e.to_string())?;
|
|
86
|
+
Ok(Response::new(bytes))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ✅ 큰 파일: Channel로 청크 스트리밍
|
|
90
|
+
#[tauri::command]
|
|
91
|
+
async fn read_large_file(
|
|
92
|
+
path: String,
|
|
93
|
+
on_chunk: Channel<Response>,
|
|
94
|
+
) -> Result<(), String> {
|
|
95
|
+
let mut file = File::open(&path)
|
|
96
|
+
.await
|
|
97
|
+
.map_err(|e| e.to_string())?;
|
|
98
|
+
|
|
99
|
+
let mut buffer = vec![0u8; 65536]; // 64KB 청크
|
|
100
|
+
|
|
101
|
+
loop {
|
|
102
|
+
let n = file.read(&mut buffer)
|
|
103
|
+
.await
|
|
104
|
+
.map_err(|e| e.to_string())?;
|
|
105
|
+
|
|
106
|
+
if n == 0 { break; }
|
|
107
|
+
|
|
108
|
+
on_chunk.send(Response::new(buffer[..n].to_vec()))
|
|
109
|
+
.map_err(|e| e.to_string())?;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Ok(())
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**바이너리 데이터 생성 예시:**
|
|
117
|
+
|
|
118
|
+
```rust
|
|
119
|
+
use image::{ImageBuffer, Rgb};
|
|
120
|
+
|
|
121
|
+
#[tauri::command]
|
|
122
|
+
fn generate_thumbnail(
|
|
123
|
+
path: String,
|
|
124
|
+
width: u32,
|
|
125
|
+
height: u32,
|
|
126
|
+
) -> Result<Response, String> {
|
|
127
|
+
let img = image::open(&path)
|
|
128
|
+
.map_err(|e| e.to_string())?;
|
|
129
|
+
|
|
130
|
+
let thumbnail = img.resize(
|
|
131
|
+
width,
|
|
132
|
+
height,
|
|
133
|
+
image::imageops::FilterType::Lanczos3,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
let mut bytes: Vec<u8> = Vec::new();
|
|
137
|
+
thumbnail.write_to(
|
|
138
|
+
&mut std::io::Cursor::new(&mut bytes),
|
|
139
|
+
image::ImageFormat::Png,
|
|
140
|
+
).map_err(|e| e.to_string())?;
|
|
141
|
+
|
|
142
|
+
Ok(Response::new(bytes))
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## JavaScript에서 Blob 생성
|
|
147
|
+
|
|
148
|
+
**다양한 바이너리 데이터 처리:**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// 이미지 파일
|
|
152
|
+
const imageBytes = await invoke<Uint8Array>('read_image', { path });
|
|
153
|
+
const imageBlob = new Blob([imageBytes], { type: 'image/png' });
|
|
154
|
+
const imageUrl = URL.createObjectURL(imageBlob);
|
|
155
|
+
|
|
156
|
+
// PDF 파일
|
|
157
|
+
const pdfBytes = await invoke<Uint8Array>('read_pdf', { path });
|
|
158
|
+
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
|
159
|
+
|
|
160
|
+
// 비디오 스트리밍
|
|
161
|
+
const videoChunks: Uint8Array[] = [];
|
|
162
|
+
const onChunk = new Channel<Uint8Array>();
|
|
163
|
+
onChunk.onmessage = (chunk) => videoChunks.push(chunk);
|
|
164
|
+
await invoke('stream_video', { path, onChunk });
|
|
165
|
+
const videoBlob = new Blob(videoChunks, { type: 'video/mp4' });
|
|
166
|
+
|
|
167
|
+
// 다운로드 링크 생성
|
|
168
|
+
const downloadUrl = URL.createObjectURL(videoBlob);
|
|
169
|
+
const a = document.createElement('a');
|
|
170
|
+
a.href = downloadUrl;
|
|
171
|
+
a.download = 'video.mp4';
|
|
172
|
+
a.click();
|
|
173
|
+
|
|
174
|
+
// 메모리 정리
|
|
175
|
+
URL.revokeObjectURL(downloadUrl);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 추가 컨텍스트
|
|
179
|
+
|
|
180
|
+
**언제 Response를 사용해야 하는가:**
|
|
181
|
+
- 이미지, 비디오, 오디오 파일
|
|
182
|
+
- PDF, 압축 파일 등 바이너리 문서
|
|
183
|
+
- 암호화된 데이터
|
|
184
|
+
- 프로토콜 버퍼, MessagePack 등 바이너리 형식
|
|
185
|
+
|
|
186
|
+
**JSON 직렬화가 적합한 경우:**
|
|
187
|
+
- 구조화된 데이터 (객체, 배열)
|
|
188
|
+
- 작은 숫자 배열 (< 100 요소)
|
|
189
|
+
- 타입 안전성이 중요한 경우
|
|
190
|
+
|
|
191
|
+
**주의사항:**
|
|
192
|
+
- `Response::new()`는 `Vec<u8>` 소유권을 가져감
|
|
193
|
+
- 매우 큰 파일(100MB+)은 Channel 스트리밍 사용
|
|
194
|
+
- `URL.createObjectURL()` 후 `URL.revokeObjectURL()`로 메모리 정리 필수
|
|
195
|
+
|
|
196
|
+
**참고:**
|
|
197
|
+
- [Tauri Response API](https://docs.rs/tauri/latest/tauri/ipc/struct.Response.html)
|
|
198
|
+
- [MDN Blob API](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Channels for Large Data Streaming
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: 5-10x faster than Events for continuous data streams
|
|
5
|
+
tags: ipc, performance, channel, streaming, tauri-v2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Channel로 대용량 데이터 스트리밍
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
대용량 데이터나 연속적인 진행률 업데이트를 전송할 때, Events는 각 메시지마다 오버헤드가 크고 느립니다. Tauri v2의 Channel은 스트리밍 최적화 프로토콜로 5-10배 빠른 처리량을 제공합니다.
|
|
13
|
+
|
|
14
|
+
**영향도:**
|
|
15
|
+
- 처리량: 5-10배 향상
|
|
16
|
+
- 메모리: 버퍼링 최적화
|
|
17
|
+
- 응답성: 실시간 진행률 표시
|
|
18
|
+
|
|
19
|
+
## ❌ 잘못된 패턴
|
|
20
|
+
|
|
21
|
+
**Events로 대용량 스트리밍:**
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
use tauri::{AppHandle, Emitter};
|
|
25
|
+
|
|
26
|
+
// ❌ Events로 청크 전송 (느림, 오버헤드 큼)
|
|
27
|
+
#[tauri::command]
|
|
28
|
+
async fn download_file(app: AppHandle, url: String) -> Result<(), String> {
|
|
29
|
+
let mut downloaded = 0u64;
|
|
30
|
+
let total = 1_000_000u64;
|
|
31
|
+
|
|
32
|
+
for chunk in 0..1000 {
|
|
33
|
+
downloaded += 1000;
|
|
34
|
+
// 각 emit마다 이벤트 버스 오버헤드
|
|
35
|
+
app.emit("download-progress", (downloaded, total))
|
|
36
|
+
.map_err(|e| e.to_string())?;
|
|
37
|
+
|
|
38
|
+
// 실제 다운로드 로직...
|
|
39
|
+
}
|
|
40
|
+
Ok(())
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// ❌ 프론트엔드: Events 리스닝
|
|
46
|
+
import { listen } from '@tauri-apps/api/event';
|
|
47
|
+
|
|
48
|
+
listen<[number, number]>('download-progress', (event) => {
|
|
49
|
+
const [downloaded, total] = event.payload;
|
|
50
|
+
updateProgressBar(downloaded / total);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await invoke('download_file', { url: 'https://...' });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**문제점:**
|
|
57
|
+
- Events는 브로드캐스트 방식 (모든 윈도우에 전송)
|
|
58
|
+
- 각 메시지마다 이벤트 버스 오버헤드
|
|
59
|
+
- 백프레셔(backpressure) 제어 없음
|
|
60
|
+
|
|
61
|
+
## ✅ 올바른 패턴
|
|
62
|
+
|
|
63
|
+
**Channel로 스트리밍:**
|
|
64
|
+
|
|
65
|
+
```rust
|
|
66
|
+
use tauri::ipc::Channel;
|
|
67
|
+
|
|
68
|
+
// ✅ Channel로 스트리밍 (빠름, 직접 연결)
|
|
69
|
+
#[tauri::command]
|
|
70
|
+
async fn download_file(
|
|
71
|
+
url: String,
|
|
72
|
+
on_progress: Channel<(u64, u64)>,
|
|
73
|
+
) -> Result<(), String> {
|
|
74
|
+
let mut downloaded = 0u64;
|
|
75
|
+
let total = 1_000_000u64;
|
|
76
|
+
|
|
77
|
+
for _chunk in 0..1000 {
|
|
78
|
+
downloaded += 1000;
|
|
79
|
+
// Channel은 단일 연결, 낮은 오버헤드
|
|
80
|
+
on_progress.send((downloaded, total))
|
|
81
|
+
.map_err(|e| e.to_string())?;
|
|
82
|
+
|
|
83
|
+
// 실제 다운로드 로직...
|
|
84
|
+
}
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ✅ 프론트엔드: Channel 사용
|
|
91
|
+
import { invoke, Channel } from '@tauri-apps/api/core';
|
|
92
|
+
|
|
93
|
+
async function downloadFile(url: string) {
|
|
94
|
+
const onProgress = new Channel<[number, number]>();
|
|
95
|
+
|
|
96
|
+
onProgress.onmessage = ([downloaded, total]) => {
|
|
97
|
+
updateProgressBar(downloaded / total);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await invoke('download_file', { url, onProgress });
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**파일 스트리밍 예시:**
|
|
105
|
+
|
|
106
|
+
```rust
|
|
107
|
+
use tokio::fs::File;
|
|
108
|
+
use tokio::io::AsyncReadExt;
|
|
109
|
+
|
|
110
|
+
#[tauri::command]
|
|
111
|
+
async fn stream_file_chunks(
|
|
112
|
+
path: String,
|
|
113
|
+
on_chunk: Channel<Vec<u8>>,
|
|
114
|
+
) -> Result<(), String> {
|
|
115
|
+
let mut file = File::open(&path)
|
|
116
|
+
.await
|
|
117
|
+
.map_err(|e| e.to_string())?;
|
|
118
|
+
|
|
119
|
+
let mut buffer = vec![0u8; 8192]; // 8KB 청크
|
|
120
|
+
|
|
121
|
+
loop {
|
|
122
|
+
let n = file.read(&mut buffer)
|
|
123
|
+
.await
|
|
124
|
+
.map_err(|e| e.to_string())?;
|
|
125
|
+
|
|
126
|
+
if n == 0 { break; }
|
|
127
|
+
|
|
128
|
+
on_chunk.send(buffer[..n].to_vec())
|
|
129
|
+
.map_err(|e| e.to_string())?;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Ok(())
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { Channel } from '@tauri-apps/api/core';
|
|
138
|
+
|
|
139
|
+
const chunks: Uint8Array[] = [];
|
|
140
|
+
const onChunk = new Channel<number[]>();
|
|
141
|
+
|
|
142
|
+
onChunk.onmessage = (chunk) => {
|
|
143
|
+
chunks.push(new Uint8Array(chunk));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await invoke('stream_file_chunks', {
|
|
147
|
+
path: '/path/to/large-file.bin',
|
|
148
|
+
onChunk,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 모든 청크 합치기
|
|
152
|
+
const blob = new Blob(chunks);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Channels vs Events 비교
|
|
156
|
+
|
|
157
|
+
| 측면 | Channels | Events |
|
|
158
|
+
|------|----------|--------|
|
|
159
|
+
| **방향** | Rust → Frontend (단방향) | 양방향 브로드캐스트 |
|
|
160
|
+
| **수신자** | 호출한 윈도우만 | 모든 윈도우 |
|
|
161
|
+
| **처리량** | 높음 (5-10x) | 낮음 |
|
|
162
|
+
| **오버헤드** | 낮음 (직접 연결) | 높음 (이벤트 버스) |
|
|
163
|
+
| **백프레셔** | 지원 | 미지원 |
|
|
164
|
+
| **사용 사례** | 대용량 스트리밍, 진행률 | 알림, 상태 브로드캐스트 |
|
|
165
|
+
|
|
166
|
+
## 추가 컨텍스트
|
|
167
|
+
|
|
168
|
+
**언제 Channel을 사용해야 하는가:**
|
|
169
|
+
- 진행률 업데이트 (다운로드, 인코딩 등)
|
|
170
|
+
- 파일 청크 스트리밍
|
|
171
|
+
- 실시간 데이터 피드 (로그, 센서 데이터)
|
|
172
|
+
- 100+ 메시지를 빠르게 전송해야 할 때
|
|
173
|
+
|
|
174
|
+
**언제 Events를 사용해야 하는가:**
|
|
175
|
+
- 모든 윈도우에 알림 필요
|
|
176
|
+
- 간헐적인 이벤트 (클릭, 상태 변경)
|
|
177
|
+
- 양방향 통신 필요
|
|
178
|
+
|
|
179
|
+
**주의사항:**
|
|
180
|
+
- Channel은 **타입 파라미터**를 반드시 명시해야 함: `Channel<T>`
|
|
181
|
+
- 프론트엔드에서 `onmessage` 핸들러 설정 후 `invoke` 호출
|
|
182
|
+
- Channel은 `invoke` 호출이 끝나면 자동으로 닫힘
|
|
183
|
+
|
|
184
|
+
**참고:**
|
|
185
|
+
- [Tauri Channels Guide](https://tauri.app/develop/calling-frontend/#channels)
|
|
186
|
+
- [IPC Concept](https://tauri.app/concept/inter-process-communication/)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Structured Error Types with thiserror
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Debuggable errors, error code-based handling
|
|
5
|
+
tags: ipc, error-handling, thiserror, serde, tauri-v2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 구조화된 에러 타입으로 디버깅 향상
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
`Result<T, String>` 같은 단순 에러는 디버깅이 어렵고 프론트엔드에서 에러 종류별 처리가 불가능합니다. `thiserror`로 구조화된 에러를 정의하고 `serde::Serialize`를 구현하면 타입 안전하고 디버깅 가능한 에러 처리가 가능합니다.
|
|
13
|
+
|
|
14
|
+
**영향도:**
|
|
15
|
+
- 디버깅: 에러 종류, 원인 명확화
|
|
16
|
+
- UX: 에러별 맞춤 메시지 표시
|
|
17
|
+
- 유지보수: 에러 코드 기반 처리
|
|
18
|
+
|
|
19
|
+
## ❌ 잘못된 패턴
|
|
20
|
+
|
|
21
|
+
**단순 String 에러:**
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
// ❌ String 에러 (종류 구분 불가)
|
|
25
|
+
#[tauri::command]
|
|
26
|
+
fn read_file(path: String) -> Result<String, String> {
|
|
27
|
+
std::fs::read_to_string(&path)
|
|
28
|
+
.map_err(|e| e.to_string()) // 에러 타입 정보 손실
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[tauri::command]
|
|
32
|
+
fn delete_file(path: String) -> Result<(), String> {
|
|
33
|
+
std::fs::remove_file(&path)
|
|
34
|
+
.map_err(|e| format!("Failed to delete: {}", e))
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ 프론트엔드: 에러 문자열 파싱으로 종류 판단
|
|
40
|
+
try {
|
|
41
|
+
await invoke('read_file', { path: '/secret.txt' });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// 문자열 파싱으로 에러 종류 추측 (취약함)
|
|
44
|
+
if (err.includes('permission')) {
|
|
45
|
+
alert('권한이 없습니다');
|
|
46
|
+
} else if (err.includes('not found')) {
|
|
47
|
+
alert('파일을 찾을 수 없습니다');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**문제점:**
|
|
53
|
+
- 에러 종류 구분 불가
|
|
54
|
+
- 디버깅 정보 부족
|
|
55
|
+
- 프론트엔드에서 일관된 처리 어려움
|
|
56
|
+
|
|
57
|
+
## ✅ 올바른 패턴
|
|
58
|
+
|
|
59
|
+
**thiserror로 구조화된 에러:**
|
|
60
|
+
|
|
61
|
+
### 1. 의존성 추가
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
# src-tauri/Cargo.toml
|
|
65
|
+
[dependencies]
|
|
66
|
+
thiserror = "2.0"
|
|
67
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. 에러 타입 정의
|
|
71
|
+
|
|
72
|
+
```rust
|
|
73
|
+
use serde::{Serialize, Serializer};
|
|
74
|
+
use thiserror::Error;
|
|
75
|
+
|
|
76
|
+
// ✅ 구조화된 에러 enum
|
|
77
|
+
#[derive(Debug, Error)]
|
|
78
|
+
enum AppError {
|
|
79
|
+
#[error("File not found: {path}")]
|
|
80
|
+
FileNotFound { path: String },
|
|
81
|
+
|
|
82
|
+
#[error("Permission denied: {path}")]
|
|
83
|
+
PermissionDenied { path: String },
|
|
84
|
+
|
|
85
|
+
#[error("Invalid file format: expected {expected}, got {actual}")]
|
|
86
|
+
InvalidFormat { expected: String, actual: String },
|
|
87
|
+
|
|
88
|
+
#[error("Network error: {0}")]
|
|
89
|
+
Network(String),
|
|
90
|
+
|
|
91
|
+
#[error(transparent)]
|
|
92
|
+
Io(#[from] std::io::Error),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ✅ Serialize 구현으로 JSON 변환
|
|
96
|
+
impl Serialize for AppError {
|
|
97
|
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
98
|
+
where
|
|
99
|
+
S: Serializer,
|
|
100
|
+
{
|
|
101
|
+
use serde::ser::SerializeStruct;
|
|
102
|
+
|
|
103
|
+
let mut state = serializer.serialize_struct("AppError", 2)?;
|
|
104
|
+
|
|
105
|
+
// 에러 코드
|
|
106
|
+
let code = match self {
|
|
107
|
+
AppError::FileNotFound { .. } => "FILE_NOT_FOUND",
|
|
108
|
+
AppError::PermissionDenied { .. } => "PERMISSION_DENIED",
|
|
109
|
+
AppError::InvalidFormat { .. } => "INVALID_FORMAT",
|
|
110
|
+
AppError::Network(_) => "NETWORK_ERROR",
|
|
111
|
+
AppError::Io(_) => "IO_ERROR",
|
|
112
|
+
};
|
|
113
|
+
state.serialize_field("code", code)?;
|
|
114
|
+
|
|
115
|
+
// 에러 메시지
|
|
116
|
+
state.serialize_field("message", &self.to_string())?;
|
|
117
|
+
|
|
118
|
+
state.end()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. 커맨드에서 사용
|
|
124
|
+
|
|
125
|
+
```rust
|
|
126
|
+
#[tauri::command]
|
|
127
|
+
fn read_file(path: String) -> Result<String, AppError> {
|
|
128
|
+
// 자동 변환: std::io::Error -> AppError::Io
|
|
129
|
+
let content = std::fs::read_to_string(&path)?;
|
|
130
|
+
Ok(content)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[tauri::command]
|
|
134
|
+
fn process_config(path: String) -> Result<Config, AppError> {
|
|
135
|
+
let content = std::fs::read_to_string(&path)
|
|
136
|
+
.map_err(|_| AppError::FileNotFound { path: path.clone() })?;
|
|
137
|
+
|
|
138
|
+
if !content.starts_with('[') {
|
|
139
|
+
return Err(AppError::InvalidFormat {
|
|
140
|
+
expected: "JSON".to_string(),
|
|
141
|
+
actual: "Plain text".to_string(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 파싱 로직...
|
|
146
|
+
Ok(Config::default())
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 4. 프론트엔드에서 에러 처리
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// ✅ 에러 타입 정의
|
|
154
|
+
interface AppError {
|
|
155
|
+
code: string;
|
|
156
|
+
message: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ✅ 에러 코드 기반 처리
|
|
160
|
+
async function loadFile(path: string) {
|
|
161
|
+
try {
|
|
162
|
+
const content = await invoke<string>('read_file', { path });
|
|
163
|
+
return content;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
const error = err as AppError;
|
|
166
|
+
|
|
167
|
+
switch (error.code) {
|
|
168
|
+
case 'FILE_NOT_FOUND':
|
|
169
|
+
toast.error('파일을 찾을 수 없습니다');
|
|
170
|
+
break;
|
|
171
|
+
case 'PERMISSION_DENIED':
|
|
172
|
+
toast.error('파일 접근 권한이 없습니다');
|
|
173
|
+
break;
|
|
174
|
+
case 'INVALID_FORMAT':
|
|
175
|
+
toast.error(`잘못된 파일 형식: ${error.message}`);
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
toast.error(`오류: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 로깅
|
|
182
|
+
console.error(`[${error.code}] ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 고급 패턴: 에러 컨텍스트
|
|
188
|
+
|
|
189
|
+
**anyhow로 에러 체인:**
|
|
190
|
+
|
|
191
|
+
```rust
|
|
192
|
+
use anyhow::{Context, Result};
|
|
193
|
+
|
|
194
|
+
#[tauri::command]
|
|
195
|
+
fn load_config(path: String) -> Result<Config, String> {
|
|
196
|
+
let content = std::fs::read_to_string(&path)
|
|
197
|
+
.context(format!("Failed to read config from {}", path))?;
|
|
198
|
+
|
|
199
|
+
let config: Config = serde_json::from_str(&content)
|
|
200
|
+
.context("Failed to parse JSON config")?;
|
|
201
|
+
|
|
202
|
+
Ok(config)
|
|
203
|
+
}
|
|
204
|
+
// 에러 메시지: "Failed to parse JSON config: unexpected character at line 5"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**tauri-specta와 통합:**
|
|
208
|
+
|
|
209
|
+
```rust
|
|
210
|
+
use specta::Type;
|
|
211
|
+
|
|
212
|
+
#[derive(Debug, Error, Type)]
|
|
213
|
+
#[serde(tag = "type", content = "data")]
|
|
214
|
+
enum AppError {
|
|
215
|
+
#[error("File not found")]
|
|
216
|
+
FileNotFound { path: String },
|
|
217
|
+
|
|
218
|
+
#[error("Permission denied")]
|
|
219
|
+
PermissionDenied,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// TypeScript에서 자동 생성:
|
|
223
|
+
// type AppError =
|
|
224
|
+
// | { type: "FileNotFound", data: { path: string } }
|
|
225
|
+
// | { type: "PermissionDenied" }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 추가 컨텍스트
|
|
229
|
+
|
|
230
|
+
**언제 구조화된 에러를 사용해야 하는가:**
|
|
231
|
+
- 에러 종류가 2개 이상
|
|
232
|
+
- 프론트엔드에서 에러별 처리 필요
|
|
233
|
+
- 디버깅 정보 제공 필요
|
|
234
|
+
- 에러 로깅/모니터링 시스템 연동
|
|
235
|
+
|
|
236
|
+
**에러 설계 원칙:**
|
|
237
|
+
- 에러 코드는 대문자 스네이크 케이스 (`FILE_NOT_FOUND`)
|
|
238
|
+
- 메시지는 사용자 친화적으로
|
|
239
|
+
- 민감 정보 (경로, 비밀번호 등) 제외
|
|
240
|
+
- 복구 가능한 에러만 반환 (패닉 금지)
|
|
241
|
+
|
|
242
|
+
**주의사항:**
|
|
243
|
+
- `#[error(transparent)]`로 다른 에러 타입 자동 변환
|
|
244
|
+
- `Serialize` 구현 시 순환 참조 주의
|
|
245
|
+
- 에러 메시지는 국제화(i18n) 고려
|
|
246
|
+
|
|
247
|
+
**참고:**
|
|
248
|
+
- [thiserror Documentation](https://docs.rs/thiserror/)
|
|
249
|
+
- [Rust Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html)
|
|
250
|
+
- [anyhow for Application Errors](https://docs.rs/anyhow/)
|