@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,375 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Rust-React State Synchronization
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: ensures consistent state across boundaries
|
|
5
|
+
tags: state, react, events, synchronization, ipc
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Rust 상태와 React 상태 동기화 패턴
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
Tauri 앱에서 **Rust 백엔드 상태**와 **React 프론트엔드 상태**가 독립적으로 관리되면 **불일치**가 발생합니다. Tauri Events를 사용하여 양방향 동기화를 구현해야 합니다.
|
|
13
|
+
|
|
14
|
+
## ❌ 잘못된 패턴
|
|
15
|
+
|
|
16
|
+
**Rust와 React 상태가 각각 독립적으로 관리:**
|
|
17
|
+
|
|
18
|
+
```rust
|
|
19
|
+
// ❌ src-tauri/src/main.rs - Rust 상태만 업데이트
|
|
20
|
+
use std::sync::Mutex;
|
|
21
|
+
use tauri::State;
|
|
22
|
+
|
|
23
|
+
struct AppStateInner {
|
|
24
|
+
count: i32
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type AppState = Mutex<AppStateInner>;
|
|
28
|
+
|
|
29
|
+
#[tauri::command]
|
|
30
|
+
fn increment(state: State<'_, AppState>) -> i32 {
|
|
31
|
+
let mut state = state.lock().unwrap();
|
|
32
|
+
state.count += 1;
|
|
33
|
+
state.count // React에 반환만 하고 끝
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// ❌ src/App.tsx - React 상태가 동기화되지 않음
|
|
39
|
+
import { useState } from 'react'
|
|
40
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
const [count, setCount] = useState(0)
|
|
44
|
+
|
|
45
|
+
const handleIncrement = async () => {
|
|
46
|
+
const newCount = await invoke<number>('increment')
|
|
47
|
+
setCount(newCount) // ✅ 이 창에서는 동기화됨
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ❌ 다른 창이나 백그라운드 업데이트는 감지 못함
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
<p>Count: {count}</p>
|
|
54
|
+
<button onClick={handleIncrement}>Increment</button>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**문제:**
|
|
61
|
+
- 다른 창에서 `increment` 호출 시 현재 창의 `count`가 업데이트되지 않음
|
|
62
|
+
- Rust 상태 변경이 React에 전파되지 않음
|
|
63
|
+
- 백그라운드 작업이 상태를 변경해도 UI가 업데이트 안됨
|
|
64
|
+
|
|
65
|
+
## ✅ 올바른 패턴
|
|
66
|
+
|
|
67
|
+
**Tauri Events로 Rust → React 동기화:**
|
|
68
|
+
|
|
69
|
+
```rust
|
|
70
|
+
// ✅ src-tauri/src/state.rs
|
|
71
|
+
use std::sync::Mutex;
|
|
72
|
+
use serde::{Deserialize, Serialize};
|
|
73
|
+
|
|
74
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
75
|
+
pub struct AppStateInner {
|
|
76
|
+
pub count: i32,
|
|
77
|
+
pub username: String
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub type AppState = Mutex<AppStateInner>;
|
|
81
|
+
|
|
82
|
+
impl AppStateInner {
|
|
83
|
+
pub fn new() -> Self {
|
|
84
|
+
Self {
|
|
85
|
+
count: 0,
|
|
86
|
+
username: String::from("Guest")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```rust
|
|
93
|
+
// ✅ src-tauri/src/main.rs - 상태 변경 시 이벤트 발행
|
|
94
|
+
mod state;
|
|
95
|
+
|
|
96
|
+
use state::{AppState, AppStateInner};
|
|
97
|
+
use tauri::{State, Manager, Emitter};
|
|
98
|
+
|
|
99
|
+
#[tauri::command]
|
|
100
|
+
fn increment(state: State<'_, AppState>, app: tauri::AppHandle) -> Result<i32, String> {
|
|
101
|
+
let new_count = {
|
|
102
|
+
let mut state = state.lock().unwrap();
|
|
103
|
+
state.count += 1;
|
|
104
|
+
state.count
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ✅ 모든 창에 상태 변경 알림
|
|
108
|
+
app.emit("state-changed", &new_count)
|
|
109
|
+
.map_err(|e| format!("Failed to emit event: {}", e))?;
|
|
110
|
+
|
|
111
|
+
Ok(new_count)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[tauri::command]
|
|
115
|
+
fn set_username(
|
|
116
|
+
state: State<'_, AppState>,
|
|
117
|
+
app: tauri::AppHandle,
|
|
118
|
+
username: String
|
|
119
|
+
) -> Result<(), String> {
|
|
120
|
+
{
|
|
121
|
+
let mut state = state.lock().unwrap();
|
|
122
|
+
state.username = username.clone();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ✅ username 변경 이벤트 발행
|
|
126
|
+
app.emit("username-changed", &username)
|
|
127
|
+
.map_err(|e| format!("Failed to emit event: {}", e))?;
|
|
128
|
+
|
|
129
|
+
Ok(())
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[tauri::command]
|
|
133
|
+
fn get_state(state: State<'_, AppState>) -> Result<AppStateInner, String> {
|
|
134
|
+
let state = state.lock()
|
|
135
|
+
.map_err(|e| format!("Failed to lock state: {}", e))?;
|
|
136
|
+
Ok(state.clone())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn main() {
|
|
140
|
+
tauri::Builder::default()
|
|
141
|
+
.manage(AppState::new(AppStateInner::new()))
|
|
142
|
+
.invoke_handler(tauri::generate_handler![
|
|
143
|
+
increment,
|
|
144
|
+
set_username,
|
|
145
|
+
get_state
|
|
146
|
+
])
|
|
147
|
+
.run(tauri::generate_context!())
|
|
148
|
+
.expect("error while running tauri application");
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// ✅ src/App.tsx - 이벤트 리스너로 동기화
|
|
154
|
+
import { useState, useEffect } from 'react'
|
|
155
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
156
|
+
import { listen } from '@tauri-apps/api/event'
|
|
157
|
+
|
|
158
|
+
interface AppState {
|
|
159
|
+
count: number
|
|
160
|
+
username: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function App() {
|
|
164
|
+
const [state, setState] = useState<AppState>({
|
|
165
|
+
count: 0,
|
|
166
|
+
username: 'Guest'
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
// ✅ 초기 상태 로드
|
|
171
|
+
invoke<AppState>('get_state').then(setState)
|
|
172
|
+
|
|
173
|
+
// ✅ Rust 상태 변경 감지
|
|
174
|
+
const unlistenCount = listen<number>('state-changed', (event) => {
|
|
175
|
+
setState(prev => ({ ...prev, count: event.payload }))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const unlistenUsername = listen<string>('username-changed', (event) => {
|
|
179
|
+
setState(prev => ({ ...prev, username: event.payload }))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// 클린업
|
|
183
|
+
return () => {
|
|
184
|
+
unlistenCount.then(fn => fn())
|
|
185
|
+
unlistenUsername.then(fn => fn())
|
|
186
|
+
}
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
const handleIncrement = async () => {
|
|
190
|
+
await invoke('increment') // 이벤트로 상태 업데이트됨
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const handleSetUsername = async (name: string) => {
|
|
194
|
+
await invoke('set_username', { username: name })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div>
|
|
199
|
+
<p>Count: {state.count}</p>
|
|
200
|
+
<p>Username: {state.username}</p>
|
|
201
|
+
<button onClick={handleIncrement}>Increment</button>
|
|
202
|
+
<button onClick={() => handleSetUsername('Alice')}>
|
|
203
|
+
Set Username
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default App
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**React → Rust 동기화 (양방향):**
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// ✅ src/App.tsx - React 상태 변경 시 Rust에 동기화
|
|
216
|
+
function App() {
|
|
217
|
+
const [localCount, setLocalCount] = useState(0)
|
|
218
|
+
|
|
219
|
+
const handleLocalIncrement = async () => {
|
|
220
|
+
// 낙관적 업데이트
|
|
221
|
+
const newCount = localCount + 1
|
|
222
|
+
setLocalCount(newCount)
|
|
223
|
+
|
|
224
|
+
// Rust 상태 동기화
|
|
225
|
+
try {
|
|
226
|
+
await invoke('increment')
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// 실패 시 롤백
|
|
229
|
+
setLocalCount(localCount)
|
|
230
|
+
console.error('Failed to sync state:', error)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div>
|
|
236
|
+
<p>Local Count: {localCount}</p>
|
|
237
|
+
<button onClick={handleLocalIncrement}>Increment</button>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**커스텀 훅으로 추상화:**
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// ✅ src/hooks/useTauriState.ts
|
|
247
|
+
import { useState, useEffect } from 'react'
|
|
248
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
249
|
+
import { listen } from '@tauri-apps/api/event'
|
|
250
|
+
|
|
251
|
+
export function useTauriState<T>(
|
|
252
|
+
getCommand: string,
|
|
253
|
+
eventName: string
|
|
254
|
+
): [T | null, (newValue: T) => Promise<void>] {
|
|
255
|
+
const [state, setState] = useState<T | null>(null)
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
// 초기 상태 로드
|
|
259
|
+
invoke<T>(getCommand).then(setState)
|
|
260
|
+
|
|
261
|
+
// 이벤트 리스너
|
|
262
|
+
const unlisten = listen<T>(eventName, (event) => {
|
|
263
|
+
setState(event.payload)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return () => {
|
|
267
|
+
unlisten.then(fn => fn())
|
|
268
|
+
}
|
|
269
|
+
}, [getCommand, eventName])
|
|
270
|
+
|
|
271
|
+
const updateState = async (newValue: T) => {
|
|
272
|
+
setState(newValue)
|
|
273
|
+
await invoke('update_state', { newValue })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return [state, updateState]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 사용 예시
|
|
280
|
+
function App() {
|
|
281
|
+
const [count, setCount] = useTauriState<number>('get_count', 'count-changed')
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
<p>Count: {count ?? 'Loading...'}</p>
|
|
286
|
+
<button onClick={() => setCount((count ?? 0) + 1)}>
|
|
287
|
+
Increment
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## 추가 컨텍스트
|
|
295
|
+
|
|
296
|
+
**양방향 동기화 아키텍처:**
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
┌─────────────┐ invoke() ┌─────────────┐
|
|
300
|
+
│ React │ ───────────────────────> │ Rust │
|
|
301
|
+
│ State │ │ State │
|
|
302
|
+
│ │ <─────────────────────── │ │
|
|
303
|
+
└─────────────┘ emit() event └─────────────┘
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**동기화 패턴 비교:**
|
|
307
|
+
|
|
308
|
+
| 패턴 | 장점 | 단점 | 권장 |
|
|
309
|
+
|------|------|------|------|
|
|
310
|
+
| **Poll (주기적 조회)** | 간단 | 비효율적, 지연 | ❌ |
|
|
311
|
+
| **Event-driven** | 실시간, 효율적 | 복잡도 증가 | ✅ |
|
|
312
|
+
| **낙관적 업데이트** | 빠른 UX | 실패 시 롤백 필요 | ⚠️ 선택 |
|
|
313
|
+
|
|
314
|
+
**여러 창 동기화:**
|
|
315
|
+
|
|
316
|
+
```rust
|
|
317
|
+
// 모든 창에 브로드캐스트
|
|
318
|
+
app.emit_all("state-changed", &new_count)?;
|
|
319
|
+
|
|
320
|
+
// 특정 창에만 전송
|
|
321
|
+
if let Some(window) = app.get_window("main") {
|
|
322
|
+
window.emit("state-changed", &new_count)?;
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**에러 처리:**
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
const unlisten = listen<number>('state-changed', (event) => {
|
|
331
|
+
setState(event.payload)
|
|
332
|
+
}).catch((error) => {
|
|
333
|
+
console.error('Failed to listen to events:', error)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
return () => {
|
|
337
|
+
unlisten.then(fn => fn()).catch(console.error)
|
|
338
|
+
}
|
|
339
|
+
}, [])
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**성능 최적화:**
|
|
343
|
+
|
|
344
|
+
```rust
|
|
345
|
+
// 디바운싱으로 과도한 이벤트 방지
|
|
346
|
+
use std::time::{Duration, Instant};
|
|
347
|
+
|
|
348
|
+
struct StateEmitter {
|
|
349
|
+
last_emit: Mutex<Instant>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
impl StateEmitter {
|
|
353
|
+
fn emit_throttled(&self, app: &tauri::AppHandle, count: i32) -> Result<(), String> {
|
|
354
|
+
let mut last = self.last_emit.lock().unwrap();
|
|
355
|
+
let now = Instant::now();
|
|
356
|
+
|
|
357
|
+
if now.duration_since(*last) > Duration::from_millis(100) {
|
|
358
|
+
app.emit("state-changed", &count)
|
|
359
|
+
.map_err(|e| e.to_string())?;
|
|
360
|
+
*last = now;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
Ok(())
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**참고:**
|
|
369
|
+
- Tauri Events: [Inter-Process Communication](https://tauri.app/v2/guides/features/events/)
|
|
370
|
+
- React useEffect: [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects)
|
|
371
|
+
|
|
372
|
+
**영향도:**
|
|
373
|
+
- 일관성: HIGH (상태 불일치 방지)
|
|
374
|
+
- 실시간성: HIGH (즉시 동기화)
|
|
375
|
+
- 복잡도: MEDIUM (이벤트 관리 필요)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Single Container for Multiple States
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: prevents state registration conflicts
|
|
5
|
+
tags: state, container, tauri, type-map, rust
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 동일 타입 중복 등록 방지
|
|
9
|
+
|
|
10
|
+
## 왜 중요한가
|
|
11
|
+
|
|
12
|
+
Tauri의 상태 관리는 **타입 기반 의존성 주입** 시스템을 사용합니다. 동일한 타입을 여러 번 `manage()`로 등록하면 **두 번째 등록이 무시**되어 의도하지 않은 상태를 사용하게 됩니다.
|
|
13
|
+
|
|
14
|
+
## ❌ 잘못된 패턴
|
|
15
|
+
|
|
16
|
+
**동일 타입을 여러 번 manage() 호출:**
|
|
17
|
+
|
|
18
|
+
```rust
|
|
19
|
+
// ❌ src-tauri/src/main.rs
|
|
20
|
+
use std::sync::Mutex;
|
|
21
|
+
use tauri::State;
|
|
22
|
+
|
|
23
|
+
type DatabaseState = Mutex<String>;
|
|
24
|
+
type CacheState = Mutex<String>;
|
|
25
|
+
|
|
26
|
+
#[tauri::command]
|
|
27
|
+
fn get_db_connection(db: State<'_, DatabaseState>) -> String {
|
|
28
|
+
let db = db.lock().unwrap();
|
|
29
|
+
db.clone()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[tauri::command]
|
|
33
|
+
fn get_cache_connection(cache: State<'_, CacheState>) -> String {
|
|
34
|
+
let cache = cache.lock().unwrap();
|
|
35
|
+
cache.clone()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn main() {
|
|
39
|
+
tauri::Builder::default()
|
|
40
|
+
.manage(DatabaseState::new(String::from("postgres://localhost")))
|
|
41
|
+
.manage(CacheState::new(String::from("redis://localhost")))
|
|
42
|
+
// ❌ 두 번째 manage()가 무시됨! (둘 다 Mutex<String> 타입)
|
|
43
|
+
.invoke_handler(tauri::generate_handler![
|
|
44
|
+
get_db_connection,
|
|
45
|
+
get_cache_connection
|
|
46
|
+
])
|
|
47
|
+
.run(tauri::generate_context!())
|
|
48
|
+
.expect("error while running tauri application");
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**프론트엔드에서 호출:**
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// 둘 다 "postgres://localhost" 반환!
|
|
56
|
+
const db = await invoke('get_db_connection') // "postgres://localhost"
|
|
57
|
+
const cache = await invoke('get_cache_connection') // "postgres://localhost" (예상: "redis://localhost")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**문제:**
|
|
61
|
+
- `CacheState`가 등록되지 않음
|
|
62
|
+
- `get_cache_connection`이 `DatabaseState`를 반환
|
|
63
|
+
- 런타임 에러는 발생하지 않지만 잘못된 상태 사용
|
|
64
|
+
|
|
65
|
+
## ✅ 올바른 패턴
|
|
66
|
+
|
|
67
|
+
**컨테이너 구조체로 여러 상태 통합:**
|
|
68
|
+
|
|
69
|
+
```rust
|
|
70
|
+
// ✅ src-tauri/src/state.rs
|
|
71
|
+
use std::sync::Mutex;
|
|
72
|
+
|
|
73
|
+
pub struct AppStateInner {
|
|
74
|
+
pub database_url: String,
|
|
75
|
+
pub cache_url: String,
|
|
76
|
+
pub api_key: String
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub type AppState = Mutex<AppStateInner>;
|
|
80
|
+
|
|
81
|
+
impl AppStateInner {
|
|
82
|
+
pub fn new() -> Self {
|
|
83
|
+
Self {
|
|
84
|
+
database_url: String::from("postgres://localhost"),
|
|
85
|
+
cache_url: String::from("redis://localhost"),
|
|
86
|
+
api_key: String::from("secret")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```rust
|
|
93
|
+
// ✅ src-tauri/src/main.rs
|
|
94
|
+
mod state;
|
|
95
|
+
|
|
96
|
+
use state::{AppState, AppStateInner};
|
|
97
|
+
use tauri::State;
|
|
98
|
+
|
|
99
|
+
#[tauri::command]
|
|
100
|
+
fn get_db_connection(state: State<'_, AppState>) -> String {
|
|
101
|
+
let state = state.lock().unwrap();
|
|
102
|
+
state.database_url.clone()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[tauri::command]
|
|
106
|
+
fn get_cache_connection(state: State<'_, AppState>) -> String {
|
|
107
|
+
let state = state.lock().unwrap();
|
|
108
|
+
state.cache_url.clone()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[tauri::command]
|
|
112
|
+
fn get_api_key(state: State<'_, AppState>) -> String {
|
|
113
|
+
let state = state.lock().unwrap();
|
|
114
|
+
state.api_key.clone()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn main() {
|
|
118
|
+
tauri::Builder::default()
|
|
119
|
+
.manage(AppState::new(AppStateInner::new())) // 단일 컨테이너 등록
|
|
120
|
+
.invoke_handler(tauri::generate_handler![
|
|
121
|
+
get_db_connection,
|
|
122
|
+
get_cache_connection,
|
|
123
|
+
get_api_key
|
|
124
|
+
])
|
|
125
|
+
.run(tauri::generate_context!())
|
|
126
|
+
.expect("error while running tauri application");
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**newtype 패턴 (타입 래핑):**
|
|
131
|
+
|
|
132
|
+
```rust
|
|
133
|
+
// ✅ src-tauri/src/state.rs - newtype 패턴
|
|
134
|
+
use std::sync::Mutex;
|
|
135
|
+
|
|
136
|
+
// 각각 고유한 타입으로 래핑
|
|
137
|
+
pub struct DatabaseState(pub Mutex<String>);
|
|
138
|
+
pub struct CacheState(pub Mutex<String>);
|
|
139
|
+
|
|
140
|
+
impl DatabaseState {
|
|
141
|
+
pub fn new(url: String) -> Self {
|
|
142
|
+
Self(Mutex::new(url))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
impl CacheState {
|
|
147
|
+
pub fn new(url: String) -> Self {
|
|
148
|
+
Self(Mutex::new(url))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```rust
|
|
154
|
+
// ✅ src-tauri/src/main.rs
|
|
155
|
+
mod state;
|
|
156
|
+
|
|
157
|
+
use state::{DatabaseState, CacheState};
|
|
158
|
+
use tauri::State;
|
|
159
|
+
|
|
160
|
+
#[tauri::command]
|
|
161
|
+
fn get_db_connection(db: State<'_, DatabaseState>) -> String {
|
|
162
|
+
let db = db.0.lock().unwrap();
|
|
163
|
+
db.clone()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[tauri::command]
|
|
167
|
+
fn get_cache_connection(cache: State<'_, CacheState>) -> String {
|
|
168
|
+
let cache = cache.0.lock().unwrap();
|
|
169
|
+
cache.clone()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn main() {
|
|
173
|
+
tauri::Builder::default()
|
|
174
|
+
.manage(DatabaseState::new(String::from("postgres://localhost")))
|
|
175
|
+
.manage(CacheState::new(String::from("redis://localhost")))
|
|
176
|
+
// ✅ 이제 각각 고유한 타입이므로 둘 다 등록됨
|
|
177
|
+
.invoke_handler(tauri::generate_handler![
|
|
178
|
+
get_db_connection,
|
|
179
|
+
get_cache_connection
|
|
180
|
+
])
|
|
181
|
+
.run(tauri::generate_context!())
|
|
182
|
+
.expect("error while running tauri application");
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 추가 컨텍스트
|
|
187
|
+
|
|
188
|
+
**Tauri의 타입 기반 상태 시스템:**
|
|
189
|
+
|
|
190
|
+
```rust
|
|
191
|
+
// Tauri 내부적으로 TypeMap 사용
|
|
192
|
+
// TypeMap<TypeId, Box<dyn Any>>
|
|
193
|
+
// 각 타입당 하나의 값만 저장 가능
|
|
194
|
+
|
|
195
|
+
.manage(value1) // TypeId::of::<T>() → value1
|
|
196
|
+
.manage(value2) // TypeId::of::<T>() → value2 (value1 덮어쓰기 또는 무시)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**패턴 비교:**
|
|
200
|
+
|
|
201
|
+
| 패턴 | 장점 | 단점 | 권장 |
|
|
202
|
+
|------|------|------|------|
|
|
203
|
+
| **단일 컨테이너** | 단순, 명확 | 큰 구조체 | ✅ 대부분의 경우 |
|
|
204
|
+
| **Newtype** | 타입 안전 | 보일러플레이트 | ⚠️ 필요 시만 |
|
|
205
|
+
| **여러 manage()** | - | ❌ 작동 안함 | ❌ 사용 금지 |
|
|
206
|
+
|
|
207
|
+
**컨테이너 구조체 설계:**
|
|
208
|
+
|
|
209
|
+
```rust
|
|
210
|
+
// ✅ 도메인별 그룹화
|
|
211
|
+
pub struct AppStateInner {
|
|
212
|
+
// 데이터베이스 관련
|
|
213
|
+
pub db_pool: DatabasePool,
|
|
214
|
+
pub db_url: String,
|
|
215
|
+
|
|
216
|
+
// 캐시 관련
|
|
217
|
+
pub cache_pool: CachePool,
|
|
218
|
+
pub cache_url: String,
|
|
219
|
+
|
|
220
|
+
// 인증 관련
|
|
221
|
+
pub jwt_secret: String,
|
|
222
|
+
pub api_key: String,
|
|
223
|
+
|
|
224
|
+
// 설정 관련
|
|
225
|
+
pub config: AppConfig
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**env 변수 로드:**
|
|
230
|
+
|
|
231
|
+
```rust
|
|
232
|
+
use std::env;
|
|
233
|
+
|
|
234
|
+
impl AppStateInner {
|
|
235
|
+
pub fn from_env() -> Self {
|
|
236
|
+
Self {
|
|
237
|
+
database_url: env::var("DATABASE_URL")
|
|
238
|
+
.unwrap_or_else(|_| String::from("postgres://localhost")),
|
|
239
|
+
cache_url: env::var("REDIS_URL")
|
|
240
|
+
.unwrap_or_else(|_| String::from("redis://localhost")),
|
|
241
|
+
api_key: env::var("API_KEY")
|
|
242
|
+
.expect("API_KEY must be set"),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn main() {
|
|
248
|
+
tauri::Builder::default()
|
|
249
|
+
.manage(AppState::new(AppStateInner::from_env()))
|
|
250
|
+
.run(tauri::generate_context!())
|
|
251
|
+
.expect("error while running tauri application");
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**디버깅 팁:**
|
|
256
|
+
|
|
257
|
+
```rust
|
|
258
|
+
// State를 찾을 수 없을 때 런타임 에러
|
|
259
|
+
#[tauri::command]
|
|
260
|
+
fn test_command(state: State<'_, WrongType>) {
|
|
261
|
+
// panic: no state of type `WrongType` is managed
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 등록된 상태 확인
|
|
265
|
+
println!("Registered state: {:?}", std::any::type_name::<AppState>());
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**참고:**
|
|
269
|
+
- Tauri State Management: [Managing State](https://tauri.app/v2/guides/features/state-management/)
|
|
270
|
+
- Rust TypeId: [std::any::TypeId](https://doc.rust-lang.org/std/any/struct.TypeId.html)
|
|
271
|
+
|
|
272
|
+
**영향도:**
|
|
273
|
+
- 타입 안전성: MEDIUM (타입 충돌 방지)
|
|
274
|
+
- 버그 가능성: HIGH (잘못된 상태 사용 방지)
|
|
275
|
+
- 유지보수: HIGH (명확한 구조)
|