@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,239 @@
|
|
|
1
|
+
# invoke 실패에 대한 ErrorBoundary 처리
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
Tauri Command 호출은 Rust panic, 권한 거부, 타입 불일치 등 다양한 이유로 실패할 수 있습니다. ErrorBoundary 없이는 전체 앱이 크래시되거나 사용자에게 불친절한 에러 메시지가 노출됩니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function DataViewer() {
|
|
14
|
+
const [data, setData] = useState(null)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// ❌ 에러 처리 없음, 실패 시 앱 크래시
|
|
18
|
+
invoke('fetch_data').then(setData)
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
// data가 null이면 렌더링 에러 발생 가능
|
|
22
|
+
return <div>{data.items.map(...)}</div>
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**문제점:**
|
|
27
|
+
- try/catch 없는 invoke 호출
|
|
28
|
+
- 에러 상태 관리 없음
|
|
29
|
+
- 사용자에게 피드백 없음
|
|
30
|
+
|
|
31
|
+
## ✅ 올바른 패턴
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
35
|
+
import { useEffect, useState } from 'react'
|
|
36
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
37
|
+
|
|
38
|
+
function DataViewer() {
|
|
39
|
+
const [data, setData] = useState<DataType | null>(null)
|
|
40
|
+
const [error, setError] = useState<Error | null>(null)
|
|
41
|
+
const [loading, setLoading] = useState(true)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
invoke<DataType>('fetch_data')
|
|
45
|
+
.then(setData)
|
|
46
|
+
.catch(err => setError(err))
|
|
47
|
+
.finally(() => setLoading(false))
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
if (loading) return <div>Loading...</div>
|
|
51
|
+
if (error) throw error // ErrorBoundary로 전파
|
|
52
|
+
if (!data) return <div>No data</div>
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
{data.items.map(item => (
|
|
57
|
+
<div key={item.id}>{item.name}</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function App() {
|
|
64
|
+
return (
|
|
65
|
+
<ErrorBoundary
|
|
66
|
+
fallback={<ErrorFallback />}
|
|
67
|
+
onError={(error, errorInfo) => {
|
|
68
|
+
console.error('Caught by ErrorBoundary:', error, errorInfo)
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<DataViewer />
|
|
72
|
+
</ErrorBoundary>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ErrorFallback({ error, resetErrorBoundary }: {
|
|
77
|
+
error: Error
|
|
78
|
+
resetErrorBoundary: () => void
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<div role="alert">
|
|
82
|
+
<h2>Something went wrong</h2>
|
|
83
|
+
<pre>{error.message}</pre>
|
|
84
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Tauri 특화 에러 타입 처리:**
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
94
|
+
|
|
95
|
+
type TauriError = {
|
|
96
|
+
message: string
|
|
97
|
+
kind?: 'PermissionDenied' | 'NotFound' | 'InvalidData' | 'Unknown'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseTauriError(error: unknown): TauriError {
|
|
101
|
+
if (typeof error === 'string') {
|
|
102
|
+
// Rust에서 String 에러
|
|
103
|
+
return { message: error, kind: 'Unknown' }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (error instanceof Error) {
|
|
107
|
+
// JS Error 객체
|
|
108
|
+
const message = error.message
|
|
109
|
+
|
|
110
|
+
if (message.includes('permission denied')) {
|
|
111
|
+
return { message, kind: 'PermissionDenied' }
|
|
112
|
+
}
|
|
113
|
+
if (message.includes('not found')) {
|
|
114
|
+
return { message, kind: 'NotFound' }
|
|
115
|
+
}
|
|
116
|
+
if (message.includes('invalid')) {
|
|
117
|
+
return { message, kind: 'InvalidData' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { message, kind: 'Unknown' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { message: 'Unknown error', kind: 'Unknown' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function DataViewer() {
|
|
127
|
+
const [error, setError] = useState<TauriError | null>(null)
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
invoke('fetch_data')
|
|
131
|
+
.then(setData)
|
|
132
|
+
.catch(err => setError(parseTauriError(err)))
|
|
133
|
+
}, [])
|
|
134
|
+
|
|
135
|
+
if (error) {
|
|
136
|
+
if (error.kind === 'PermissionDenied') {
|
|
137
|
+
return <div>권한이 필요합니다. 설정에서 권한을 허용해주세요.</div>
|
|
138
|
+
}
|
|
139
|
+
if (error.kind === 'NotFound') {
|
|
140
|
+
return <div>데이터를 찾을 수 없습니다.</div>
|
|
141
|
+
}
|
|
142
|
+
return <div>오류: {error.message}</div>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return <div>{/* ... */}</div>
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**재시도 로직:**
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
153
|
+
import { useState, useCallback } from 'react'
|
|
154
|
+
|
|
155
|
+
function useInvokeWithRetry<T>(
|
|
156
|
+
command: string,
|
|
157
|
+
args?: Record<string, unknown>,
|
|
158
|
+
maxRetries = 3
|
|
159
|
+
) {
|
|
160
|
+
const [data, setData] = useState<T | null>(null)
|
|
161
|
+
const [error, setError] = useState<Error | null>(null)
|
|
162
|
+
const [loading, setLoading] = useState(false)
|
|
163
|
+
const [retries, setRetries] = useState(0)
|
|
164
|
+
|
|
165
|
+
const execute = useCallback(async () => {
|
|
166
|
+
setLoading(true)
|
|
167
|
+
setError(null)
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
170
|
+
try {
|
|
171
|
+
const result = await invoke<T>(command, args)
|
|
172
|
+
setData(result)
|
|
173
|
+
setRetries(i)
|
|
174
|
+
return
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (i === maxRetries) {
|
|
177
|
+
setError(err as Error)
|
|
178
|
+
setRetries(i)
|
|
179
|
+
} else {
|
|
180
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setLoading(false)
|
|
186
|
+
}, [command, args, maxRetries])
|
|
187
|
+
|
|
188
|
+
return { data, error, loading, retries, execute }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 사용 예시
|
|
192
|
+
function DataViewer() {
|
|
193
|
+
const { data, error, loading, retries, execute } = useInvokeWithRetry<Data>(
|
|
194
|
+
'fetch_data'
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
execute()
|
|
199
|
+
}, [execute])
|
|
200
|
+
|
|
201
|
+
if (loading) return <div>Loading... (Attempt {retries + 1})</div>
|
|
202
|
+
if (error) return <div>Failed after {retries} retries: {error.message}</div>
|
|
203
|
+
return <div>{data && JSON.stringify(data)}</div>
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 추가 컨텍스트
|
|
208
|
+
|
|
209
|
+
**ErrorBoundary 라이브러리:**
|
|
210
|
+
- [react-error-boundary](https://github.com/bvaughn/react-error-boundary): 가장 인기
|
|
211
|
+
- React 19+ 내장 ErrorBoundary (계획 중)
|
|
212
|
+
|
|
213
|
+
**Rust 에러를 사용자 친화적으로 변환:**
|
|
214
|
+
|
|
215
|
+
```rust
|
|
216
|
+
// Rust 측에서 에러 메시지 커스터마이징
|
|
217
|
+
#[tauri::command]
|
|
218
|
+
fn fetch_data() -> Result<Data, String> {
|
|
219
|
+
match load_data() {
|
|
220
|
+
Ok(data) => Ok(data),
|
|
221
|
+
Err(_) => Err("데이터를 불러올 수 없습니다. 나중에 다시 시도해주세요.".to_string())
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**전역 에러 처리:**
|
|
227
|
+
```tsx
|
|
228
|
+
function App() {
|
|
229
|
+
return (
|
|
230
|
+
<ErrorBoundary fallback={<GlobalError />}>
|
|
231
|
+
<Router />
|
|
232
|
+
</ErrorBoundary>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**참고:** [Error Handling in Tauri](https://beta.tauri.app/develop/calling-rust/#error-handling)
|
|
238
|
+
|
|
239
|
+
영향도: HIGH - 사용자 경험, 앱 안정성, 에러 복구 가능성
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# listen/unlisten 라이프사이클 관리
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
Tauri의 이벤트 리스너는 수동으로 등록 해제하지 않으면 메모리에 남아 누수를 일으킵니다. 컴포넌트가 여러 번 마운트/언마운트되는 경우 리스너가 중복 등록되어 동일 이벤트에 여러 핸들러가 실행될 수 있습니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { listen } from '@tauri-apps/api/event'
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function NotificationPanel() {
|
|
14
|
+
const [messages, setMessages] = useState<string[]>([])
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// ❌ unlisten 호출 없음 (메모리 누수)
|
|
18
|
+
listen<string>('notification', event => {
|
|
19
|
+
setMessages(prev => [...prev, event.payload])
|
|
20
|
+
})
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
{messages.map((msg, i) => <div key={i}>{msg}</div>)}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**문제점:**
|
|
32
|
+
- cleanup에서 unlisten 호출 안 함 (리스너 계속 활성 상태)
|
|
33
|
+
- 컴포넌트 재마운트 시 리스너 중복 등록
|
|
34
|
+
- 언마운트된 컴포넌트에서 setState 호출 위험
|
|
35
|
+
|
|
36
|
+
## ✅ 올바른 패턴
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { listen, UnlistenFn } from '@tauri-apps/api/event'
|
|
40
|
+
import { useEffect, useState } from 'react'
|
|
41
|
+
|
|
42
|
+
function NotificationPanel() {
|
|
43
|
+
const [messages, setMessages] = useState<string[]>([])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let unlisten: UnlistenFn | undefined
|
|
47
|
+
|
|
48
|
+
listen<string>('notification', event => {
|
|
49
|
+
setMessages(prev => [...prev, event.payload])
|
|
50
|
+
}).then(unlistenFn => {
|
|
51
|
+
unlisten = unlistenFn
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
unlisten?.()
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
{messages.map((msg, i) => <div key={i}>{msg}</div>)}
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**once() 패턴 (일회성 이벤트):**
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { once } from '@tauri-apps/api/event'
|
|
71
|
+
|
|
72
|
+
function WelcomeScreen() {
|
|
73
|
+
const [ready, setReady] = useState(false)
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let unlisten: UnlistenFn | undefined
|
|
77
|
+
|
|
78
|
+
once<void>('app-ready', () => {
|
|
79
|
+
setReady(true)
|
|
80
|
+
}).then(unlistenFn => {
|
|
81
|
+
unlisten = unlistenFn
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return () => unlisten?.()
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
return ready ? <MainApp /> : <SplashScreen />
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**커스텀 hook 패턴:**
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { listen, Event, UnlistenFn } from '@tauri-apps/api/event'
|
|
95
|
+
import { useEffect, useState } from 'react'
|
|
96
|
+
|
|
97
|
+
function useEvent<T>(
|
|
98
|
+
eventName: string,
|
|
99
|
+
handler: (event: Event<T>) => void
|
|
100
|
+
) {
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
let unlisten: UnlistenFn | undefined
|
|
103
|
+
|
|
104
|
+
listen<T>(eventName, handler).then(unlistenFn => {
|
|
105
|
+
unlisten = unlistenFn
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return () => unlisten?.()
|
|
109
|
+
}, [eventName, handler])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 사용 예시
|
|
113
|
+
function StatusBar() {
|
|
114
|
+
const [status, setStatus] = useState('idle')
|
|
115
|
+
|
|
116
|
+
useEvent<string>('status-update', event => {
|
|
117
|
+
setStatus(event.payload)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return <div>Status: {status}</div>
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 추가 컨텍스트
|
|
125
|
+
|
|
126
|
+
**cleanup 타이밍:**
|
|
127
|
+
- useEffect return 함수는 언마운트 시 또는 의존성 변경 시 실행
|
|
128
|
+
- 리스너는 항상 cleanup에서 제거해야 메모리 누수 방지
|
|
129
|
+
|
|
130
|
+
**글로벌 이벤트 vs 로컬 이벤트:**
|
|
131
|
+
- `listen()`: 모든 윈도우에서 수신
|
|
132
|
+
- `emit()`: 현재 윈도우로 전송
|
|
133
|
+
- `once()`: 한 번만 실행 후 자동 제거
|
|
134
|
+
|
|
135
|
+
**타입 안정성:**
|
|
136
|
+
```tsx
|
|
137
|
+
type NotificationPayload = {
|
|
138
|
+
title: string
|
|
139
|
+
body: string
|
|
140
|
+
level: 'info' | 'warning' | 'error'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
listen<NotificationPayload>('notification', event => {
|
|
144
|
+
// event.payload는 타입 안전하게 추론됨
|
|
145
|
+
console.log(event.payload.title)
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**참고:** [Tauri Events Guide](https://beta.tauri.app/develop/calling-rust/#events)
|
|
150
|
+
|
|
151
|
+
영향도: HIGH - 메모리 누수, 중복 핸들러 실행 방지
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# convertFileSrc()로 로컬 파일 렌더링
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
Tauri 앱에서 로컬 파일을 브라우저에서 렌더링하려면 `asset:` 프로토콜 URL로 변환해야 합니다. 직접 파일 경로를 사용하면 CORS 에러가 발생하고, base64 변환은 메모리 낭비와 성능 저하를 일으킵니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
11
|
+
import { useState, useEffect } from 'react'
|
|
12
|
+
|
|
13
|
+
function ImageViewer({ filePath }: { filePath: string }) {
|
|
14
|
+
const [imageData, setImageData] = useState('')
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// ❌ 파일을 base64로 읽어서 메모리에 전체 로드
|
|
18
|
+
invoke<string>('read_file_as_base64', { path: filePath })
|
|
19
|
+
.then(base64 => setImageData(`data:image/png;base64,${base64}`))
|
|
20
|
+
}, [filePath])
|
|
21
|
+
|
|
22
|
+
return <img src={imageData} alt="Local file" />
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**문제점:**
|
|
27
|
+
- 파일 전체를 메모리에 로드 (큰 파일 시 느림)
|
|
28
|
+
- Rust ↔ JS 직렬화 오버헤드
|
|
29
|
+
- 비디오/오디오 스트리밍 불가능
|
|
30
|
+
|
|
31
|
+
## ✅ 올바른 패턴
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
35
|
+
|
|
36
|
+
function ImageViewer({ filePath }: { filePath: string }) {
|
|
37
|
+
// ✅ 파일 경로를 asset: 프로토콜 URL로 변환
|
|
38
|
+
const assetUrl = convertFileSrc(filePath)
|
|
39
|
+
|
|
40
|
+
return <img src={assetUrl} alt="Local file" />
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**비디오/오디오 예시:**
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
48
|
+
|
|
49
|
+
function VideoPlayer({ videoPath }: { videoPath: string }) {
|
|
50
|
+
const videoUrl = convertFileSrc(videoPath)
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<video controls>
|
|
54
|
+
<source src={videoUrl} type="video/mp4" />
|
|
55
|
+
Your browser does not support video playback.
|
|
56
|
+
</video>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function AudioPlayer({ audioPath }: { audioPath: string }) {
|
|
61
|
+
const audioUrl = convertFileSrc(audioPath)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<audio controls>
|
|
65
|
+
<source src={audioUrl} type="audio/mpeg" />
|
|
66
|
+
</audio>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**PDF 뷰어 예시:**
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
75
|
+
|
|
76
|
+
function PDFViewer({ pdfPath }: { pdfPath: string }) {
|
|
77
|
+
const pdfUrl = convertFileSrc(pdfPath)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<iframe
|
|
81
|
+
src={pdfUrl}
|
|
82
|
+
width="100%"
|
|
83
|
+
height="600px"
|
|
84
|
+
title="PDF Viewer"
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**동적 파일 목록:**
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
94
|
+
import { readDir } from '@tauri-apps/plugin-fs'
|
|
95
|
+
import { useEffect, useState } from 'react'
|
|
96
|
+
|
|
97
|
+
function ImageGallery({ dirPath }: { dirPath: string }) {
|
|
98
|
+
const [images, setImages] = useState<string[]>([])
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
readDir(dirPath).then(entries => {
|
|
102
|
+
const imageUrls = entries
|
|
103
|
+
.filter(e => /\.(png|jpg|jpeg|gif)$/i.test(e.name))
|
|
104
|
+
.map(e => convertFileSrc(e.path))
|
|
105
|
+
setImages(imageUrls)
|
|
106
|
+
})
|
|
107
|
+
}, [dirPath])
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="gallery">
|
|
111
|
+
{images.map((url, i) => (
|
|
112
|
+
<img key={i} src={url} alt={`Image ${i}`} />
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 추가 컨텍스트
|
|
120
|
+
|
|
121
|
+
**asset: 프로토콜 작동 방식:**
|
|
122
|
+
- `convertFileSrc('/path/to/file.png')` → `asset://localhost/path/to/file.png`
|
|
123
|
+
- Tauri 런타임이 프로토콜 요청을 가로채서 파일 스트림 제공
|
|
124
|
+
- 브라우저는 일반 HTTP 리소스처럼 처리 (캐싱, 부분 로드 지원)
|
|
125
|
+
|
|
126
|
+
**권한 설정 (tauri.conf.json):**
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"app": {
|
|
131
|
+
"security": {
|
|
132
|
+
"assetProtocol": {
|
|
133
|
+
"enable": true,
|
|
134
|
+
"scope": [
|
|
135
|
+
"$APPDATA/**",
|
|
136
|
+
"$RESOURCE/**"
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**파일 경로 타입:**
|
|
145
|
+
- 절대 경로: `/Users/name/file.png`, `C:\Users\name\file.png`
|
|
146
|
+
- 앱 리소스 경로: `$RESOURCE/assets/image.png`
|
|
147
|
+
- AppData 경로: `$APPDATA/cache/thumb.png`
|
|
148
|
+
|
|
149
|
+
**브라우저 캐싱:**
|
|
150
|
+
- asset: URL은 브라우저 캐시 활용 (동일 파일 재요청 방지)
|
|
151
|
+
- base64 Data URL은 캐싱 불가
|
|
152
|
+
|
|
153
|
+
**참고:** [Asset Protocol Scope](https://beta.tauri.app/develop/calling-rust/#asset-protocol-scope)
|
|
154
|
+
|
|
155
|
+
영향도: HIGH - 성능, 메모리 사용량, 스트리밍 지원
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# useEffect + invoke 패턴으로 Tauri Command 호출
|
|
2
|
+
|
|
3
|
+
## 왜 중요한가
|
|
4
|
+
|
|
5
|
+
Tauri의 `invoke()` 호출은 비동기 IPC이므로 React 컴포넌트 라이프사이클과 통합 시 cleanup이 필수입니다. 컴포넌트 언마운트 후 응답이 도착하면 "Can't perform state update on unmounted component" 경고와 메모리 누수가 발생합니다.
|
|
6
|
+
|
|
7
|
+
## ❌ 잘못된 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
14
|
+
const [user, setUser] = useState(null)
|
|
15
|
+
|
|
16
|
+
// ❌ cleanup 없음, 언마운트 후 setState 위험
|
|
17
|
+
invoke('get_user', { userId })
|
|
18
|
+
.then(setUser)
|
|
19
|
+
.catch(console.error)
|
|
20
|
+
|
|
21
|
+
return user ? <div>{user.name}</div> : <div>Loading...</div>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**문제점:**
|
|
26
|
+
- useEffect 없이 invoke 호출 (매 렌더링마다 실행)
|
|
27
|
+
- cleanup 로직 없음 (언마운트 후 setState)
|
|
28
|
+
- 에러 처리 불충분
|
|
29
|
+
|
|
30
|
+
## ✅ 올바른 패턴
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
34
|
+
import { useEffect, useState } from 'react'
|
|
35
|
+
|
|
36
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
37
|
+
const [user, setUser] = useState<User | null>(null)
|
|
38
|
+
const [error, setError] = useState<string | null>(null)
|
|
39
|
+
const [loading, setLoading] = useState(true)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
let cancelled = false
|
|
43
|
+
|
|
44
|
+
setLoading(true)
|
|
45
|
+
setError(null)
|
|
46
|
+
|
|
47
|
+
invoke<User>('get_user', { userId })
|
|
48
|
+
.then(data => {
|
|
49
|
+
if (!cancelled) setUser(data)
|
|
50
|
+
})
|
|
51
|
+
.catch(err => {
|
|
52
|
+
if (!cancelled) setError(err.message)
|
|
53
|
+
})
|
|
54
|
+
.finally(() => {
|
|
55
|
+
if (!cancelled) setLoading(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true
|
|
60
|
+
}
|
|
61
|
+
}, [userId])
|
|
62
|
+
|
|
63
|
+
if (loading) return <div>Loading...</div>
|
|
64
|
+
if (error) return <div>Error: {error}</div>
|
|
65
|
+
return user ? <div>{user.name}</div> : null
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**개선된 커스텀 hook 패턴:**
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { invoke } from '@tauri-apps/api/core'
|
|
73
|
+
import { useEffect, useState } from 'react'
|
|
74
|
+
|
|
75
|
+
function useInvoke<T>(
|
|
76
|
+
command: string,
|
|
77
|
+
args?: Record<string, unknown>
|
|
78
|
+
) {
|
|
79
|
+
const [data, setData] = useState<T | null>(null)
|
|
80
|
+
const [error, setError] = useState<Error | null>(null)
|
|
81
|
+
const [loading, setLoading] = useState(true)
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
let cancelled = false
|
|
85
|
+
|
|
86
|
+
setLoading(true)
|
|
87
|
+
setError(null)
|
|
88
|
+
|
|
89
|
+
invoke<T>(command, args)
|
|
90
|
+
.then(result => !cancelled && setData(result))
|
|
91
|
+
.catch(err => !cancelled && setError(err))
|
|
92
|
+
.finally(() => !cancelled && setLoading(false))
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true
|
|
96
|
+
}
|
|
97
|
+
}, [command, JSON.stringify(args)])
|
|
98
|
+
|
|
99
|
+
return { data, error, loading }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 사용 예시
|
|
103
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
104
|
+
const { data: user, error, loading } = useInvoke<User>(
|
|
105
|
+
'get_user',
|
|
106
|
+
{ userId }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (loading) return <div>Loading...</div>
|
|
110
|
+
if (error) return <div>Error: {error.message}</div>
|
|
111
|
+
return user ? <div>{user.name}</div> : null
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 추가 컨텍스트
|
|
116
|
+
|
|
117
|
+
**AbortController 패턴 (Tauri v2.1+):**
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const controller = new AbortController()
|
|
122
|
+
|
|
123
|
+
invoke('long_running_task', { signal: controller.signal })
|
|
124
|
+
.then(setResult)
|
|
125
|
+
.catch(err => {
|
|
126
|
+
if (err.name !== 'AbortError') setError(err)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return () => controller.abort()
|
|
130
|
+
}, [])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**권장 의존성 관리:**
|
|
134
|
+
- 원시 값 (`userId`, `query`): 직접 의존성 배열에 추가
|
|
135
|
+
- 객체/배열 args: `JSON.stringify(args)` 또는 `useMemo`로 안정화
|
|
136
|
+
|
|
137
|
+
**참고:** TanStack Query와 함께 사용 시 `queryFn`에서 invoke 호출하면 자동 cleanup 제공.
|
|
138
|
+
|
|
139
|
+
영향도: HIGH - 메모리 누수, 상태 버그, 콘솔 경고 방지
|