@wooojin/forgen 0.4.8 → 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/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/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -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/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- 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/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- 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 +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: be-perf-node
|
|
3
|
+
description: Node.js 서비스의 p95/p99 성능 문제를 진단하고 수정. DB N+1, Event Loop 차단, GC 압박, 동기 차단, lock contention, network roundtrip 카테고리별 절차로 접근한다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# be-perf (Node.js)
|
|
7
|
+
|
|
8
|
+
> **호출 시점**: "p99가 800ms야 잡아줘", "메모리가 계속 올라가", "DB 쿼리가 느려졌어", "CPU가 치솟아".
|
|
9
|
+
> **선행 로딩**: `principles/common.md` + `principles/node.md` 필수.
|
|
10
|
+
|
|
11
|
+
## 0. 절대 금지
|
|
12
|
+
|
|
13
|
+
1. 측정 없이 최적화 추측 금지 — "아마 이게 느릴 것 같아"는 근거 없음.
|
|
14
|
+
2. p50만 보고 OK 선언 금지 — p95/p99 반드시 확인.
|
|
15
|
+
3. 프로파일링 없이 `async/await` → callback 전환 금지 (성능 개선 미미, 가독성 손실 큼).
|
|
16
|
+
|
|
17
|
+
## 1. 진단 절차
|
|
18
|
+
|
|
19
|
+
### Step 1 — 현재 지표 수집
|
|
20
|
+
|
|
21
|
+
먼저 수치를 확인하라. 추측으로 시작하지 마라.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# APM 또는 Prometheus 메트릭
|
|
25
|
+
# http_request_duration_seconds{quantile="0.95"} — p95
|
|
26
|
+
# http_request_duration_seconds{quantile="0.99"} — p99
|
|
27
|
+
|
|
28
|
+
# 로드 테스트로 현재 기준선 측정
|
|
29
|
+
npx autocannon -c 100 -d 30 http://localhost:3000/api/orders
|
|
30
|
+
# Requests/sec, Latency 분포, Errors 확인
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Step 2 — 병목 카테고리 분류
|
|
34
|
+
|
|
35
|
+
| 증상 | 가능한 카테고리 |
|
|
36
|
+
|------|-----------------|
|
|
37
|
+
| DB 쿼리가 느림 (APM에서 확인) | N+1, 인덱스 누락, 슬로우 쿼리 |
|
|
38
|
+
| CPU 사용률 치솟음 | Event Loop 차단, GC 압박, 동기 연산 |
|
|
39
|
+
| 메모리 지속 증가 | 메모리 누수, GC 부족 |
|
|
40
|
+
| 특정 엔드포인트만 느림 | 해당 경로 분석 (외부 API, 직렬화) |
|
|
41
|
+
| 모든 요청 느림 | Event Loop 차단, 연결 풀 소진 |
|
|
42
|
+
|
|
43
|
+
### Step 3 — 카테고리별 진단 실행
|
|
44
|
+
|
|
45
|
+
## 2. 카테고리별 진단 및 픽스
|
|
46
|
+
|
|
47
|
+
### 2.1 DB N+1
|
|
48
|
+
|
|
49
|
+
**탐지**: APM에서 단일 요청에 DB 쿼리 N+1개 발생. 로그에서 반복 쿼리 패턴.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Prisma 탐지: queryRawUnsafe 이벤트 로깅
|
|
53
|
+
prisma.$on('query', (e) => {
|
|
54
|
+
if (process.env.NODE_ENV === 'development') {
|
|
55
|
+
logger.debug({ query: e.query, duration: e.duration }, 'DB query');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**픽스**:
|
|
61
|
+
```typescript
|
|
62
|
+
// WRONG: N+1
|
|
63
|
+
const orders = await prisma.order.findMany();
|
|
64
|
+
for (const order of orders) {
|
|
65
|
+
order.user = await prisma.user.findUnique({ where: { id: order.userId } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// RIGHT: include로 JOIN
|
|
69
|
+
const orders = await prisma.order.findMany({
|
|
70
|
+
include: { user: { select: { id: true, name: true } } }, // 필요한 필드만
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 대용량: DataLoader 패턴
|
|
74
|
+
import DataLoader from 'dataloader';
|
|
75
|
+
const userLoader = new DataLoader<string, User>(async (ids) => {
|
|
76
|
+
const users = await prisma.user.findMany({ where: { id: { in: [...ids] } } });
|
|
77
|
+
return ids.map(id => users.find(u => u.id === id) ?? null);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2.2 Event Loop 차단
|
|
82
|
+
|
|
83
|
+
**탐지**: `--inspect` 플래그로 Node.js 프로파일러 실행.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node --inspect --prof app.js # V8 프로파일 생성
|
|
87
|
+
node --prof-process isolate-*.log > profile.txt # 분석
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
또는 clinic.js (권장):
|
|
91
|
+
```bash
|
|
92
|
+
npx clinic doctor -- node app.js
|
|
93
|
+
npx clinic flame -- node app.js # Flamegraph
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**픽스**:
|
|
97
|
+
```typescript
|
|
98
|
+
// 동기 블로킹 → 비동기
|
|
99
|
+
// WRONG
|
|
100
|
+
import { readFileSync } from 'node:fs';
|
|
101
|
+
app.get('/config', (req, res) => {
|
|
102
|
+
const config = readFileSync('config.json', 'utf-8'); // 차단!
|
|
103
|
+
res.json(JSON.parse(config));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// RIGHT
|
|
107
|
+
import { readFile } from 'node:fs/promises';
|
|
108
|
+
app.get('/config', async (req, res) => {
|
|
109
|
+
const config = await readFile('config.json', 'utf-8');
|
|
110
|
+
res.json(JSON.parse(config));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// CPU 집약 작업 → worker_threads
|
|
114
|
+
import { Worker } from 'node:worker_threads';
|
|
115
|
+
function runInWorker(data: unknown): Promise<unknown> {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const worker = new Worker('./workers/compute.js', { workerData: data });
|
|
118
|
+
worker.on('message', resolve);
|
|
119
|
+
worker.on('error', reject);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2.3 GC 압박 (메모리)
|
|
125
|
+
|
|
126
|
+
**탐지**:
|
|
127
|
+
```bash
|
|
128
|
+
# heapdump 분석
|
|
129
|
+
node --expose-gc app.js
|
|
130
|
+
# 또는
|
|
131
|
+
npx clinic heapprofiler -- node app.js
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// 메모리 사용량 모니터링
|
|
136
|
+
setInterval(() => {
|
|
137
|
+
const mem = process.memoryUsage();
|
|
138
|
+
logger.info({
|
|
139
|
+
heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB',
|
|
140
|
+
heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + 'MB',
|
|
141
|
+
rss: Math.round(mem.rss / 1024 / 1024) + 'MB',
|
|
142
|
+
}, 'Memory usage');
|
|
143
|
+
}, 30_000);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**흔한 누수 패턴**:
|
|
147
|
+
```typescript
|
|
148
|
+
// WRONG: EventEmitter 리스너 누수
|
|
149
|
+
app.on('request', handler); // 제거 없음
|
|
150
|
+
|
|
151
|
+
// RIGHT: 명시적 제거
|
|
152
|
+
const handler = () => { ... };
|
|
153
|
+
app.on('request', handler);
|
|
154
|
+
// ... 정리 시
|
|
155
|
+
app.off('request', handler);
|
|
156
|
+
|
|
157
|
+
// WRONG: 클로저가 큰 객체 참조
|
|
158
|
+
function processRequest(req: Request) {
|
|
159
|
+
const hugeBuffer = Buffer.alloc(1024 * 1024 * 100); // 100MB
|
|
160
|
+
return async () => {
|
|
161
|
+
// hugeBuffer가 클로저에 갇혀 GC 불가
|
|
162
|
+
return hugeBuffer.length;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 2.4 외부 API 지연 (Network Roundtrip)
|
|
168
|
+
|
|
169
|
+
**탐지**: OpenTelemetry span에서 external call duration 확인.
|
|
170
|
+
|
|
171
|
+
**픽스**:
|
|
172
|
+
```typescript
|
|
173
|
+
// 병렬화 — 독립적인 외부 호출은 동시 실행
|
|
174
|
+
const [user, inventory] = await Promise.all([
|
|
175
|
+
userApiClient.getUser(userId),
|
|
176
|
+
inventoryApiClient.getStock(productId),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// 타임아웃 설정 (기본값 없음에 의존 금지)
|
|
180
|
+
const response = await fetch(url, {
|
|
181
|
+
signal: AbortSignal.timeout(3000), // 3초 타임아웃
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 서킷 브레이커 (opossum)
|
|
185
|
+
import CircuitBreaker from 'opossum';
|
|
186
|
+
const breaker = new CircuitBreaker(externalApiCall, {
|
|
187
|
+
timeout: 3000,
|
|
188
|
+
errorThresholdPercentage: 50,
|
|
189
|
+
resetTimeout: 30000,
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 2.5 DB 연결 풀 소진
|
|
194
|
+
|
|
195
|
+
**탐지**: DB 응답 느림 + 연결 대기 큐 증가.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Prisma 연결 풀 설정
|
|
199
|
+
const prisma = new PrismaClient({
|
|
200
|
+
datasources: {
|
|
201
|
+
db: {
|
|
202
|
+
url: `${DATABASE_URL}?connection_limit=10&pool_timeout=10`,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// pg (node-postgres) 연결 풀
|
|
208
|
+
import { Pool } from 'pg';
|
|
209
|
+
const pool = new Pool({
|
|
210
|
+
max: 10, // 최대 연결 수
|
|
211
|
+
idleTimeoutMillis: 30000,
|
|
212
|
+
connectionTimeoutMillis: 2000,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 연결 풀 모니터링
|
|
216
|
+
setInterval(() => {
|
|
217
|
+
logger.info({
|
|
218
|
+
total: pool.totalCount,
|
|
219
|
+
idle: pool.idleCount,
|
|
220
|
+
waiting: pool.waitingCount,
|
|
221
|
+
}, 'Connection pool stats');
|
|
222
|
+
}, 10_000);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 2.6 직렬화 비용 (대용량 JSON)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// WRONG: 대용량 객체 통째로 JSON.stringify
|
|
229
|
+
res.json(await db.findManyWithAllFields());
|
|
230
|
+
|
|
231
|
+
// RIGHT: 필요한 필드만 선택 (projection)
|
|
232
|
+
const orders = await prisma.order.findMany({
|
|
233
|
+
select: { id: true, status: true, createdAt: true }, // 필요한 것만
|
|
234
|
+
take: 20, // 페이지네이션
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 대용량 응답: 스트리밍
|
|
238
|
+
import { pipeline } from 'node:stream/promises';
|
|
239
|
+
import { Readable } from 'node:stream';
|
|
240
|
+
app.get('/export', async (req, res) => {
|
|
241
|
+
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
242
|
+
const cursor = db.findManyCursor(); // DB 커서
|
|
243
|
+
await pipeline(cursor, res);
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## 3. 출력 형식
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
## 성능 진단 결과
|
|
251
|
+
|
|
252
|
+
### 측정 기준선
|
|
253
|
+
- p50: Xms / p95: Xms / p99: Xms
|
|
254
|
+
- 목표 SLO: p95 < Xms (docs/slo.md)
|
|
255
|
+
|
|
256
|
+
### 발견된 병목
|
|
257
|
+
1. [카테고리] file:line — 설명 (예상 개선: X%)
|
|
258
|
+
2. ...
|
|
259
|
+
|
|
260
|
+
### 적용한 픽스
|
|
261
|
+
- 변경 파일: <목록>
|
|
262
|
+
- 재측정 결과: p95 Xms → Xms
|
|
263
|
+
|
|
264
|
+
### 추가 권고 (이번 PR 범위 외)
|
|
265
|
+
- ...
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## 4. 관련 문서
|
|
269
|
+
|
|
270
|
+
- 원칙: [`principles/common.md`](../../../principles/common.md) F섹션 (Performance Baseline)
|
|
271
|
+
- 리뷰: [`skills/node/be-review/SKILL.md`](../be-review/SKILL.md)
|
|
272
|
+
- 코퍼스: `sources/node-runtime/`, `sources/postgres/`
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: be-review-node
|
|
3
|
+
description: Node.js/TypeScript PR을 사내 BE 원칙 기준으로 리뷰. [SEVERITY] file:line — 이슈 형식으로 출력하고, 머지 차단/비차단을 명확히 구분한다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# be-review (Node.js)
|
|
7
|
+
|
|
8
|
+
> **호출 시점**: "이 PR 리뷰해줘", "이 코드 리뷰해줘", "OWASP 관점에서 검토해줘".
|
|
9
|
+
> **선행 로딩**: `principles/common.md` + `principles/node.md` 필수.
|
|
10
|
+
|
|
11
|
+
## 0. 절대 금지
|
|
12
|
+
|
|
13
|
+
1. 이슈 없이 "좋아 보입니다" 완료 선언 금지 — 모든 체크리스트 항목 확인.
|
|
14
|
+
2. 주관적 스타일 의견을 [HIGH]로 분류 금지.
|
|
15
|
+
3. 자동 수정 가능한 포매팅 이슈를 리뷰에 포함 금지 (eslint --fix 로 처리).
|
|
16
|
+
|
|
17
|
+
## 1. 리뷰 출력 형식
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
## 리뷰 요약
|
|
21
|
+
- 변경: N files +X -Y
|
|
22
|
+
- HIGH N, MED N, LOW N / 머지 [차단|비차단]
|
|
23
|
+
|
|
24
|
+
[HIGH] src/orders/order.controller.ts:42 — SQL 문자열 concat, injection 위험
|
|
25
|
+
[HIGH] src/orders/order.service.ts:88 — 빈 catch 블록, 에러 묵살
|
|
26
|
+
[MED] src/orders/order.service.ts:120 — optional couponCode를 required 취급 (명세 위반)
|
|
27
|
+
[LOW] src/orders/types.ts:15 — 매직 넘버 300, 상수 추출 권장
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### SEVERITY 기준
|
|
31
|
+
|
|
32
|
+
| SEVERITY | 정의 | 머지 |
|
|
33
|
+
|----------|------|------|
|
|
34
|
+
| **HIGH** | 보안 취약점, 데이터 손실 가능, 명세 위반, 에러 묵살, 프로세스 불안정 | 차단 |
|
|
35
|
+
| **MED** | 성능 저하(N+1), 관찰가능성 누락, 타입 안전성 위반, 에러 구조 불일치 | 권고 (팀 합의 시 통과) |
|
|
36
|
+
| **LOW** | 가독성, 매직 넘버, 주석, 함수 길이 | 비차단 |
|
|
37
|
+
|
|
38
|
+
## 2. 체크리스트
|
|
39
|
+
|
|
40
|
+
### 2.1 보안 (HIGH 기준)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
[ ] SQL/NoSQL injection — 문자열 concat 쿼리 없음
|
|
44
|
+
[ ] 인가 검증 — 모든 리소스 접근에 소유권 확인 (OWASP API1)
|
|
45
|
+
[ ] 하드코딩 시크릿 없음 — API key, password, token
|
|
46
|
+
[ ] 입력 검증 — 모든 external input에 Zod/joi 검증
|
|
47
|
+
[ ] 응답에 민감 필드 노출 없음 — password, hash, internal ID (OWASP API3)
|
|
48
|
+
[ ] CORS 설정 — `*` 미사용 (origin allowlist)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2.2 에러 모델 (HIGH/MED)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
[ ] 빈 catch 블록 없음 — 최소 logger.error + re-throw
|
|
55
|
+
[ ] HTTP 200 + { success: false } 패턴 없음 — 4xx/5xx 적절히 사용
|
|
56
|
+
[ ] 에러 응답 구조 일관성 — { error: { code, message, requestId } }
|
|
57
|
+
[ ] 4xx와 5xx 경계 올바름 — 클라이언트 오류 vs 서버 오류 혼동 없음
|
|
58
|
+
[ ] unhandledRejection 핸들러 존재
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2.3 Node.js 특화 (MED/HIGH)
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
[ ] async/await 일관성 — callback 혼용 없음
|
|
65
|
+
[ ] Event Loop 차단 없음 — 동기 I/O (readFileSync 등) 핸들러 내 미사용
|
|
66
|
+
[ ] optional 필드를 required로 취급하지 않음 (명세 일치)
|
|
67
|
+
[ ] TypeScript strict 통과 — any 타입 남용 없음
|
|
68
|
+
[ ] 런타임 검증 — 외부 데이터에 as T 단언 미사용
|
|
69
|
+
[ ] process.exit() 직접 호출 없음 (graceful shutdown 우회)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2.4 성능 (MED)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
[ ] N+1 쿼리 없음 — 루프 내 DB 쿼리 패턴
|
|
76
|
+
[ ] 트랜잭션 내 외부 API 호출 없음
|
|
77
|
+
[ ] Promise.all — 독립 async 작업 병렬화
|
|
78
|
+
[ ] 인덱스 없는 WHERE 조건 없음 (신규 쿼리)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2.5 관찰가능성 (MED)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[ ] 구조화 로그 (JSON) — console.log 미사용
|
|
85
|
+
[ ] 에러 로그에 requestId 포함
|
|
86
|
+
[ ] 중요 비즈니스 이벤트 로그 존재 (주문 생성, 결제 완료 등)
|
|
87
|
+
[ ] 민감 정보 로그 미포함 (password, 카드번호, PII)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2.6 코드 품질 (LOW)
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
[ ] 함수 50줄 이하
|
|
94
|
+
[ ] 중첩 깊이 4 이하 (early return 활용)
|
|
95
|
+
[ ] 매직 넘버 상수화
|
|
96
|
+
[ ] 같은 파일 3+회 수정 시 전체 재설계 검토
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 3. 이슈 카탈로그 (즉시 참조)
|
|
100
|
+
|
|
101
|
+
| 패턴 | SEVERITY | 설명 |
|
|
102
|
+
|------|----------|------|
|
|
103
|
+
| `db.query("... WHERE id = " + id)` | HIGH | SQL injection |
|
|
104
|
+
| `catch (err) {}` | HIGH | 에러 묵살 |
|
|
105
|
+
| `res.status(200).json({ success: false })` | HIGH | 에러 응답 오용 |
|
|
106
|
+
| `const user = cache.get() as User` | MED | 런타임 검증 없는 단언 |
|
|
107
|
+
| `for (const x of list) { await db.findBy... }` | MED | N+1 쿼리 |
|
|
108
|
+
| `await externalApi()` inside transaction | MED | 트랜잭션 내 외부 호출 |
|
|
109
|
+
| `console.log(req.body)` | MED | 구조화 로그 미사용 + PII 노출 위험 |
|
|
110
|
+
| `if (req.params.userId !== req.user.id)` 누락 | HIGH | OWASP API1 (소유권 검증) |
|
|
111
|
+
| `res.json(await db.findUser(id))` (전체 모델) | MED | OWASP API3 (필드 노출) |
|
|
112
|
+
| `fs.readFileSync(...)` in handler | MED | Event loop 차단 |
|
|
113
|
+
| 함수 > 50줄 | LOW | 책임별 분리 |
|
|
114
|
+
|
|
115
|
+
## 4. 관련 문서
|
|
116
|
+
|
|
117
|
+
- 원칙: [`principles/common.md`](../../../principles/common.md), [`principles/node.md`](../../../principles/node.md)
|
|
118
|
+
- 보안 체크리스트: [`skills/node/be-security/SKILL.md`](../be-security/SKILL.md)
|