@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,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Generate TypeScript Bindings with tauri-specta
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: End-to-end type safety, eliminate runtime type errors
|
|
5
|
+
tags: ipc, typescript, type-safety, tauri-specta, devex
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# tauri-specta로 TypeScript 바인딩 자동 생성
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
수동으로 TypeScript 타입을 작성하면 Rust 커맨드와 불일치가 발생하여 런타임 에러가 발생합니다. `tauri-specta`를 사용하면 Rust 타입에서 TypeScript 바인딩을 자동 생성하여 완벽한 타입 안전성을 확보할 수 있습니다.
|
|
13
|
+
|
|
14
|
+
**영향도:**
|
|
15
|
+
- 타입 안전성: 런타임 에러 사전 방지
|
|
16
|
+
- 개발 경험: 자동 완성, 타입 체크
|
|
17
|
+
- 유지보수: Rust 변경 시 TS 자동 업데이트
|
|
18
|
+
|
|
19
|
+
## ❌ 잘못된 패턴
|
|
20
|
+
|
|
21
|
+
**수동 타입 정의 (불일치 위험):**
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
// Rust 커맨드
|
|
25
|
+
#[derive(serde::Serialize)]
|
|
26
|
+
struct User {
|
|
27
|
+
id: i32,
|
|
28
|
+
name: String,
|
|
29
|
+
email: String,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[tauri::command]
|
|
33
|
+
fn get_user(id: i32) -> Result<User, String> {
|
|
34
|
+
Ok(User {
|
|
35
|
+
id,
|
|
36
|
+
name: "Alice".to_string(),
|
|
37
|
+
email: "alice@example.com".to_string(),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// ❌ 수동으로 타입 작성 (Rust와 동기화 안됨)
|
|
44
|
+
interface User {
|
|
45
|
+
id: number;
|
|
46
|
+
name: string;
|
|
47
|
+
// email 필드 누락! 런타임 에러 위험
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const user = await invoke<User>('get_user', { id: 1 });
|
|
51
|
+
console.log(user.email); // 타입 체크 통과, 런타임 undefined
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**문제점:**
|
|
55
|
+
- Rust 타입 변경 시 TypeScript에 반영 안됨
|
|
56
|
+
- 오타, 필드 누락 등으로 런타임 에러 발생
|
|
57
|
+
- 리팩토링 시 일관성 유지 어려움
|
|
58
|
+
|
|
59
|
+
## ✅ 올바른 패턴
|
|
60
|
+
|
|
61
|
+
**tauri-specta로 바인딩 자동 생성:**
|
|
62
|
+
|
|
63
|
+
### 1. 의존성 추가
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
# src-tauri/Cargo.toml
|
|
67
|
+
[dependencies]
|
|
68
|
+
tauri-specta = { version = "2.0", features = ["derive", "typescript"] }
|
|
69
|
+
specta = { version = "2.0", features = ["derive"] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Rust 타입에 derive 추가
|
|
73
|
+
|
|
74
|
+
```rust
|
|
75
|
+
use specta::Type;
|
|
76
|
+
use serde::{Deserialize, Serialize};
|
|
77
|
+
use tauri_specta::Event;
|
|
78
|
+
|
|
79
|
+
// ✅ Type derive로 TypeScript 생성 가능
|
|
80
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
|
81
|
+
struct User {
|
|
82
|
+
id: i32,
|
|
83
|
+
name: String,
|
|
84
|
+
email: String,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
88
|
+
struct UserCreatedEvent {
|
|
89
|
+
user: User,
|
|
90
|
+
timestamp: String,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[tauri::command]
|
|
94
|
+
#[specta::specta] // 커맨드 시그니처 추출
|
|
95
|
+
fn get_user(id: i32) -> Result<User, String> {
|
|
96
|
+
Ok(User {
|
|
97
|
+
id,
|
|
98
|
+
name: "Alice".to_string(),
|
|
99
|
+
email: "alice@example.com".to_string(),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[tauri::command]
|
|
104
|
+
#[specta::specta]
|
|
105
|
+
fn create_user(name: String, email: String) -> Result<User, String> {
|
|
106
|
+
Ok(User {
|
|
107
|
+
id: 1,
|
|
108
|
+
name,
|
|
109
|
+
email,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. 바인딩 생성 빌드 스크립트
|
|
115
|
+
|
|
116
|
+
```rust
|
|
117
|
+
// src-tauri/src/lib.rs (또는 main.rs)
|
|
118
|
+
use tauri_specta::{collect_commands, collect_events, Builder};
|
|
119
|
+
|
|
120
|
+
fn main() {
|
|
121
|
+
let builder = Builder::<tauri::Wry>::new()
|
|
122
|
+
.commands(collect_commands![get_user, create_user])
|
|
123
|
+
.events(collect_events![UserCreatedEvent]);
|
|
124
|
+
|
|
125
|
+
#[cfg(debug_assertions)]
|
|
126
|
+
builder
|
|
127
|
+
.export(specta_typescript::Typescript::default(), "../src/bindings.ts")
|
|
128
|
+
.expect("Failed to export typescript bindings");
|
|
129
|
+
|
|
130
|
+
tauri::Builder::default()
|
|
131
|
+
.plugin(builder.into_plugin())
|
|
132
|
+
.invoke_handler(tauri::generate_handler![get_user, create_user])
|
|
133
|
+
.run(tauri::generate_context!())
|
|
134
|
+
.expect("error while running tauri application");
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 4. 생성된 TypeScript 바인딩 사용
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// src/bindings.ts (자동 생성됨)
|
|
142
|
+
export interface User {
|
|
143
|
+
id: number;
|
|
144
|
+
name: string;
|
|
145
|
+
email: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface UserCreatedEvent {
|
|
149
|
+
user: User;
|
|
150
|
+
timestamp: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const commands = {
|
|
154
|
+
getUser: (id: number) => invoke<User>('get_user', { id }),
|
|
155
|
+
createUser: (name: string, email: string) =>
|
|
156
|
+
invoke<User>('create_user', { name, email }),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const events = {
|
|
160
|
+
userCreated: (handler: (event: UserCreatedEvent) => void) =>
|
|
161
|
+
listen<UserCreatedEvent>('user-created', handler),
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// ✅ 완벽한 타입 안전성
|
|
167
|
+
import { commands, events } from './bindings';
|
|
168
|
+
|
|
169
|
+
// 자동 완성, 타입 체크
|
|
170
|
+
const user = await commands.getUser(1);
|
|
171
|
+
console.log(user.email); // 타입 안전
|
|
172
|
+
|
|
173
|
+
// Rust에서 email 필드 제거하면 TypeScript 컴파일 에러
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 대체 도구: TauRPC
|
|
177
|
+
|
|
178
|
+
**타입 안전 + RPC 스타일 API:**
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// TauRPC는 추가로 함수형 API 제공
|
|
182
|
+
import { createTauRPCProxy } from 'taurpc';
|
|
183
|
+
|
|
184
|
+
const api = await createTauRPCProxy();
|
|
185
|
+
|
|
186
|
+
// 함수처럼 호출 가능
|
|
187
|
+
const user = await api.getUser(1);
|
|
188
|
+
const newUser = await api.createUser("Bob", "bob@example.com");
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**특징:**
|
|
192
|
+
- `tauri-specta` 기반
|
|
193
|
+
- RPC 스타일 함수 호출
|
|
194
|
+
- 자동 재시도, 에러 처리
|
|
195
|
+
|
|
196
|
+
## 추가 컨텍스트
|
|
197
|
+
|
|
198
|
+
**언제 tauri-specta를 사용해야 하는가:**
|
|
199
|
+
- TypeScript 프론트엔드 사용
|
|
200
|
+
- 복잡한 데이터 구조 전달
|
|
201
|
+
- 여러 커맨드 관리
|
|
202
|
+
- 팀 협업 (타입 일관성 필수)
|
|
203
|
+
|
|
204
|
+
**설정 옵션:**
|
|
205
|
+
|
|
206
|
+
```rust
|
|
207
|
+
// TypeScript 생성 옵션
|
|
208
|
+
use specta_typescript::Typescript;
|
|
209
|
+
|
|
210
|
+
let config = Typescript::default()
|
|
211
|
+
.formatter(specta_typescript::formatter::prettier) // Prettier 포맷팅
|
|
212
|
+
.header("// Generated by tauri-specta. DO NOT EDIT!") // 헤더 추가
|
|
213
|
+
.bigint(specta_typescript::BigIntExportBehavior::Number); // BigInt 처리
|
|
214
|
+
|
|
215
|
+
builder.export(config, "../src/bindings.ts").unwrap();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**주의사항:**
|
|
219
|
+
- `#[specta::specta]` 매크로를 모든 커맨드에 추가 필수
|
|
220
|
+
- `Type` derive는 `Serialize`/`Deserialize`와 함께 사용
|
|
221
|
+
- 생성된 파일은 `.gitignore`에 추가하지 말 것 (팀원도 타입 체크 필요)
|
|
222
|
+
- `cargo run` 시 자동 생성되도록 설정 (CI/CD에서도 동작)
|
|
223
|
+
|
|
224
|
+
**참고:**
|
|
225
|
+
- [tauri-specta GitHub](https://github.com/specta-rs/tauri-specta)
|
|
226
|
+
- [Specta Documentation](https://docs.rs/specta/)
|
|
227
|
+
- [TauRPC](https://github.com/MatsDK/TauRPC)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# 파생 boolean 구독
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
연속적인 값(윈도우 크기, 스크롤 위치 등)을 직접 구독하면 픽셀 단위로 리렌더링이 발생합니다. 파생된 boolean 값을 구독하면 리렌더링 빈도를 크게 줄일 수 있습니다. Tauri 앱에서는 윈도우 이벤트, 파일 워처 등에서 특히 중요합니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function Sidebar() {
|
|
14
|
+
const [width, setWidth] = useState(0)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const appWindow = getCurrentWindow()
|
|
18
|
+
|
|
19
|
+
// ❌ 윈도우 크기 변경마다 리렌더링 (연속적)
|
|
20
|
+
const unlisten = appWindow.listen('tauri://resize', async () => {
|
|
21
|
+
const size = await appWindow.innerSize()
|
|
22
|
+
setWidth(size.width)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
unlisten.then(fn => fn())
|
|
27
|
+
}
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
// boolean 변환은 컴포넌트 내부 (매 렌더링마다 계산)
|
|
31
|
+
const isMobile = width < 768
|
|
32
|
+
|
|
33
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**문제점:**
|
|
38
|
+
- 윈도우 크기 변경마다 리렌더링 (픽셀 단위)
|
|
39
|
+
- 실제로는 boolean 값만 필요한데 연속 값 구독
|
|
40
|
+
- 불필요한 렌더링으로 성능 저하
|
|
41
|
+
|
|
42
|
+
## ✅ 올바른 패턴
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
46
|
+
import { useEffect, useState } from 'react'
|
|
47
|
+
|
|
48
|
+
function Sidebar() {
|
|
49
|
+
// ✅ boolean 값만 구독 (변경 시에만 리렌더링)
|
|
50
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const appWindow = getCurrentWindow()
|
|
54
|
+
|
|
55
|
+
const checkMobile = async () => {
|
|
56
|
+
const size = await appWindow.innerSize()
|
|
57
|
+
const mobile = size.width < 768
|
|
58
|
+
setIsMobile(prev => prev !== mobile ? mobile : prev) // 값 변경 시에만 업데이트
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
checkMobile()
|
|
62
|
+
|
|
63
|
+
const unlisten = appWindow.listen('tauri://resize', checkMobile)
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
unlisten.then(fn => fn())
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**useMediaQuery 커스텀 hook (웹과 유사):**
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
78
|
+
import { useEffect, useState } from 'react'
|
|
79
|
+
|
|
80
|
+
function useWindowWidth() {
|
|
81
|
+
const [width, setWidth] = useState(0)
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const appWindow = getCurrentWindow()
|
|
85
|
+
|
|
86
|
+
const updateWidth = async () => {
|
|
87
|
+
const size = await appWindow.innerSize()
|
|
88
|
+
setWidth(size.width)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateWidth()
|
|
92
|
+
const unlisten = appWindow.listen('tauri://resize', updateWidth)
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
unlisten.then(fn => fn())
|
|
96
|
+
}
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
return width
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function useMediaQuery(breakpoint: number) {
|
|
103
|
+
const width = useWindowWidth()
|
|
104
|
+
// ✅ boolean 반환 (리렌더링 빈도 감소)
|
|
105
|
+
return width < breakpoint
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 사용 예시
|
|
109
|
+
function App() {
|
|
110
|
+
const isMobile = useMediaQuery(768)
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
{isMobile ? <MobileView /> : <DesktopView />}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**파일 워처 예시:**
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { listen } from '@tauri-apps/api/event'
|
|
124
|
+
import { useEffect, useState } from 'react'
|
|
125
|
+
|
|
126
|
+
type FileEvent = {
|
|
127
|
+
path: string
|
|
128
|
+
size: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function FileMonitor() {
|
|
132
|
+
// ❌ 파일 크기 연속 업데이트 (불필요)
|
|
133
|
+
const [fileSize, setFileSize] = useState(0)
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
let unlisten: UnlistenFn | undefined
|
|
137
|
+
|
|
138
|
+
listen<FileEvent>('file-changed', event => {
|
|
139
|
+
setFileSize(event.payload.size) // ❌ 매번 리렌더링
|
|
140
|
+
}).then(fn => unlisten = fn)
|
|
141
|
+
|
|
142
|
+
return () => unlisten?.()
|
|
143
|
+
}, [])
|
|
144
|
+
|
|
145
|
+
const isLarge = fileSize > 10 * 1024 * 1024 // 10MB
|
|
146
|
+
|
|
147
|
+
return <div>{isLarge ? 'Large file' : 'Normal file'}</div>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ✅ 개선된 버전
|
|
151
|
+
function FileMonitor() {
|
|
152
|
+
const [isLarge, setIsLarge] = useState(false)
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
let unlisten: UnlistenFn | undefined
|
|
156
|
+
|
|
157
|
+
listen<FileEvent>('file-changed', event => {
|
|
158
|
+
const large = event.payload.size > 10 * 1024 * 1024
|
|
159
|
+
setIsLarge(prev => prev !== large ? large : prev) // ✅ 변경 시에만 업데이트
|
|
160
|
+
}).then(fn => unlisten = fn)
|
|
161
|
+
|
|
162
|
+
return () => unlisten?.()
|
|
163
|
+
}, [])
|
|
164
|
+
|
|
165
|
+
return <div>{isLarge ? 'Large file' : 'Normal file'}</div>
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**스크롤 위치 → 버튼 표시:**
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useEffect, useState } from 'react'
|
|
173
|
+
|
|
174
|
+
function ScrollToTop() {
|
|
175
|
+
// ❌ 스크롤 위치 직접 구독 (연속 업데이트)
|
|
176
|
+
const [scrollY, setScrollY] = useState(0)
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const handleScroll = () => setScrollY(window.scrollY)
|
|
180
|
+
window.addEventListener('scroll', handleScroll)
|
|
181
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
182
|
+
}, [])
|
|
183
|
+
|
|
184
|
+
const showButton = scrollY > 300
|
|
185
|
+
|
|
186
|
+
return showButton ? <button>Top</button> : null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ✅ 개선된 버전
|
|
190
|
+
function ScrollToTop() {
|
|
191
|
+
const [showButton, setShowButton] = useState(false)
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const handleScroll = () => {
|
|
195
|
+
const show = window.scrollY > 300
|
|
196
|
+
setShowButton(prev => prev !== show ? show : prev) // ✅ boolean만 업데이트
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
window.addEventListener('scroll', handleScroll)
|
|
200
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
return showButton ? <button>Top</button> : null
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 추가 컨텍스트
|
|
208
|
+
|
|
209
|
+
**파생 boolean 구독이 유용한 경우:**
|
|
210
|
+
- 윈도우 크기 → 모바일/데스크톱 구분
|
|
211
|
+
- 스크롤 위치 → 버튼 표시/숨김
|
|
212
|
+
- 파일 크기 → 경고 표시
|
|
213
|
+
- 타이머 → 만료 여부
|
|
214
|
+
|
|
215
|
+
**최적화 기법:**
|
|
216
|
+
- `prev !== current ? current : prev` 패턴으로 불필요한 업데이트 방지
|
|
217
|
+
- 연속 값을 구독해야 하는 경우 `useDeferredValue` 사용 (React 18+)
|
|
218
|
+
|
|
219
|
+
**Tauri 윈도우 이벤트:**
|
|
220
|
+
- `tauri://resize`: 윈도우 크기 변경
|
|
221
|
+
- `tauri://move`: 윈도우 이동
|
|
222
|
+
- `tauri://focus`: 포커스 상태 변경
|
|
223
|
+
- `tauri://blur`: 블러 상태 변경
|
|
224
|
+
|
|
225
|
+
**성능 비교:**
|
|
226
|
+
- 연속 값 구독: 수십~수백 리렌더링/초
|
|
227
|
+
- boolean 구독: 0~2 리렌더링/이벤트
|
|
228
|
+
|
|
229
|
+
**참고:** [React useDeferredValue](https://react.dev/reference/react/useDeferredValue)
|
|
230
|
+
|
|
231
|
+
영향도: MEDIUM - 리렌더링 빈도 감소, 성능 최적화
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# 함수형 setState로 안정적 콜백
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
현재 상태 값을 기반으로 상태를 업데이트할 때 상태 변수를 직접 참조하면 stale closure가 발생하고, 콜백이 상태 변경마다 재생성됩니다. 함수형 setState를 사용하면 안정적인 콜백 참조를 유지하고 불필요한 리렌더링을 방지할 수 있습니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
11
|
+
import { useState, useCallback } from 'react'
|
|
12
|
+
|
|
13
|
+
function NotificationPanel() {
|
|
14
|
+
const [notifications, setNotifications] = useState<string[]>([])
|
|
15
|
+
|
|
16
|
+
// ❌ 콜백이 notifications에 의존 (매번 재생성)
|
|
17
|
+
const addNotification = useCallback(async (message: string) => {
|
|
18
|
+
setNotifications([...notifications, message])
|
|
19
|
+
}, [notifications]) // ❌ notifications 의존성으로 인한 재생성
|
|
20
|
+
|
|
21
|
+
// ❌ 의존성 누락 (stale closure 버그)
|
|
22
|
+
const clearNotifications = useCallback(() => {
|
|
23
|
+
setNotifications([]) // 항상 초기 notifications 참조
|
|
24
|
+
}, []) // ❌ stale closure 위험
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<button onClick={() => addNotification('New message')}>Add</button>
|
|
29
|
+
<button onClick={clearNotifications}>Clear</button>
|
|
30
|
+
{notifications.map((msg, i) => <div key={i}>{msg}</div>)}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**문제점:**
|
|
37
|
+
- `notifications`가 변경될 때마다 콜백 재생성
|
|
38
|
+
- 자식 컴포넌트가 불필요하게 리렌더링
|
|
39
|
+
- 의존성 누락 시 stale closure 버그
|
|
40
|
+
|
|
41
|
+
## ✅ 올바른 패턴
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
45
|
+
import { useState, useCallback } from 'react'
|
|
46
|
+
|
|
47
|
+
function NotificationPanel() {
|
|
48
|
+
const [notifications, setNotifications] = useState<string[]>([])
|
|
49
|
+
|
|
50
|
+
// ✅ 안정적인 콜백, 재생성 안 됨
|
|
51
|
+
const addNotification = useCallback(async (message: string) => {
|
|
52
|
+
setNotifications(curr => [...curr, message])
|
|
53
|
+
}, []) // ✅ 의존성 불필요
|
|
54
|
+
|
|
55
|
+
// ✅ 항상 최신 상태 사용
|
|
56
|
+
const clearNotifications = useCallback(() => {
|
|
57
|
+
setNotifications([])
|
|
58
|
+
}, []) // ✅ 안전하고 안정적
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<button onClick={() => addNotification('New message')}>Add</button>
|
|
63
|
+
<button onClick={clearNotifications}>Clear</button>
|
|
64
|
+
{notifications.map((msg, i) => <div key={i}>{msg}</div>)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Tauri IPC와 함께 사용:**
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
74
|
+
import { listen, UnlistenFn } from '@tauri-apps/api/event'
|
|
75
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
76
|
+
|
|
77
|
+
function FileWatcher() {
|
|
78
|
+
const [files, setFiles] = useState<string[]>([])
|
|
79
|
+
|
|
80
|
+
// ✅ 함수형 setState로 안정적인 이벤트 핸들러
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
let unlisten: UnlistenFn | undefined
|
|
83
|
+
|
|
84
|
+
listen<string>('file-added', event => {
|
|
85
|
+
setFiles(curr => [...curr, event.payload]) // ✅ 최신 상태 참조
|
|
86
|
+
}).then(fn => unlisten = fn)
|
|
87
|
+
|
|
88
|
+
return () => unlisten?.()
|
|
89
|
+
}, []) // ✅ 의존성 없음
|
|
90
|
+
|
|
91
|
+
// ✅ 안정적인 삭제 핸들러
|
|
92
|
+
const removeFile = useCallback((fileName: string) => {
|
|
93
|
+
setFiles(curr => curr.filter(f => f !== fileName))
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
{files.map(file => (
|
|
99
|
+
<div key={file}>
|
|
100
|
+
{file}
|
|
101
|
+
<button onClick={() => removeFile(file)}>Remove</button>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**invoke 호출과 상태 업데이트:**
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
113
|
+
import { useState, useCallback } from 'react'
|
|
114
|
+
|
|
115
|
+
type TodoItem = {
|
|
116
|
+
id: string
|
|
117
|
+
title: string
|
|
118
|
+
completed: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function TodoList() {
|
|
122
|
+
const [todos, setTodos] = useState<TodoItem[]>([])
|
|
123
|
+
|
|
124
|
+
// ✅ 함수형 setState로 안정적인 추가 로직
|
|
125
|
+
const addTodo = useCallback(async (title: string) => {
|
|
126
|
+
const newTodo = await invoke<TodoItem>('create_todo', { title })
|
|
127
|
+
setTodos(curr => [...curr, newTodo])
|
|
128
|
+
}, [])
|
|
129
|
+
|
|
130
|
+
// ✅ 함수형 setState로 안정적인 토글 로직
|
|
131
|
+
const toggleTodo = useCallback(async (id: string) => {
|
|
132
|
+
await invoke('toggle_todo', { id })
|
|
133
|
+
setTodos(curr =>
|
|
134
|
+
curr.map(todo =>
|
|
135
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
}, [])
|
|
139
|
+
|
|
140
|
+
// ✅ 함수형 setState로 안정적인 삭제 로직
|
|
141
|
+
const deleteTodo = useCallback(async (id: string) => {
|
|
142
|
+
await invoke('delete_todo', { id })
|
|
143
|
+
setTodos(curr => curr.filter(todo => todo.id !== id))
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
{todos.map(todo => (
|
|
149
|
+
<div key={todo.id}>
|
|
150
|
+
<input
|
|
151
|
+
type="checkbox"
|
|
152
|
+
checked={todo.completed}
|
|
153
|
+
onChange={() => toggleTodo(todo.id)}
|
|
154
|
+
/>
|
|
155
|
+
{todo.title}
|
|
156
|
+
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
<button onClick={() => addTodo('New Task')}>Add Todo</button>
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 추가 컨텍스트
|
|
166
|
+
|
|
167
|
+
**장점:**
|
|
168
|
+
1. **안정적인 콜백 참조** - 상태 변경 시 콜백 재생성 불필요
|
|
169
|
+
2. **Stale closure 없음** - 항상 최신 상태 값으로 동작
|
|
170
|
+
3. **적은 의존성** - 의존성 배열 단순화
|
|
171
|
+
4. **버그 방지** - React closure 버그의 가장 흔한 원인 제거
|
|
172
|
+
|
|
173
|
+
**함수형 업데이트를 사용해야 하는 경우:**
|
|
174
|
+
- 현재 상태 값에 의존하는 모든 setState
|
|
175
|
+
- 상태가 필요한 useCallback/useMemo 내부
|
|
176
|
+
- 상태를 참조하는 이벤트 핸들러
|
|
177
|
+
- 상태를 업데이트하는 비동기 작업 (invoke, listen)
|
|
178
|
+
|
|
179
|
+
**직접 업데이트를 사용해도 되는 경우:**
|
|
180
|
+
- 정적 값으로 상태 설정: `setCount(0)`
|
|
181
|
+
- props/arguments로만 상태 설정: `setName(newName)`
|
|
182
|
+
- 상태가 이전 값에 의존하지 않는 경우
|
|
183
|
+
|
|
184
|
+
**Tauri 이벤트 리스너에서 특히 중요:**
|
|
185
|
+
- 이벤트 핸들러는 등록 시점의 상태를 클로저로 캡처
|
|
186
|
+
- 함수형 setState 없이는 초기 상태만 참조 (버그)
|
|
187
|
+
- cleanup 시에도 최신 상태 참조 필요
|
|
188
|
+
|
|
189
|
+
**참고:** [React useState](https://react.dev/reference/react/useState)
|
|
190
|
+
|
|
191
|
+
영향도: MEDIUM - 성능, 버그 방지, 콜백 안정성
|