@wooojin/forgen 0.4.7 → 0.4.9
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/checks/self-score-deflation.js +6 -4
- package/dist/cli.js +47 -2
- package/dist/core/auto-compound-runner.js +6 -2
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dashboard.js +2 -2
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/doctor.d.ts +10 -0
- package/dist/core/doctor.js +49 -8
- package/dist/core/harness.js +8 -2
- package/dist/core/init.js +53 -0
- package/dist/core/inspect-cli.js +4 -4
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/migrate-evidence-host.js +1 -1
- package/dist/core/notify.js +7 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/paths.d.ts +16 -2
- package/dist/core/paths.js +16 -2
- package/dist/core/session-store.d.ts +12 -1
- package/dist/core/session-store.js +77 -1
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +191 -8
- package/dist/core/statusline-cli.js +34 -1
- package/dist/core/v1-bootstrap.d.ts +7 -0
- package/dist/core/v1-bootstrap.js +28 -6
- package/dist/engine/compound-extractor.js +40 -1
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/engine/learn-cli.js +2 -2
- package/dist/engine/lifecycle/bypass-detector.js +3 -2
- package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
- package/dist/engine/lifecycle/signals.js +2 -2
- package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
- package/dist/engine/solution-candidate.js +1 -1
- package/dist/engine/solution-outcomes.js +1 -1
- package/dist/engine/solution-quarantine.js +1 -1
- package/dist/engine/solution-weakness.js +8 -2
- package/dist/forge/cli.js +1 -1
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/shared/hook-response.js +1 -1
- package/dist/hooks/shared/hook-timing.js +3 -3
- package/dist/hooks/solution-injector.js +94 -1
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +72 -1
- package/dist/host/install-orchestrator.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/preset/facet-catalog.js +2 -2
- package/dist/renderer/rule-renderer.js +7 -7
- package/dist/store/compound-usage-store.js +1 -1
- package/dist/store/implicit-feedback-store.js +2 -2
- package/dist/store/profile-store.d.ts +11 -0
- package/dist/store/profile-store.js +23 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Node.js + TypeScript 원칙
|
|
3
|
+
version: 2026-05-18
|
|
4
|
+
sources:
|
|
5
|
+
- sources/node-runtime/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Node.js + TypeScript 원칙
|
|
9
|
+
|
|
10
|
+
> [공통 원칙](./common.md)을 먼저 따르고, 아래는 Node.js/TypeScript 특화.
|
|
11
|
+
|
|
12
|
+
## N0. 의사결정 우선순위
|
|
13
|
+
|
|
14
|
+
1. **비동기는 async/await 일관성** — callback 혼용 금지
|
|
15
|
+
2. **process 안정성** — unhandledRejection / uncaughtException 반드시 처리
|
|
16
|
+
3. **Event Loop 보호** — CPU heavy는 worker_threads로 격리
|
|
17
|
+
4. **입력 경계 강화** — Zod/io-ts 런타임 검증
|
|
18
|
+
5. **TypeScript strict** — 타입 시스템을 안전망으로 최대 활용
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## N1. async/await 일관성
|
|
23
|
+
|
|
24
|
+
**callback과 async/await를 혼용하지 마라. async/await로 통일한다.**
|
|
25
|
+
|
|
26
|
+
### N1.1 기본 규칙
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// WRONG: Promise + callback 혼용
|
|
30
|
+
function fetchUser(id: string, callback: (err: Error | null, user?: User) => void) {
|
|
31
|
+
db.query('SELECT * FROM users WHERE id = ?', [id])
|
|
32
|
+
.then(result => callback(null, result))
|
|
33
|
+
.catch(err => callback(err));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// RIGHT: async/await 통일
|
|
37
|
+
async function fetchUser(id: string): Promise<User> {
|
|
38
|
+
const result = await db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### N1.2 기존 callback API 래핑
|
|
44
|
+
|
|
45
|
+
Node.js 레거시 API (`fs.readFile` 등)는 `util.promisify` 또는 `fs/promises` 사용:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { readFile } from 'node:fs/promises'; // 모던 방식
|
|
49
|
+
// 또는
|
|
50
|
+
import { promisify } from 'node:util';
|
|
51
|
+
import { readFile } from 'node:fs';
|
|
52
|
+
const readFileAsync = promisify(readFile); // 레거시 래핑
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### N1.3 병렬 처리
|
|
56
|
+
|
|
57
|
+
독립적인 async 작업은 `Promise.all` / `Promise.allSettled`:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// WRONG: 순차 await (불필요한 직렬화)
|
|
61
|
+
const user = await fetchUser(userId);
|
|
62
|
+
const orders = await fetchOrders(userId);
|
|
63
|
+
|
|
64
|
+
// RIGHT: 병렬 실행
|
|
65
|
+
const [user, orders] = await Promise.all([fetchUser(userId), fetchOrders(userId)]);
|
|
66
|
+
|
|
67
|
+
// 일부 실패 허용 시
|
|
68
|
+
const results = await Promise.allSettled([fetchUser(userId), fetchOrders(userId)]);
|
|
69
|
+
results.forEach(result => {
|
|
70
|
+
if (result.status === 'rejected') logger.warn('fetch failed', result.reason);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## N2. Top-Level Error Handler
|
|
77
|
+
|
|
78
|
+
**process 수준 에러를 반드시 처리한다. 처리하지 않으면 서비스가 조용히 죽는다.**
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// app.ts — 진입점에서 최우선 등록
|
|
82
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
83
|
+
logger.error({ reason, promise }, 'Unhandled Promise Rejection');
|
|
84
|
+
// graceful exit: 진행 중인 요청 처리 후 종료
|
|
85
|
+
gracefulShutdown(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.on('uncaughtException', (err, origin) => {
|
|
89
|
+
logger.fatal({ err, origin }, 'Uncaught Exception — process will exit');
|
|
90
|
+
// uncaughtException 후 프로세스 상태 보장 불가 → 즉시 종료
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Graceful shutdown: SIGTERM (Docker/K8s 종료 신호)
|
|
95
|
+
process.on('SIGTERM', () => {
|
|
96
|
+
logger.info('SIGTERM received, starting graceful shutdown');
|
|
97
|
+
gracefulShutdown(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function gracefulShutdown(exitCode: number) {
|
|
101
|
+
// 1. 새 요청 거부 (load balancer 헬스체크 실패 대기)
|
|
102
|
+
server.close();
|
|
103
|
+
// 2. 진행 중 요청 완료 대기 (max 30s)
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 30_000));
|
|
105
|
+
// 3. DB 연결 종료
|
|
106
|
+
await db.destroy();
|
|
107
|
+
process.exit(exitCode);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### N2.1 Express/Fastify 에러 핸들러
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Express 에러 핸들러 (4인자 필수)
|
|
115
|
+
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
116
|
+
const statusCode = err instanceof AppError ? err.statusCode : 500;
|
|
117
|
+
logger.error({ err, requestId: req.id, path: req.path }, 'Request error');
|
|
118
|
+
res.status(statusCode).json({
|
|
119
|
+
error: {
|
|
120
|
+
code: err instanceof AppError ? err.code : 'INTERNAL_ERROR',
|
|
121
|
+
message: statusCode < 500 ? err.message : '서버 오류가 발생했습니다',
|
|
122
|
+
requestId: req.id,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Fastify는 setErrorHandler 사용
|
|
128
|
+
fastify.setErrorHandler((err, request, reply) => {
|
|
129
|
+
request.log.error({ err }, 'Request error');
|
|
130
|
+
reply.status(err.statusCode ?? 500).send({ error: { ... } });
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## N3. Event Loop 보호
|
|
137
|
+
|
|
138
|
+
**Node.js는 단일 스레드. CPU heavy 작업이 Event Loop를 막으면 모든 요청이 멈춘다.**
|
|
139
|
+
|
|
140
|
+
### N3.1 Event Loop 차단 탐지
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// 차단 탐지: toobusy-js 또는 clinic.js
|
|
144
|
+
import toobusy from 'toobusy-js';
|
|
145
|
+
app.use((req, res, next) => {
|
|
146
|
+
if (toobusy()) {
|
|
147
|
+
res.status(503).json({ error: { code: 'SERVICE_UNAVAILABLE', message: '일시적 과부하' } });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
next();
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### N3.2 CPU Heavy 작업 격리
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// worker_threads로 CPU 집약 작업 격리
|
|
158
|
+
import { Worker, workerData, parentPort } from 'node:worker_threads';
|
|
159
|
+
import { resolve } from 'node:path';
|
|
160
|
+
|
|
161
|
+
function runHeavyTask(input: unknown): Promise<unknown> {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const worker = new Worker(resolve(__dirname, 'workers/heavy-task.js'), {
|
|
164
|
+
workerData: input,
|
|
165
|
+
});
|
|
166
|
+
worker.on('message', resolve);
|
|
167
|
+
worker.on('error', reject);
|
|
168
|
+
worker.on('exit', code => {
|
|
169
|
+
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
차단 기준: 10ms 이상 동기 CPU 연산 → worker_threads 고려.
|
|
176
|
+
- 이미지 리사이징 (sharp는 내부 libuv 스레드풀 사용 → 괜찮음)
|
|
177
|
+
- 암호화 (crypto 모듈 → 내부 C++ 바인딩, 대부분 OK)
|
|
178
|
+
- JSON 파싱 대용량 (>1MB) → 차단 위험
|
|
179
|
+
- bcrypt 해싱 → 동기 방식 금지, 비동기 사용
|
|
180
|
+
|
|
181
|
+
### N3.3 setImmediate / process.nextTick 사용 기준
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// process.nextTick: 현재 operation 완료 즉시 (I/O 앞)
|
|
185
|
+
// 주의: 무한 루프 가능 → 재귀 호출 금지
|
|
186
|
+
process.nextTick(() => emitter.emit('ready'));
|
|
187
|
+
|
|
188
|
+
// setImmediate: 현재 I/O 이벤트 사이클 이후
|
|
189
|
+
// 긴 작업을 여러 tick으로 분산할 때
|
|
190
|
+
function processLargeArray(arr: unknown[], callback: () => void, index = 0) {
|
|
191
|
+
if (index >= arr.length) return callback();
|
|
192
|
+
processSingleItem(arr[index]);
|
|
193
|
+
setImmediate(() => processLargeArray(arr, callback, index + 1));
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## N4. Stream Backpressure 존중
|
|
200
|
+
|
|
201
|
+
**readable.pipe()는 자동으로 backpressure를 처리한다. 수동 구현 시 반드시 확인.**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// WRONG: backpressure 무시
|
|
205
|
+
readable.on('data', chunk => {
|
|
206
|
+
writable.write(chunk); // writable buffer가 가득 차도 계속 push
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// RIGHT: pipe 사용 (자동 backpressure)
|
|
210
|
+
readable.pipe(writable);
|
|
211
|
+
|
|
212
|
+
// 또는 stream.pipeline (에러 처리 포함)
|
|
213
|
+
import { pipeline } from 'node:stream/promises';
|
|
214
|
+
await pipeline(
|
|
215
|
+
fs.createReadStream('large-file.csv'),
|
|
216
|
+
new TransformStream(), // 변환 단계
|
|
217
|
+
fs.createWriteStream('output.csv'),
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### N4.1 고수량 스트림 패턴
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Readable stream async iterator (Node.js 10+)
|
|
225
|
+
async function processCSV(filePath: string): Promise<void> {
|
|
226
|
+
const readable = fs.createReadStream(filePath).pipe(csvParser());
|
|
227
|
+
for await (const row of readable) {
|
|
228
|
+
await processRow(row); // 처리 완료 후 다음 청크 요청 (backpressure 자연스럽게 적용)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## N5. 입력 경계 검증 (Zod)
|
|
236
|
+
|
|
237
|
+
**런타임 검증 없는 TypeScript 타입 단언(`as User`)은 안전하지 않다.**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { z } from 'zod';
|
|
241
|
+
|
|
242
|
+
// 스키마 정의 — single source of truth
|
|
243
|
+
const CreateOrderSchema = z.object({
|
|
244
|
+
userId: z.string().uuid(),
|
|
245
|
+
items: z.array(
|
|
246
|
+
z.object({
|
|
247
|
+
productId: z.string().uuid(),
|
|
248
|
+
quantity: z.number().int().positive(),
|
|
249
|
+
})
|
|
250
|
+
).min(1),
|
|
251
|
+
couponCode: z.string().optional(), // 옵셔널 명시 — 검증 로직에서 required 취급 금지
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
|
|
255
|
+
|
|
256
|
+
// 핸들러
|
|
257
|
+
async function createOrder(req: Request, res: Response) {
|
|
258
|
+
const parsed = CreateOrderSchema.safeParse(req.body);
|
|
259
|
+
if (!parsed.success) {
|
|
260
|
+
return res.status(400).json({
|
|
261
|
+
error: {
|
|
262
|
+
code: 'VALIDATION_ERROR',
|
|
263
|
+
message: '입력값이 올바르지 않습니다',
|
|
264
|
+
details: parsed.error.flatten().fieldErrors,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const input: CreateOrderInput = parsed.data;
|
|
269
|
+
// 이 시점부터 input은 타입 안전
|
|
270
|
+
await orderService.create(input);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **옵셔널 필드는 스키마와 비즈니스 로직에서 동일하게 처리**: `.optional()` 이면 값이 없어도 통과.
|
|
275
|
+
- 응답 직렬화도 검증: `z.output` 또는 `.transform()` 으로 응답 필드 선택.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## N6. TypeScript Strict 설정
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
// tsconfig.json — 최소 설정
|
|
283
|
+
{
|
|
284
|
+
"compilerOptions": {
|
|
285
|
+
"strict": true, // null 체크, implicit any, 등 통합
|
|
286
|
+
"noUncheckedIndexedAccess": true, // arr[0] 타입이 T | undefined (배열 경계 안전)
|
|
287
|
+
"exactOptionalPropertyTypes": true, // optional: 값이 undefined일 때 assign 금지
|
|
288
|
+
"noImplicitReturns": true, // 모든 분기에서 return 강제
|
|
289
|
+
"noFallthroughCasesInSwitch": true,
|
|
290
|
+
"forceConsistentCasingInFileNames": true,
|
|
291
|
+
"moduleResolution": "bundler", // ESM + bundler 환경
|
|
292
|
+
"target": "ES2022",
|
|
293
|
+
"lib": ["ES2022"]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### N6.1 타입 단언 규칙
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// WRONG: 타입 단언으로 런타임 오류 숨김
|
|
302
|
+
const user = cache.get('user') as User; // undefined일 수 있음
|
|
303
|
+
|
|
304
|
+
// RIGHT: 명시적 가드
|
|
305
|
+
const raw = cache.get('user');
|
|
306
|
+
if (!raw) throw new Error('Cache miss: user');
|
|
307
|
+
const user = UserSchema.parse(raw); // 런타임 검증
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- `as T` 단언은 외부 데이터, 레거시 코드와의 경계에서만 허용. 내부 코드에서는 타입 추론 활용.
|
|
311
|
+
- `!` non-null assertion은 null/undefined가 불가능한 이유를 주석으로 설명.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## N7. 의존성 트리 관리
|
|
316
|
+
|
|
317
|
+
근거: `sources/node-runtime/`
|
|
318
|
+
|
|
319
|
+
### N7.1 의존성 최소화 원칙
|
|
320
|
+
|
|
321
|
+
- 새 패키지 도입 전 체크리스트:
|
|
322
|
+
1. 표준 라이브러리(`node:fs`, `node:crypto`)로 해결 가능한가?
|
|
323
|
+
2. 기존 의존성으로 해결 가능한가?
|
|
324
|
+
3. 코드 10줄 미만이면 직접 구현하는 것이 낫지 않은가?
|
|
325
|
+
- 번들 크기 영향: `bundlephobia.com` 또는 `npm ls --depth=0` 확인.
|
|
326
|
+
|
|
327
|
+
### N7.2 보안 감사
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# 의존성 취약점 감사 (CI에 포함)
|
|
331
|
+
npm audit --audit-level=high
|
|
332
|
+
|
|
333
|
+
# 의존성 업데이트 (weekly)
|
|
334
|
+
npx npm-check-updates -u && npm install
|
|
335
|
+
|
|
336
|
+
# supply chain 감사
|
|
337
|
+
npx audit-ci --high
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### N7.3 Lock 파일 정책
|
|
341
|
+
|
|
342
|
+
- `package-lock.json` 또는 `pnpm-lock.yaml` 반드시 커밋.
|
|
343
|
+
- CI에서 `npm ci` (lockfile 기반 설치). `npm install` 금지.
|
|
344
|
+
- 의존성 업데이트는 별도 PR — 기능 PR에 섞지 않는다.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## N8. 구조화 로그 (Pino)
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import pino from 'pino';
|
|
352
|
+
|
|
353
|
+
const logger = pino({
|
|
354
|
+
level: process.env.LOG_LEVEL ?? 'info',
|
|
355
|
+
// 프로덕션: JSON 출력 (stdout)
|
|
356
|
+
// 개발: pino-pretty로 가독성 향상
|
|
357
|
+
transport: process.env.NODE_ENV === 'development'
|
|
358
|
+
? { target: 'pino-pretty', options: { colorize: true } }
|
|
359
|
+
: undefined,
|
|
360
|
+
// 민감 정보 자동 리댁션
|
|
361
|
+
redact: ['req.headers.authorization', '*.password', '*.cardNumber'],
|
|
362
|
+
base: {
|
|
363
|
+
service: process.env.SERVICE_NAME ?? 'unknown',
|
|
364
|
+
env: process.env.NODE_ENV ?? 'development',
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Request logger 미들웨어 (Fastify는 내장, Express는 pino-http)
|
|
369
|
+
import pinoHttp from 'pino-http';
|
|
370
|
+
app.use(pinoHttp({ logger }));
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## N9. Node.js 안티패턴 카탈로그
|
|
376
|
+
|
|
377
|
+
| 안티패턴 | 픽스 |
|
|
378
|
+
|----------|------|
|
|
379
|
+
| `process.on('unhandledRejection')` 미등록 | 앱 진입점에서 즉시 등록 |
|
|
380
|
+
| 동기 I/O (`fs.readFileSync`) in 핸들러 | `fs/promises` 비동기 버전 |
|
|
381
|
+
| `require()` 루프 내 동적 로딩 | 모듈 수준 캐싱 (require는 캐시됨, import() 동적 주의) |
|
|
382
|
+
| callback + async 혼용 | async/await 통일 |
|
|
383
|
+
| 런타임 검증 없는 `as T` 단언 | Zod safeParse |
|
|
384
|
+
| `npm install` in CI | `npm ci` (lockfile 기반) |
|
|
385
|
+
| 로그에 `req.body` 통째로 기록 | redact 설정 |
|
|
386
|
+
| `bcrypt.hashSync()` | `bcrypt.hash()` (비동기) |
|
|
387
|
+
| `JSON.parse(hugeString)` 동기 | 청크 분할 또는 worker_threads |
|
|
388
|
+
| `tsconfig.strict: false` | strict 활성화 |
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: be-build-go
|
|
3
|
+
description: Go 요구사항을 받아 합의된 사내 원칙대로 구현. 명세→API contract→구현→테스트 매핑을 강제하고, error-as-value·context 전파·goroutine 안전성을 적용한다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# be-build (Go)
|
|
7
|
+
|
|
8
|
+
> **호출 시점**: "이 API 명세대로 Go로 구현해줘", "이 서비스 Go로 만들어줘".
|
|
9
|
+
> **선행 로딩**: `principles/common.md` + `principles/go.md` 필수.
|
|
10
|
+
|
|
11
|
+
## 0. 절대 금지
|
|
12
|
+
|
|
13
|
+
1. 명세 읽기 전에 코드 쓰지 마라.
|
|
14
|
+
2. `panic` 을 일반 에러 처리에 사용 금지.
|
|
15
|
+
3. `context.Context` 없는 DB/HTTP 호출 금지.
|
|
16
|
+
4. goroutine 종료 조건 없이 시작 금지.
|
|
17
|
+
5. 에러 무시 (`_ = err`) 금지 — 이유가 있으면 주석 필수.
|
|
18
|
+
6. 인터페이스를 구현 패키지에서 정의 금지 (consumer 측 정의).
|
|
19
|
+
|
|
20
|
+
## 1. 워크플로우
|
|
21
|
+
|
|
22
|
+
### Step 1 — 요구사항 → 체크리스트 변환
|
|
23
|
+
|
|
24
|
+
명세 받자마자, 다른 어떤 작업도 하기 전에:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
## 요구사항 체크리스트
|
|
28
|
+
- [ ] R-01: <명세 원문 직접 인용>
|
|
29
|
+
- [ ] R-02: ...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
optional/required 구분 명시. optional은 "없어도 통과" 케이스 매핑.
|
|
33
|
+
|
|
34
|
+
### Step 2 — API Contract 정의
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
// types/order.go — 계약 먼저
|
|
38
|
+
type CreateOrderRequest struct {
|
|
39
|
+
UserID string `json:"userId" validate:"required,uuid"`
|
|
40
|
+
Items []OrderItem `json:"items" validate:"required,min=1,dive"`
|
|
41
|
+
CouponCode *string `json:"couponCode,omitempty"` // optional — pointer로 absent 구분
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type CreateOrderResponse struct {
|
|
45
|
+
OrderID string `json:"orderId"`
|
|
46
|
+
Status string `json:"status"`
|
|
47
|
+
CreatedAt time.Time `json:"createdAt"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type OrderItem struct {
|
|
51
|
+
ProductID string `json:"productId" validate:"required,uuid"`
|
|
52
|
+
Quantity int `json:"quantity" validate:"required,min=1"`
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Step 3 — 체크리스트 → 테스트 매핑표
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
## 매핑표
|
|
60
|
+
| 요구사항 | 함수 | 테스트 파일:케이스 |
|
|
61
|
+
|----------|------|---------------------|
|
|
62
|
+
| R-01 | OrderHandler.Create | order_test.go:TestCreate_Success |
|
|
63
|
+
| R-02 | OrderService.Validate | order_test.go:TestCreate_NoCoupon |
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Step 4 — 패키지 구조 결정
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
internal/
|
|
70
|
+
├─ handler/ HTTP 핸들러 (입력 검증, 라우팅)
|
|
71
|
+
├─ service/ 비즈니스 로직 (인터페이스 정의)
|
|
72
|
+
├─ repository/ DB 접근 (구현)
|
|
73
|
+
└─ domain/ 엔티티, 도메인 에러
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
결정 기록: "service 레이어가 트랜잭션 경계. repository는 순수 쿼리."
|
|
77
|
+
|
|
78
|
+
### Step 5 — TDD (Red → Green → Refactor)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
go test ./... -run TestCreate_Success # 실패 확인
|
|
82
|
+
# 구현
|
|
83
|
+
go test ./... -run TestCreate_Success # 통과 확인
|
|
84
|
+
go test -race ./... # 레이스 컨디션 확인
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 6 — 셀프 체크리스트
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
- [ ] 모든 함수 첫 인자가 context.Context (I/O 함수)
|
|
91
|
+
- [ ] goroutine 시작 시 종료 조건 존재
|
|
92
|
+
- [ ] 에러 래핑 — fmt.Errorf("funcName: %w", err)
|
|
93
|
+
- [ ] 에러 무시 없음
|
|
94
|
+
- [ ] golangci-lint 통과
|
|
95
|
+
- [ ] go test -race 통과
|
|
96
|
+
- [ ] optional 필드 pointer 또는 omitempty 처리
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Step 7 — 완료 선언
|
|
100
|
+
|
|
101
|
+
매핑표 모든 행 ✅ + 테스트 green + race 없음 + lint 통과.
|
|
102
|
+
|
|
103
|
+
## 2. 구현 디폴트
|
|
104
|
+
|
|
105
|
+
### 2.1 HTTP 핸들러 (net/http + chi)
|
|
106
|
+
|
|
107
|
+
```go
|
|
108
|
+
// handler/order.go
|
|
109
|
+
func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
110
|
+
var req CreateOrderRequest
|
|
111
|
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
112
|
+
respondError(w, http.StatusBadRequest, "INVALID_JSON", err.Error(), r)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if err := h.validate.Struct(req); err != nil {
|
|
116
|
+
respondError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error(), r)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
order, err := h.svc.CreateOrder(r.Context(), req)
|
|
121
|
+
if err != nil {
|
|
122
|
+
switch {
|
|
123
|
+
case errors.Is(err, domain.ErrUserNotFound):
|
|
124
|
+
respondError(w, http.StatusNotFound, "USER_NOT_FOUND", err.Error(), r)
|
|
125
|
+
case errors.Is(err, domain.ErrInsufficientStock):
|
|
126
|
+
respondError(w, http.StatusUnprocessableEntity, "INSUFFICIENT_STOCK", err.Error(), r)
|
|
127
|
+
default:
|
|
128
|
+
slog.ErrorContext(r.Context(), "create order failed", "err", err, "requestId", requestID(r))
|
|
129
|
+
respondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "서버 오류가 발생했습니다", r)
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
respondJSON(w, http.StatusCreated, order)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func respondError(w http.ResponseWriter, status int, code, message string, r *http.Request) {
|
|
137
|
+
respondJSON(w, status, map[string]interface{}{
|
|
138
|
+
"error": map[string]interface{}{
|
|
139
|
+
"code": code,
|
|
140
|
+
"message": message,
|
|
141
|
+
"requestId": requestID(r),
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 2.2 서비스 레이어 (인터페이스 + 트랜잭션)
|
|
148
|
+
|
|
149
|
+
```go
|
|
150
|
+
// service/order.go
|
|
151
|
+
|
|
152
|
+
// consumer 측 인터페이스 정의
|
|
153
|
+
type orderRepository interface {
|
|
154
|
+
Create(ctx context.Context, order *domain.Order) error
|
|
155
|
+
DecrementStock(ctx context.Context, productID string, qty int) error
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type OrderService struct {
|
|
159
|
+
repo orderRepository
|
|
160
|
+
db *sql.DB // 트랜잭션용
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
|
|
164
|
+
// TX: order 생성 + stock 차감 원자적 처리
|
|
165
|
+
tx, err := s.db.BeginTx(ctx, nil)
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil, fmt.Errorf("CreateOrder begin tx: %w", err)
|
|
168
|
+
}
|
|
169
|
+
defer tx.Rollback() // 성공 시 Commit 후 Rollback은 no-op
|
|
170
|
+
|
|
171
|
+
order := &domain.Order{
|
|
172
|
+
ID: uuid.New().String(),
|
|
173
|
+
UserID: req.UserID,
|
|
174
|
+
Status: "pending",
|
|
175
|
+
}
|
|
176
|
+
if err := s.repo.Create(ctx, order); err != nil {
|
|
177
|
+
return nil, fmt.Errorf("CreateOrder create: %w", err)
|
|
178
|
+
}
|
|
179
|
+
for _, item := range req.Items {
|
|
180
|
+
if err := s.repo.DecrementStock(ctx, item.ProductID, item.Quantity); err != nil {
|
|
181
|
+
return nil, fmt.Errorf("CreateOrder decrement stock %s: %w", item.ProductID, err)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if err := tx.Commit(); err != nil {
|
|
186
|
+
return nil, fmt.Errorf("CreateOrder commit: %w", err)
|
|
187
|
+
}
|
|
188
|
+
return order, nil
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 2.3 에러 모델
|
|
193
|
+
|
|
194
|
+
```go
|
|
195
|
+
// domain/errors.go
|
|
196
|
+
var (
|
|
197
|
+
ErrUserNotFound = errors.New("user not found")
|
|
198
|
+
ErrInsufficientStock = errors.New("insufficient stock")
|
|
199
|
+
ErrOrderNotFound = errors.New("order not found")
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// 상세 정보가 필요한 구조체 에러
|
|
203
|
+
type ValidationError struct {
|
|
204
|
+
Fields map[string]string
|
|
205
|
+
}
|
|
206
|
+
func (e *ValidationError) Error() string {
|
|
207
|
+
return fmt.Sprintf("validation failed: %v", e.Fields)
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 2.4 관찰가능성
|
|
212
|
+
|
|
213
|
+
```go
|
|
214
|
+
import (
|
|
215
|
+
"go.opentelemetry.io/otel"
|
|
216
|
+
"go.opentelemetry.io/otel/attribute"
|
|
217
|
+
"go.opentelemetry.io/otel/codes"
|
|
218
|
+
"log/slog"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
var tracer = otel.Tracer("order-service")
|
|
222
|
+
|
|
223
|
+
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
|
|
224
|
+
ctx, span := tracer.Start(ctx, "OrderService.CreateOrder")
|
|
225
|
+
defer span.End()
|
|
226
|
+
|
|
227
|
+
span.SetAttributes(
|
|
228
|
+
attribute.String("order.userId", req.UserID),
|
|
229
|
+
attribute.Int("order.itemCount", len(req.Items)),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
order, err := s.createInternal(ctx, req)
|
|
233
|
+
if err != nil {
|
|
234
|
+
span.RecordError(err)
|
|
235
|
+
span.SetStatus(codes.Error, err.Error())
|
|
236
|
+
return nil, err
|
|
237
|
+
}
|
|
238
|
+
slog.InfoContext(ctx, "order created",
|
|
239
|
+
slog.String("orderId", order.ID),
|
|
240
|
+
slog.String("userId", order.UserID),
|
|
241
|
+
)
|
|
242
|
+
return order, nil
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 3. 출력 형식
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
## 완료 보고
|
|
250
|
+
- 체크리스트: N/N ✅
|
|
251
|
+
- 매핑표: 모든 행 테스트 green
|
|
252
|
+
- go test -race: PASS
|
|
253
|
+
- golangci-lint: 0 issues
|
|
254
|
+
- 변경 파일: <목록>
|
|
255
|
+
- 의사결정: <패키지 구조 1-2줄>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 4. 관련 문서
|
|
259
|
+
|
|
260
|
+
- 원칙: [`principles/common.md`](../../../principles/common.md), [`principles/go.md`](../../../principles/go.md)
|
|
261
|
+
- 리뷰: [`skills/go/be-review/SKILL.md`](../be-review/SKILL.md)
|
|
262
|
+
- 성능: [`skills/go/be-perf/SKILL.md`](../be-perf/SKILL.md)
|