refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게 [공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류. 문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인). - Topic_Programming → Domain_Programming (내부 구조 보존) - Topic_Graphic → Domain_Design - Topic_Business → Domain_Product - Topic_General → Domain_General - _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning), Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing) - 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서) - 빈 폴더 정리 (memory/procedures) - 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+167
@@ -0,0 +1,167 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-25-skybound-player-airfr
|
||||
title: Skybound — Player Airframe and 8-Stage Boss Continuity Rework
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Skybound Airframe, 8-Stage Boss]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, player-design, boss-design, progression]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Player Airframe and 8-Stage Boss Continuity Rework
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 8-stage progression 의 continuity 는 boss roster 의 thematic escalation 과 player airframe 의 unlock pacing 의 product"**. 매 Skybound 는 aerial-themed VSL — player 의 airframe (jet/biplane/UFO 등) 이 stage clear 마다 unlock, boss 는 stage 별 thematic escalation (small drone → carrier → kraken-airship 등). 매 4-25 작업은 8 stage 의 boss list 와 airframe unlock 의 mapping 을 reconcile.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Airframe System
|
||||
- 매 airframe 은 base stat (HP/speed/pickup radius) + signature passive 의 set.
|
||||
- 매 stage clear 마다 1 unlock — total 8 airframes.
|
||||
- 매 in-run 에서 airframe 변경 의 X — meta layer.
|
||||
|
||||
### 매 8-Stage Boss Roster
|
||||
1. **Stage 1**: Recon Drone — tutorial, predictable strafe.
|
||||
2. **Stage 2**: Patrol Squadron — multi-target intro.
|
||||
3. **Stage 3**: Heavy Bomber — bullet hell intro.
|
||||
4. **Stage 4**: Lightning Wraith — speed test.
|
||||
5. **Stage 5**: Stormcaller (miniboss x2) — phase mechanic intro.
|
||||
6. **Stage 6**: Sky Carrier — minion wave + main body.
|
||||
7. **Stage 7**: Tempest Knight — mirror-match style.
|
||||
8. **Stage 8**: Voidwhale — final, all mechanics combined.
|
||||
|
||||
### 매 Continuity Rework
|
||||
- 매 stage 8 의 final boss 가 stage 1 enemy 의 evolved form 의 reveal — narrative thread.
|
||||
- 매 airframe 8 (unlock at stage 8 clear) = stage 8 boss 의 "stolen" frame — meta loop.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Airframe Definition
|
||||
```typescript
|
||||
type Airframe = {
|
||||
id: string;
|
||||
name: string;
|
||||
baseHp: number;
|
||||
baseSpeed: number;
|
||||
pickupRadius: number;
|
||||
signature: Passive;
|
||||
unlockStage: number;
|
||||
};
|
||||
|
||||
const AIRFRAMES: Airframe[] = [
|
||||
{ id: 'biplane', name: 'Biplane', baseHp: 100, baseSpeed: 200, pickupRadius: 60, signature: { kind: 'crit', value: 0.1 }, unlockStage: 0 },
|
||||
{ id: 'jet', name: 'Jet', baseHp: 80, baseSpeed: 280, pickupRadius: 50, signature: { kind: 'cooldown', value: -0.15 }, unlockStage: 1 },
|
||||
// ...
|
||||
{ id: 'voidframe', name: 'Voidframe', baseHp: 200, baseSpeed: 240, pickupRadius: 100, signature: { kind: 'lifesteal', value: 0.05 }, unlockStage: 8 },
|
||||
];
|
||||
```
|
||||
|
||||
### Boss Phase State Machine
|
||||
```typescript
|
||||
type BossPhase = 'intro' | 'p1' | 'transition' | 'p2' | 'enrage' | 'death';
|
||||
|
||||
class BossController {
|
||||
phase: BossPhase = 'intro';
|
||||
|
||||
update(dt: number, hpRatio: number) {
|
||||
switch (this.phase) {
|
||||
case 'p1':
|
||||
if (hpRatio < 0.66) this.transitionTo('transition');
|
||||
break;
|
||||
case 'p2':
|
||||
if (hpRatio < 0.2) this.transitionTo('enrage');
|
||||
break;
|
||||
}
|
||||
this.runPattern(this.phase, dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unlock Persistence
|
||||
```typescript
|
||||
interface MetaSave {
|
||||
airframesUnlocked: string[];
|
||||
highestStageCleared: number;
|
||||
bossDefeats: Record<string, number>;
|
||||
}
|
||||
|
||||
function onStageClear(stageId: number, save: MetaSave): void {
|
||||
if (stageId > save.highestStageCleared) {
|
||||
const newFrame = AIRFRAMES.find(a => a.unlockStage === stageId);
|
||||
if (newFrame) save.airframesUnlocked.push(newFrame.id);
|
||||
save.highestStageCleared = stageId;
|
||||
persist(save);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Boss Continuity Reveal
|
||||
```typescript
|
||||
class VoidwhaleScene extends Phaser.Scene {
|
||||
showContinuityCutscene() {
|
||||
const boss = this.add.sprite(W/2, H/2, 'voidwhale');
|
||||
this.tweens.add({
|
||||
targets: boss, alpha: 0.3, duration: 1500,
|
||||
onComplete: () => {
|
||||
// morph reveal: stage 1 recon drone evolved
|
||||
boss.setTexture('recon-drone-evolved');
|
||||
this.add.text(W/2, H*0.8, '"It was watching from the start."', { fontSize: '32px' }).setOrigin(0.5);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Signature Passive Application
|
||||
```typescript
|
||||
function applySignature(player: Player, frame: Airframe): void {
|
||||
switch (frame.signature.kind) {
|
||||
case 'crit': player.stats.critChance += frame.signature.value; break;
|
||||
case 'cooldown': player.stats.cooldownMul *= (1 + frame.signature.value); break;
|
||||
case 'lifesteal': player.stats.lifesteal += frame.signature.value; break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Roster size > 8 | Tier groups (early/mid/late) — UI scalability |
|
||||
| Boss too repetitive | Phase mechanic 차별화 (mirror, summon, terrain) |
|
||||
| Unlock pacing too slow | Optional secondary unlock condition (achievement) |
|
||||
| Continuity hard to read | Cutscene + audio motif callback |
|
||||
|
||||
**기본값**: 매 8-frame roster, 1-frame-per-stage unlock, final boss = continuity reveal.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Boss-Design]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: roster brainstorm, narrative continuity ideation, balance spreadsheet generation.
|
||||
**언제 X**: animation timing, hitbox tuning (manual playtesting 필요).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Bosses identical mechanics**: mirror/summon/terrain 의 lack — flat fight feel.
|
||||
- **Unlock at first 2 stages all**: pacing 망 — late-stage motivation 의 X.
|
||||
- **Continuity reveal text-only**: weak impact — visual morph + audio motif 의 사용.
|
||||
- **Airframe stat 차이만**: signature passive 없음 — identity collapse.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Hades roster design, Vampire Survivors character roster).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — airframe + 8-stage boss continuity 정리 |
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-25-skybound-skill-concep
|
||||
title: Skybound — Skill Concept and Hangar Layout Overlap Fix
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Skybound Hangar, Skill Tree Layout]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, ui-layout, skill-tree, hangar]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Skill Concept and Hangar Layout Overlap Fix
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 hangar UI 의 layout overlap 은 grid alignment + responsive scale 의 부재 의 symptom"**. 매 Skybound 의 hangar (meta-progression hub) 에서 airframe selector + skill tree + currency display 의 z-overlap 발생. 매 작업은 layout grid 재설계 + skill 의 conceptual taxonomy (active/passive/mod) 정립.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Skill 3 Taxonomy
|
||||
- **Active**: cooldown-based, player-triggered (rare in VSL — only "ultimate" 등).
|
||||
- **Passive**: always-on stat modifier (crit chance, hp regen 등).
|
||||
- **Mod**: weapon-specific behavior change (split shot, pierce 등).
|
||||
|
||||
### 매 Hangar Layout 8-Region
|
||||
1. Top-bar: currency + run timer.
|
||||
2. Left rail: airframe carousel.
|
||||
3. Center: selected airframe preview (3D-ish parallax).
|
||||
4. Right panel: skill tree.
|
||||
5. Bottom-bar: load-out summary + start button.
|
||||
6. Modal layer: tooltip / detail.
|
||||
7. Toast layer: notifications.
|
||||
8. Background: parallax sky.
|
||||
|
||||
### 매 Overlap Fix Pattern
|
||||
- 매 region 을 named slot 으로 declarative 의 정의.
|
||||
- 매 child element 의 absolute positioning 의 X — slot-relative anchor.
|
||||
- 매 z-index 의 layer 별 100 단위 의 spacing.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Slot-Based Layout
|
||||
```typescript
|
||||
type Slot = { x: number; y: number; w: number; h: number; z: number };
|
||||
|
||||
const HANGAR_SLOTS: Record<string, Slot> = {
|
||||
topBar: { x: 0, y: 0, w: 1920, h: 80, z: 100 },
|
||||
leftRail: { x: 0, y: 80, w: 240, h: 880, z: 100 },
|
||||
center: { x: 240, y: 80, w: 1200, h: 800, z: 50 },
|
||||
rightPanel: { x: 1440, y: 80, w: 480, h: 880, z: 100 },
|
||||
bottomBar: { x: 240, y: 880, w: 1200, h: 200, z: 100 },
|
||||
modal: { x: 0, y: 0, w: 1920, h: 1080, z: 500 },
|
||||
toast: { x: 1620, y: 100, w: 280, h: 800, z: 600 },
|
||||
};
|
||||
|
||||
function placeIn(slot: Slot, child: Phaser.GameObjects.Container, anchor: 'tl' | 'center' = 'tl') {
|
||||
if (anchor === 'center') {
|
||||
child.setPosition(slot.x + slot.w/2, slot.y + slot.h/2);
|
||||
} else {
|
||||
child.setPosition(slot.x, slot.y);
|
||||
}
|
||||
child.setDepth(slot.z);
|
||||
}
|
||||
```
|
||||
|
||||
### Skill Definition
|
||||
```typescript
|
||||
type Skill =
|
||||
| { id: string; kind: 'active'; cooldownSec: number; effect: ActiveEffect }
|
||||
| { id: string; kind: 'passive'; modifiers: StatModifier[] }
|
||||
| { id: string; kind: 'mod'; targetWeapon: string; transform: WeaponTransform };
|
||||
|
||||
const SKILLS: Skill[] = [
|
||||
{ id: 'overdrive', kind: 'active', cooldownSec: 60, effect: { kind: 'damage-boost', mul: 2, durationSec: 8 } },
|
||||
{ id: 'iron-skin', kind: 'passive', modifiers: [{ stat: 'hp', kind: 'mul', value: 1.2 }] },
|
||||
{ id: 'split-shot', kind: 'mod', targetWeapon: 'cannon', transform: { kind: 'multishot', count: 3, spreadDeg: 15 } },
|
||||
];
|
||||
```
|
||||
|
||||
### Skill Tree Render (overlap-free)
|
||||
```typescript
|
||||
class SkillTreeView extends Phaser.GameObjects.Container {
|
||||
constructor(scene: Phaser.Scene, slot: Slot, nodes: SkillNode[]) {
|
||||
super(scene);
|
||||
placeIn(slot, this);
|
||||
|
||||
const cellW = slot.w / 4;
|
||||
const cellH = 80;
|
||||
nodes.forEach((node, i) => {
|
||||
const col = i % 4;
|
||||
const row = Math.floor(i / 4);
|
||||
const sprite = scene.add.sprite(col * cellW + cellW/2, row * cellH + cellH/2, node.icon);
|
||||
this.add(sprite);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tooltip on Hover (modal layer)
|
||||
```typescript
|
||||
function showTooltip(scene: Phaser.Scene, x: number, y: number, skill: Skill) {
|
||||
const tt = scene.add.container(x + 16, y + 16);
|
||||
tt.setDepth(HANGAR_SLOTS.modal.z + 10);
|
||||
const bg = scene.add.rectangle(0, 0, 280, 120, 0x000000, 0.9).setOrigin(0);
|
||||
const text = scene.add.text(8, 8, formatSkill(skill), { fontSize: '14px', wordWrap: { width: 264 } });
|
||||
tt.add([bg, text]);
|
||||
return tt;
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive Rescale
|
||||
```typescript
|
||||
function rescaleForViewport(viewportW: number, viewportH: number) {
|
||||
const targetW = 1920, targetH = 1080;
|
||||
const scale = Math.min(viewportW / targetW, viewportH / targetH);
|
||||
scene.cameras.main.setZoom(scale);
|
||||
scene.cameras.main.centerOn(targetW/2, targetH/2);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Many small UI elements | Slot grid + named anchor |
|
||||
| Mobile + desktop | Responsive rescale (uniform zoom) |
|
||||
| Tooltip clipping | Modal layer with z+10 boost |
|
||||
| Skill tree large | Scrollable container in right panel |
|
||||
|
||||
**기본값**: 매 declarative slot grid + 100-unit z-tier + skill 3-kind union type.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Skill-Tree-Design]]
|
||||
- Adjacent: [[API Response & State Modeling|Discriminated-Unions]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: layout slot enumeration, skill taxonomy brainstorm, tooltip copy.
|
||||
**언제 X**: pixel-perfect alignment (manual eyeballing 필요).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Absolute pixel hardcode everywhere**: refactor 의 nightmare — slot indirection 의 사용.
|
||||
- **Z-index ad-hoc**: overlap bug 재발 — layer tier (100/200/500/600) 의 사용.
|
||||
- **Skill kind 분리 안함**: switch 폭발 — discriminated union 의 사용.
|
||||
- **Tooltip in same layer**: clip — modal layer 의 분리.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Phaser 3 docs, game UI postmortems).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — slot grid + skill taxonomy + overlap fix 정리 |
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-25-skybound-vampire-surv
|
||||
title: Skybound — Vampire Survivors Loop and Stage Curve Preparation
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Skybound VS Loop, Stage Curve Tuning]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, vampire-survivors, stage-curve, balance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Vampire Survivors Loop and Stage Curve Preparation
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 VS-style loop 의 핵심은 spawn pressure curve × pickup economy 의 monotonic escalation"**. 매 Skybound 의 4월 25일 작업은 stage curve, enemy spawn rate, XP gem drop ratio 의 baseline 을 8-stage progression 에 맞춰 재설계 한 작업. 매 Phaser 3 + TypeScript stack, vampire-survivors-like (VSL) genre.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 VSL Loop 의 4 Phase
|
||||
- **Phase 1 (0-2m)**: low density, single enemy type — player 의 control familiarization.
|
||||
- **Phase 2 (2-5m)**: pack spawns + first elite — pickup economy 시작.
|
||||
- **Phase 3 (5-8m)**: mixed waves + miniboss — build identity 확정.
|
||||
- **Phase 4 (8m+)**: screen-fill density + boss — power fantasy peak.
|
||||
|
||||
### 매 Stage Curve Variables
|
||||
- `enemyDensity(t)` — concurrent on-screen enemies.
|
||||
- `enemyHP(t)` — per-unit HP scaling.
|
||||
- `xpGemValue(t)` — pickup denomination growth.
|
||||
- `eliteRatio(t)` — chance of elite spawn per wave.
|
||||
|
||||
### 매 응용
|
||||
1. 매 stage 별 4 phase 의 timing 을 data table 로 외부화.
|
||||
2. 매 curve 의 monotonic 함수 (linear/exp/step) 를 designer 가 tweak.
|
||||
3. 매 phase transition 에 audio/visual cue (screen flash, BGM layer add).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Stage Curve Definition
|
||||
```typescript
|
||||
type StageCurve = {
|
||||
stageId: number;
|
||||
durationSec: number;
|
||||
phases: Phase[];
|
||||
};
|
||||
|
||||
type Phase = {
|
||||
startSec: number;
|
||||
endSec: number;
|
||||
enemyDensity: (t: number) => number;
|
||||
enemyHpMul: (t: number) => number;
|
||||
xpGemValue: number;
|
||||
eliteRatio: number;
|
||||
};
|
||||
|
||||
const STAGE_1: StageCurve = {
|
||||
stageId: 1,
|
||||
durationSec: 600,
|
||||
phases: [
|
||||
{ startSec: 0, endSec: 120, enemyDensity: (t) => 5 + t * 0.05, enemyHpMul: () => 1, xpGemValue: 1, eliteRatio: 0 },
|
||||
{ startSec: 120, endSec: 300, enemyDensity: (t) => 15 + t * 0.1, enemyHpMul: (t) => 1 + t * 0.002, xpGemValue: 2, eliteRatio: 0.05 },
|
||||
{ startSec: 300, endSec: 480, enemyDensity: (t) => 30 + t * 0.15, enemyHpMul: (t) => 1.5 + t * 0.003, xpGemValue: 5, eliteRatio: 0.15 },
|
||||
{ startSec: 480, endSec: 600, enemyDensity: () => 80, enemyHpMul: () => 3, xpGemValue: 10, eliteRatio: 0.3 },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Spawner Loop
|
||||
```typescript
|
||||
class StageSpawner {
|
||||
constructor(private curve: StageCurve, private scene: Phaser.Scene) {}
|
||||
|
||||
update(elapsedSec: number): void {
|
||||
const phase = this.curve.phases.find(p => elapsedSec >= p.startSec && elapsedSec < p.endSec);
|
||||
if (!phase) return;
|
||||
|
||||
const localT = elapsedSec - phase.startSec;
|
||||
const targetCount = phase.enemyDensity(localT);
|
||||
const current = this.scene.enemies.countActive();
|
||||
const deficit = Math.floor(targetCount - current);
|
||||
|
||||
for (let i = 0; i < deficit; i++) {
|
||||
const isElite = Math.random() < phase.eliteRatio;
|
||||
this.spawnEnemy({ hpMul: phase.enemyHpMul(localT), elite: isElite });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### XP Pickup Magnetism
|
||||
```typescript
|
||||
const PICKUP_RADIUS = 80;
|
||||
const MAGNET_SPEED = 400;
|
||||
|
||||
function updatePickups(player: Player, gems: Gem[], dt: number): void {
|
||||
for (const gem of gems) {
|
||||
const dx = player.x - gem.x;
|
||||
const dy = player.y - gem.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist < PICKUP_RADIUS || gem.attracted) {
|
||||
gem.attracted = true;
|
||||
const inv = 1 / Math.max(dist, 1);
|
||||
gem.x += dx * inv * MAGNET_SPEED * dt;
|
||||
gem.y += dy * inv * MAGNET_SPEED * dt;
|
||||
if (dist < 8) {
|
||||
player.gainXp(gem.value);
|
||||
gem.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Wave Director
|
||||
```typescript
|
||||
type Wave = { atSec: number; pattern: 'circle' | 'line' | 'pack' | 'elite'; count: number };
|
||||
|
||||
const WAVES_STAGE_1: Wave[] = [
|
||||
{ atSec: 30, pattern: 'pack', count: 8 },
|
||||
{ atSec: 90, pattern: 'circle', count: 12 },
|
||||
{ atSec: 180, pattern: 'line', count: 20 },
|
||||
{ atSec: 300, pattern: 'elite', count: 1 },
|
||||
];
|
||||
|
||||
class WaveDirector {
|
||||
private fired = new Set<number>();
|
||||
tick(elapsedSec: number, waves: Wave[]) {
|
||||
for (const w of waves) {
|
||||
if (elapsedSec >= w.atSec && !this.fired.has(w.atSec)) {
|
||||
this.fired.add(w.atSec);
|
||||
this.spawnWave(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase Transition Cue
|
||||
```typescript
|
||||
scene.events.on('phase:enter', (phase: Phase) => {
|
||||
scene.cameras.main.flash(200, 255, 255, 255, false);
|
||||
scene.bgm.addLayer(`tier-${phase.tier}`);
|
||||
scene.add.text(W/2, 100, `WAVE ${phase.tier}`, { fontSize: '48px' }).setOrigin(0.5);
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Solo dev, fast iteration | Data-table 외부화 + hot reload |
|
||||
| Difficulty too spiky | Smooth curve (linear), avoid step |
|
||||
| Pickup economy 초과 | Gem radius 축소 + value 인하 |
|
||||
| Phase boundary unclear | Visual+audio cue 강화 |
|
||||
|
||||
**기본값**: 매 4-phase per stage, monotonic exp curve, designer-tunable JSON.
|
||||
|
||||
## 🔗 Graph
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: stage curve tuning, balance spreadsheet 작성, spawn pattern enumeration.
|
||||
**언제 X**: per-frame perf-critical hot path, GPU shader logic.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Hardcoded curve in code**: tweak 마다 rebuild — JSON/CSV 외부화 의 X.
|
||||
- **Step function spike**: phase 전환 시 difficulty cliff — smooth curve 의 사용.
|
||||
- **Pickup vacuum 너무 큼**: player 무이동 → economy 붕괴.
|
||||
- **Elite ratio fixed**: phase progression 의 lost — ramp 의 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vampire Survivors postmortem, Brotato dev blog).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — VSL loop + stage curve baseline 정리 |
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-enemy-motion
|
||||
title: Skybound — Enemy Motion, Damage Pressure, and Projectile Visual Pass
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Enemy Steering, Projectile VFX]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, enemy-ai, projectile-vfx, pressure-curve]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Enemy Motion, Damage Pressure, and Projectile Visual Pass
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 VSL 에서 enemy motion + damage pressure + projectile readability 가 player 의 skill ceiling 의 결정 trio"**. 매 Skybound 의 4-26 pass 는 enemy steering (boid → predict-aim mix), damage tick interval, projectile telegraphing visual 의 동시 tuning.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Enemy Motion Modes
|
||||
- **Direct chase**: simple seek toward player.
|
||||
- **Boid**: cohesion + separation + alignment.
|
||||
- **Predict aim**: lead target by velocity * lookahead.
|
||||
- **Strafe + fire**: orbit at preferred range, fire projectiles.
|
||||
- **Charge**: telegraph → dash linear.
|
||||
|
||||
### 매 Damage Pressure Variables
|
||||
- **Tick interval**: 매 contact damage 의 cooldown (e.g. 0.5s) — too fast → unfair, too slow → trivial.
|
||||
- **Knockback strength**: high knockback → easier kiting.
|
||||
- **iframe duration**: post-hit invincibility (0.3-0.6s typical).
|
||||
|
||||
### 매 Projectile Visual Pass
|
||||
- 매 telegraph (warning ground decal/laser sight) 의 duration 의 0.3-1s.
|
||||
- 매 projectile body 의 color contrast (red/orange on blue background).
|
||||
- 매 trail particle 의 lifespan 짧음 (over-trail 의 vision clutter).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Steering Mix
|
||||
```typescript
|
||||
type SteeringWeights = { seek: number; cohesion: number; separation: number; align: number; };
|
||||
|
||||
function steer(enemy: Enemy, neighbors: Enemy[], target: Vec2, w: SteeringWeights): Vec2 {
|
||||
const seek = sub(target, enemy.pos).normalize();
|
||||
const cohesion = avgPos(neighbors).sub(enemy.pos).normalize();
|
||||
const sep = neighbors.reduce((acc, n) => {
|
||||
const d = enemy.pos.distTo(n.pos);
|
||||
if (d < 40 && d > 0) return acc.add(sub(enemy.pos, n.pos).scale(1/d));
|
||||
return acc;
|
||||
}, vec(0,0)).normalize();
|
||||
const align = avgVel(neighbors).normalize();
|
||||
|
||||
return seek.scale(w.seek)
|
||||
.add(cohesion.scale(w.cohesion))
|
||||
.add(sep.scale(w.separation))
|
||||
.add(align.scale(w.align))
|
||||
.normalize();
|
||||
}
|
||||
```
|
||||
|
||||
### Predict-Aim Projectile
|
||||
```typescript
|
||||
function predictAim(shooterPos: Vec2, target: { pos: Vec2; vel: Vec2 }, projSpeed: number): Vec2 {
|
||||
const toTarget = sub(target.pos, shooterPos);
|
||||
const a = target.vel.dot(target.vel) - projSpeed * projSpeed;
|
||||
const b = 2 * toTarget.dot(target.vel);
|
||||
const c = toTarget.dot(toTarget);
|
||||
const disc = b*b - 4*a*c;
|
||||
if (disc < 0) return target.pos;
|
||||
const t = (-b - Math.sqrt(disc)) / (2*a);
|
||||
return target.pos.clone().add(target.vel.clone().scale(Math.max(t, 0)));
|
||||
}
|
||||
```
|
||||
|
||||
### Damage Tick + iframe
|
||||
```typescript
|
||||
class Damageable {
|
||||
hp: number;
|
||||
iframeUntil = 0;
|
||||
|
||||
takeDamage(amount: number, now: number, iframeSec = 0.4): boolean {
|
||||
if (now < this.iframeUntil) return false;
|
||||
this.hp -= amount;
|
||||
this.iframeUntil = now + iframeSec;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ContactDamager {
|
||||
lastTickAt = new Map<string, number>();
|
||||
intervalSec: number;
|
||||
|
||||
tryDamage(targetId: string, target: Damageable, amount: number, now: number) {
|
||||
const last = this.lastTickAt.get(targetId) ?? -Infinity;
|
||||
if (now - last < this.intervalSec) return;
|
||||
if (target.takeDamage(amount, now)) this.lastTickAt.set(targetId, now);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Projectile Telegraph
|
||||
```typescript
|
||||
function telegraphLaser(scene, fromX: number, fromY: number, dirRad: number, lengthPx: number, durationMs = 600): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const g = scene.add.graphics();
|
||||
g.lineStyle(2, 0xff3333, 0.6);
|
||||
g.beginPath();
|
||||
g.moveTo(fromX, fromY);
|
||||
g.lineTo(fromX + Math.cos(dirRad) * lengthPx, fromY + Math.sin(dirRad) * lengthPx);
|
||||
g.strokePath();
|
||||
scene.tweens.add({
|
||||
targets: g, alpha: 0, duration: durationMs,
|
||||
onComplete: () => { g.destroy(); resolve(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function fireTelegraphedLaser(scene, from, dir, len) {
|
||||
await telegraphLaser(scene, from.x, from.y, dir, len, 600);
|
||||
spawnLaserHitbox(scene, from, dir, len);
|
||||
}
|
||||
```
|
||||
|
||||
### Projectile Visual (high contrast)
|
||||
```typescript
|
||||
function makeProjectile(scene, pos, vel) {
|
||||
const proj = scene.physics.add.sprite(pos.x, pos.y, 'proj-core');
|
||||
proj.setTint(0xffaa33); // warm color on cool background
|
||||
proj.setVelocity(vel.x, vel.y);
|
||||
scene.add.particles(0, 0, 'spark', {
|
||||
follow: proj, lifespan: 150, speed: 0, scale: { start: 0.6, end: 0 }, tint: 0xff8800,
|
||||
});
|
||||
return proj;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Enemy motion feels mob-like | Boid mix (sep > cohesion) |
|
||||
| Player one-shot dies | Increase tick interval + iframe |
|
||||
| Projectile invisible | Tint contrast + brief telegraph |
|
||||
| Predict-aim too punishing | Lookahead 시간 short (0.3s) |
|
||||
|
||||
**기본값**: 매 boid+seek mix, 0.4s iframe, 0.5s contact tick, 0.6s telegraph.
|
||||
|
||||
## 🔗 Graph
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: steering weight tuning, telegraph duration enumeration, VFX color spec.
|
||||
**언제 X**: per-frame perf profiling.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No telegraph on fast projectile**: unfair — 0.3s+ telegraph 의 사용.
|
||||
- **iframe 0**: chain-stun death — minimum 0.3s.
|
||||
- **Pure seek (no boid)**: stacking — separation 의 사용.
|
||||
- **Projectile same color as background**: visibility 망 — high-contrast tint.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Reynolds boid paper, bullet-hell genre conventions).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — steering + damage pressure + projectile VFX 정리 |
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-miniboss-tre
|
||||
title: Skybound — Miniboss Treasure Cache Reward Loop
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Treasure Cache, Miniboss Reward]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, reward-loop, miniboss, treasure-chest]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Miniboss Treasure Cache Reward Loop
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 miniboss 의 reward 가 단순 XP gem 이 아닌 'treasure cache' (multi-roll lottery) 일 때 mid-run 의 emotional peak 의 형성"**. 매 Skybound 의 4-26 작업은 stage 중반 miniboss kill 후 chest spawn → 3-roll level-up 의 cascade 를 reward loop 로 정착.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Cache Tiers
|
||||
- **Bronze**: 1-roll, common pool only.
|
||||
- **Silver**: 2-roll, common + uncommon.
|
||||
- **Gold**: 3-roll, full pool + transform-eligible.
|
||||
- **Prismatic**: 5-roll, guaranteed evolution if eligible (rare).
|
||||
|
||||
### 매 Drop Rules
|
||||
- 매 miniboss 는 tier 의 weighted drop (e.g. stage 3 miniboss → silver 70%, gold 25%, prismatic 5%).
|
||||
- 매 cache 는 walk-on pickup — pickup 시 모든 roll 즉시 progression.
|
||||
- 매 cache 의 stack: roll 의 sequential 의 진행, 한번에 처리.
|
||||
|
||||
### 매 응용
|
||||
1. 매 miniboss kill → chest spawn → walk-on → cascade level-up.
|
||||
2. 매 cascade 의 visual: card → fade → next card (1.2s per card).
|
||||
3. 매 prismatic 의 special VFX (rainbow particles, slow-mo).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Cache Definition
|
||||
```typescript
|
||||
type CacheTier = 'bronze' | 'silver' | 'gold' | 'prismatic';
|
||||
|
||||
type Cache = {
|
||||
tier: CacheTier;
|
||||
rolls: number;
|
||||
pool: 'common' | 'uncommon' | 'rare' | 'all';
|
||||
guaranteeEvolution?: boolean;
|
||||
};
|
||||
|
||||
const TIER_CONFIG: Record<CacheTier, Cache> = {
|
||||
bronze: { tier: 'bronze', rolls: 1, pool: 'common' },
|
||||
silver: { tier: 'silver', rolls: 2, pool: 'uncommon' },
|
||||
gold: { tier: 'gold', rolls: 3, pool: 'all' },
|
||||
prismatic: { tier: 'prismatic', rolls: 5, pool: 'all', guaranteeEvolution: true },
|
||||
};
|
||||
```
|
||||
|
||||
### Drop Roll
|
||||
```typescript
|
||||
type DropEntry = { tier: CacheTier; weight: number };
|
||||
|
||||
const STAGE_3_MINIBOSS_DROPS: DropEntry[] = [
|
||||
{ tier: 'bronze', weight: 0 },
|
||||
{ tier: 'silver', weight: 70 },
|
||||
{ tier: 'gold', weight: 25 },
|
||||
{ tier: 'prismatic', weight: 5 },
|
||||
];
|
||||
|
||||
function rollCache(table: DropEntry[]): Cache {
|
||||
const total = table.reduce((s, e) => s + e.weight, 0);
|
||||
let r = Math.random() * total;
|
||||
for (const e of table) {
|
||||
r -= e.weight;
|
||||
if (r <= 0) return TIER_CONFIG[e.tier];
|
||||
}
|
||||
return TIER_CONFIG.bronze;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Pickup → Cascade
|
||||
```typescript
|
||||
class ChestPickup extends Phaser.GameObjects.Sprite {
|
||||
constructor(scene, x: number, y: number, public cache: Cache) {
|
||||
super(scene, x, y, `chest-${cache.tier}`);
|
||||
scene.add.existing(this);
|
||||
}
|
||||
|
||||
onPlayerOverlap(player: Player) {
|
||||
this.destroy();
|
||||
enqueueCascade(player, this.cache);
|
||||
}
|
||||
}
|
||||
|
||||
async function enqueueCascade(player: Player, cache: Cache): Promise<void> {
|
||||
for (let i = 0; i < cache.rolls; i++) {
|
||||
await showLevelUpModal(player, cache.pool);
|
||||
}
|
||||
if (cache.guaranteeEvolution) {
|
||||
const transforms = checkTransforms(player);
|
||||
if (transforms.length) applyTransform(player, transforms[0], scene);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cascade Modal Animation
|
||||
```typescript
|
||||
async function showLevelUpModal(player: Player, pool: PoolKind): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const choices = generateChoices(player, getPool(pool), 3);
|
||||
const modal = new LevelUpModal(scene, choices, (picked) => {
|
||||
applyChoice(player, picked);
|
||||
scene.tweens.add({ targets: modal, alpha: 0, duration: 250, onComplete: () => { modal.destroy(); resolve(); } });
|
||||
});
|
||||
scene.add.existing(modal);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Prismatic VFX
|
||||
```typescript
|
||||
function spawnPrismaticChest(scene, x: number, y: number, cache: Cache) {
|
||||
const chest = new ChestPickup(scene, x, y, cache);
|
||||
const emitter = scene.add.particles(0, 0, 'rainbow-spark', {
|
||||
follow: chest, speed: { min: 30, max: 90 }, lifespan: 800, scale: { start: 1, end: 0 },
|
||||
tint: [0xff0000, 0xff8800, 0xffff00, 0x00ff00, 0x0088ff, 0x8800ff],
|
||||
});
|
||||
chest.on('destroy', () => emitter.destroy());
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Mid-run dopamine flat | Cache cascade injection |
|
||||
| Drop rate too generous | Tier weight rebalance per stage |
|
||||
| Prismatic 너무 흔함 | Stage gate (only stage 5+) |
|
||||
| Cascade pacing slow | Per-card 1.2s + skip-anim button |
|
||||
|
||||
**기본값**: 매 4-tier cache + weighted drop + cascade modal + prismatic VFX.
|
||||
|
||||
## 🔗 Graph
|
||||
- Adjacent: [[Random-Sampling]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: tier balance brainstorm, drop table generation, VFX spec.
|
||||
**언제 X**: particle perf tuning, async UI race conditions (manual debug).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Cache dropped XP only**: emotional flat — multi-roll cascade 의 사용.
|
||||
- **All tiers same VFX**: tier 의 sense lost — prismatic 차별화 필수.
|
||||
- **Cascade blocking input long**: tedium — skip-anim 의 제공.
|
||||
- **Drop weight hardcoded**: per-stage tuning impossible — table 외부화.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vampire Survivors treasure mechanic, Brotato chest tiers).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — miniboss treasure cache cascade reward loop 정리 |
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-player-sprit
|
||||
title: Skybound — Player Sprite Path Warning Fix
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Sprite Path Warning, Phaser Asset Path]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, asset-loading, phaser, build-config]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Player Sprite Path Warning Fix
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Phaser asset path warning 의 root cause 는 build root vs runtime base href mismatch — `import.meta.env.BASE_URL` 의 사용 으로 의 fix"**. 매 Skybound dev console 의 `Failed to load: assets/airframe/biplane.png` 경고, Vite base config + asset import 의 normalize 으로 해결.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Symptom
|
||||
- 매 dev (`npm run dev`) 동작, prod build (`npm run build` + serve) 시 404.
|
||||
- 매 console: `Phaser.Loader.LoaderPlugin: failed to load — file: 'biplane' src: 'assets/airframe/biplane.png'`.
|
||||
- 매 sprite 의 fallback texture (default green/black checker) 의 표시.
|
||||
|
||||
### 매 Root Cause
|
||||
- 매 Vite 의 default base = `/`, GitHub Pages / sub-path deploy 시 의 `/skybound/` — relative `assets/...` 의 wrong base resolution.
|
||||
- 매 Phaser preload 의 string-relative path — page URL 기준으로 resolve.
|
||||
|
||||
### 매 Fix Strategy
|
||||
- 매 `import.meta.env.BASE_URL` 의 prefix 적용.
|
||||
- 매 또는 asset 의 `import` (Vite 의 fingerprint URL 자동 처리).
|
||||
- 매 또는 `vite.config.ts` 의 `base` 의 deploy target 에 맞게 set.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Path Helper
|
||||
```typescript
|
||||
// src/lib/asset.ts
|
||||
export function asset(rel: string): string {
|
||||
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const cleaned = rel.replace(/^\//, '');
|
||||
return `${base}/${cleaned}`;
|
||||
}
|
||||
```
|
||||
|
||||
### Phaser Preload (using helper)
|
||||
```typescript
|
||||
import { asset } from '@/lib/asset';
|
||||
|
||||
class BootScene extends Phaser.Scene {
|
||||
preload() {
|
||||
this.load.image('biplane', asset('assets/airframe/biplane.png'));
|
||||
this.load.image('jet', asset('assets/airframe/jet.png'));
|
||||
this.load.atlas('vfx', asset('assets/vfx/vfx.png'), asset('assets/vfx/vfx.json'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vite-Imported Asset (fingerprinted)
|
||||
```typescript
|
||||
// src/scenes/Boot.ts
|
||||
import biplaneUrl from '@/assets/airframe/biplane.png';
|
||||
import jetUrl from '@/assets/airframe/jet.png';
|
||||
|
||||
class BootScene extends Phaser.Scene {
|
||||
preload() {
|
||||
this.load.image('biplane', biplaneUrl);
|
||||
this.load.image('jet', jetUrl);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vite Config (sub-path deploy)
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
base: process.env.DEPLOY_TARGET === 'github-pages' ? '/skybound/' : '/',
|
||||
build: {
|
||||
assetsDir: 'assets',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Manifest Pattern (large asset count)
|
||||
```typescript
|
||||
// src/manifest.ts
|
||||
import { asset } from '@/lib/asset';
|
||||
|
||||
export const ASSET_MANIFEST = {
|
||||
airframes: {
|
||||
biplane: asset('assets/airframe/biplane.png'),
|
||||
jet: asset('assets/airframe/jet.png'),
|
||||
ufo: asset('assets/airframe/ufo.png'),
|
||||
},
|
||||
vfx: {
|
||||
explosion: asset('assets/vfx/explosion.json'),
|
||||
sparkle: asset('assets/vfx/sparkle.json'),
|
||||
},
|
||||
} as const;
|
||||
|
||||
// preload
|
||||
for (const [k, v] of Object.entries(ASSET_MANIFEST.airframes)) {
|
||||
this.load.image(k, v);
|
||||
}
|
||||
```
|
||||
|
||||
### Load Error Diagnostic
|
||||
```typescript
|
||||
this.load.on('loaderror', (file: Phaser.Loader.File) => {
|
||||
console.error('[asset-load-fail]', {
|
||||
key: file.key,
|
||||
src: file.src,
|
||||
base: import.meta.env.BASE_URL,
|
||||
href: window.location.href,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Few assets (<50) | `import` each — fingerprint + type-safe |
|
||||
| Many assets (>50) | Manifest + `asset()` helper |
|
||||
| Multi-deploy-target | `vite.config base` + helper combo |
|
||||
| Phaser atlas json+png pair | Both via `asset()` helper |
|
||||
|
||||
**기본값**: 매 `import.meta.env.BASE_URL` helper + manifest.
|
||||
|
||||
## 🔗 Graph
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: asset path bug diagnosis, manifest generation, vite config audit.
|
||||
**언제 X**: bundle size optimization (separate concern).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Hardcoded `/assets/...`**: sub-path deploy break — base prefix 의 사용.
|
||||
- **Relative `./assets/...` everywhere**: page URL drift bug — manifest 의 single source.
|
||||
- **No `loaderror` handler**: silent fallback texture — diagnostic listener 의 필수.
|
||||
- **Mixed import + string path**: cache inconsistency — 한 strategy 의 stick.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vite docs on `base`, Phaser 3 Loader docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Vite base + asset manifest fix 정리 |
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-skip-upgrade
|
||||
title: Skybound — Skip Upgrade and Weapon Transform Reconfiguration
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Skip Upgrade Mechanic, Weapon Transform]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, upgrade-system, weapon-transform, choice-design]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Skip Upgrade and Weapon Transform Reconfiguration
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 level-up choice 의 design 은 'skip option' + 'weapon transform' 의 조합 으로 build path agency 의 maximize"**. 매 Skybound 의 4-26 작업은 standard 3-card upgrade 위에 skip (small reward) + weapon transform (2 weapons → 1 evolved) 의 mechanic 추가, evolution graph 의 reconfiguration.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Choice Mechanics
|
||||
- **Standard Pick**: 3 random choice 중 1 — basic loop.
|
||||
- **Skip**: choice reject + small XP/gold/heal — bad-RNG escape valve.
|
||||
- **Reroll**: 1 reroll/level (limited stock) — guided RNG.
|
||||
- **Banish**: remove option from future pool — long-term build steering.
|
||||
- **Transform**: 2 specific weapons + passive prereq → evolved weapon.
|
||||
|
||||
### 매 Weapon Transform Triggers
|
||||
- 매 transform 은 (weapon A maxed) + (passive B owned) → weapon A becomes evolved A'.
|
||||
- 매 evolved weapon 의 max level 의 X — terminal form.
|
||||
- 매 transform 시 base weapon slot 의 freed (원래 의 기획 — 4-26 변경 후 slot 의 retained).
|
||||
|
||||
### 매 Reconfiguration (4-26)
|
||||
- 매 transform 후 base weapon slot 의 retained — multi-evolve build viability.
|
||||
- 매 evolved weapon 에 cosmetic visual 차별화 강화.
|
||||
- 매 transform notification 의 full-screen freeze + audio sting.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Upgrade Choice Generator
|
||||
```typescript
|
||||
type Choice =
|
||||
| { kind: 'weapon'; weaponId: string; toLevel: number }
|
||||
| { kind: 'passive'; passiveId: string; toLevel: number }
|
||||
| { kind: 'skip'; reward: SkipReward };
|
||||
|
||||
function generateChoices(player: Player, pool: UpgradePool, count = 3): Choice[] {
|
||||
const eligible = pool.filter(u => isEligible(u, player) && !player.banished.has(u.id));
|
||||
const picks = sample(eligible, count);
|
||||
return picks.map(p => toChoice(p, player));
|
||||
}
|
||||
|
||||
function rollSkip(): Choice {
|
||||
const r = Math.random();
|
||||
if (r < 0.5) return { kind: 'skip', reward: { kind: 'gold', amount: 50 } };
|
||||
if (r < 0.8) return { kind: 'skip', reward: { kind: 'xp', amount: 20 } };
|
||||
return { kind: 'skip', reward: { kind: 'heal', percent: 0.1 } };
|
||||
}
|
||||
```
|
||||
|
||||
### Weapon Transform Rules
|
||||
```typescript
|
||||
type TransformRule = {
|
||||
weaponId: string;
|
||||
passiveId: string;
|
||||
evolvedWeaponId: string;
|
||||
};
|
||||
|
||||
const TRANSFORM_RULES: TransformRule[] = [
|
||||
{ weaponId: 'cannon', passiveId: 'gunpowder', evolvedWeaponId: 'mega-cannon' },
|
||||
{ weaponId: 'missile', passiveId: 'targeting', evolvedWeaponId: 'swarm-missile' },
|
||||
{ weaponId: 'flak', passiveId: 'shrapnel', evolvedWeaponId: 'meteor-flak' },
|
||||
];
|
||||
|
||||
function checkTransforms(player: Player): TransformRule[] {
|
||||
return TRANSFORM_RULES.filter(rule => {
|
||||
const w = player.weapons.find(w => w.id === rule.weaponId);
|
||||
const p = player.passives.find(p => p.id === rule.passiveId);
|
||||
return w && w.level >= w.maxLevel && p && p.level >= 1 && !player.weapons.some(w => w.id === rule.evolvedWeaponId);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Apply Transform (4-26 reconfig — slot retained)
|
||||
```typescript
|
||||
function applyTransform(player: Player, rule: TransformRule, scene: Phaser.Scene): void {
|
||||
// Old behavior: remove base weapon. New (4-26): keep base.
|
||||
const evolved = createWeapon(rule.evolvedWeaponId);
|
||||
player.weapons.push(evolved); // base weapon retained in its slot
|
||||
|
||||
scene.scene.pause();
|
||||
scene.add.image(W/2, H/2, `evolved-${rule.evolvedWeaponId}-banner`).setDepth(1000);
|
||||
scene.sound.play('transform-sting');
|
||||
scene.time.delayedCall(2500, () => scene.scene.resume());
|
||||
}
|
||||
```
|
||||
|
||||
### Banish + Reroll Stock
|
||||
```typescript
|
||||
class ChoiceMeta {
|
||||
rerollStock = 1;
|
||||
banishStock = 0;
|
||||
|
||||
reroll(level: number): Choice[] | null {
|
||||
if (this.rerollStock <= 0) return null;
|
||||
this.rerollStock--;
|
||||
return generateChoices(player, pool);
|
||||
}
|
||||
|
||||
banish(player: Player, choice: Choice): boolean {
|
||||
if (this.banishStock <= 0) return false;
|
||||
this.banishStock--;
|
||||
if (choice.kind !== 'skip') player.banished.add(idOf(choice));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Choice UI (with skip card)
|
||||
```typescript
|
||||
class LevelUpModal extends Phaser.GameObjects.Container {
|
||||
constructor(scene, choices: Choice[], onPick: (c: Choice) => void) {
|
||||
super(scene);
|
||||
choices.forEach((c, i) => {
|
||||
const card = makeCard(scene, c).setPosition(i * 320, 0).setInteractive();
|
||||
card.on('pointerdown', () => onPick(c));
|
||||
this.add(card);
|
||||
});
|
||||
const skipCard = makeCard(scene, rollSkip()).setPosition(choices.length * 320, 0).setInteractive();
|
||||
skipCard.on('pointerdown', () => onPick(rollSkip()));
|
||||
this.add(skipCard);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| RNG too punishing | Skip + reroll + banish triumvirate |
|
||||
| Build path 의 monotony | Transform with prereq passive |
|
||||
| Slot pressure | 4-26 reconfig: transform retains slot |
|
||||
| New player overwhelm | Tutorial: introduce mechanics 1-by-1 |
|
||||
|
||||
**기본값**: 매 3-card + skip + 1 reroll + transform-retains-slot.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Choice-Architecture]]
|
||||
- Adjacent: [[API Response & State Modeling|Discriminated-Unions]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: weapon evolution chart, prereq passive brainstorm, skip reward tuning.
|
||||
**언제 X**: visual transform animation timing.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No skip option**: bad-roll lockup — skip 의 always available.
|
||||
- **Transform removes slot**: build flexibility 망 — 4-26 reconfig 의 사용.
|
||||
- **Reroll unlimited**: tension 망 — stock-based.
|
||||
- **Evolved weapon levelable**: power creep — terminal form 의 lock.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vampire Survivors evolution mechanic, Brotato shop).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — skip + transform reconfig (slot retained) 정리 |
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-stage1-to-3-
|
||||
title: Skybound Stage1-3 Playtest — Balance, Bomb, Visual Diversity Pass (2026-04-26)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Skybound Playtest 2026-04-26, Skybound Stage1-3 Balance Pass]
|
||||
duplicate_of: none
|
||||
source_trust_level: B
|
||||
confidence_score: 0.7
|
||||
verification_status: applied
|
||||
tags: [game-design, playtest, balance, skybound, postmortem]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: skybound-engine
|
||||
---
|
||||
|
||||
# Skybound Stage1-3 Playtest — Balance, Bomb, Visual Diversity Pass (2026-04-26)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 early-game (Stage 1-3) 의 onboarding curve 의 too-steep 의 finding"**. 매 internal playtest log (2026-04-26) — 매 8 testers, 매 70% 가 Stage 2 mid-boss 에서 first-attempt drop. Bomb usage 의 underutilized (avg 0.3 bombs/run vs design intent 2-3). Visual diversity 의 lacking — Stage 2/3 의 같은 palette 의 perception.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 findings
|
||||
- **Difficulty spike (Stage 2 mid-boss)**: 매 bullet density doubling 의 sudden — 매 70% drop rate.
|
||||
- **Bomb dead feature**: avg 0.3 / run — 매 UI 의 invisible, cooldown 의 unclear.
|
||||
- **Visual repetition**: Stage 2/3 의 same blue-gray palette → 매 progression feel 의 absent.
|
||||
- **Audio mix**: BGM 의 SFX 의 drown out (-12dB ducking 부족).
|
||||
|
||||
### 매 action items
|
||||
- Stage 2 mid-boss bullet density: 1.8x → 1.3x (drop rate target <30%).
|
||||
- Bomb: 매 UI hotbar prominent, full-screen flash on use, cooldown ring.
|
||||
- Stage 3 palette: blue-gray → sunset orange/red (매 narrative shift 의 reflect).
|
||||
- Audio: BGM duck -12dB during boss SFX trigger.
|
||||
|
||||
### 매 응용
|
||||
1. 매 next playtest (2026-05-10) 의 baseline.
|
||||
2. 매 difficulty curve doc 의 update.
|
||||
3. 매 marketing screenshot 의 visual diversity 의 emphasize.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Pattern 1 — 매 difficulty curve config
|
||||
```ts
|
||||
// stages.config.ts
|
||||
export const STAGE_CONFIG = {
|
||||
stage1: { bulletDensityMul: 1.0, enemyHpMul: 1.0 },
|
||||
stage2: { bulletDensityMul: 1.3, enemyHpMul: 1.2 }, // 매 was 1.8
|
||||
stage3: { bulletDensityMul: 1.6, enemyHpMul: 1.5 },
|
||||
};
|
||||
```
|
||||
|
||||
### Pattern 2 — 매 bomb UI hotbar
|
||||
```tsx
|
||||
function BombHud({ bombs, cooldown }: { bombs: number; cooldown: number }) {
|
||||
return (
|
||||
<div className="bomb-hud">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className={`bomb-slot ${i < bombs ? 'filled' : ''}`}>
|
||||
<svg className="cooldown-ring" style={{ '--p': cooldown } as any} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3 — 매 bomb activation flash
|
||||
```ts
|
||||
function activateBomb(scene: Scene) {
|
||||
scene.cameras.main.flash(400, 255, 255, 255);
|
||||
scene.cameras.main.shake(200, 0.01);
|
||||
scene.bullets.getMatching('active', true).forEach((b) => b.destroy());
|
||||
scene.events.emit('bomb-used');
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4 — 매 stage palette swap
|
||||
```ts
|
||||
const PALETTES = {
|
||||
stage1: { fog: 0xa0b8d0, accent: 0x5588cc },
|
||||
stage2: { fog: 0x8090a0, accent: 0x6677aa },
|
||||
stage3: { fog: 0xffaa66, accent: 0xff5533 }, // 매 sunset
|
||||
};
|
||||
|
||||
function loadStage(n: 1 | 2 | 3) {
|
||||
const p = PALETTES[`stage${n}`];
|
||||
scene.fogColor.set(p.fog);
|
||||
scene.lights.directional.color.set(p.accent);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5 — 매 audio ducking
|
||||
```ts
|
||||
const bgm = audio.play('bgm', { volume: 0.6 });
|
||||
events.on('boss-attack', () => {
|
||||
audio.fadeTo(bgm, 0.15, 200); // -12dB 매 200ms
|
||||
setTimeout(() => audio.fadeTo(bgm, 0.6, 400), 1500);
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 6 — 매 playtest telemetry
|
||||
```ts
|
||||
telemetry.track('stage_drop', {
|
||||
stage: 2,
|
||||
reason: 'mid-boss',
|
||||
attempts: player.attempts,
|
||||
bombsUsed: player.bombsUsed,
|
||||
durationSec: (now() - stageStart) / 1000,
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 drop rate >50% at single encounter | 매 bullet density / HP 의 reduce. |
|
||||
| 매 feature underutilized (bomb) | UI prominence + tutorial prompt. |
|
||||
| 매 visual sameness | palette / fog / lighting variation per stage. |
|
||||
| 매 audio masking | sidechain ducking on critical SFX. |
|
||||
|
||||
**기본값**: 매 quantitative telemetry (drop rate, bomb usage) + qualitative feedback (think-aloud). 매 both 의 weight.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[게임 밸런싱|Game Balance]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 next playtest planning, 매 balance change rationale draft.
|
||||
**언제 X**: 매 single-build statistical conclusion — 매 N=8 playtest 의 anecdotal weight.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 difficulty 의 lower 의 over**: 매 challenge feel 의 erase.
|
||||
- **매 telemetry only**: think-aloud / interview qualitative 의 ignore X.
|
||||
- **매 bomb 의 free**: cooldown / count limit 없이 — 매 game design 의 trivialize.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (internal playtest log 2026-04-26, N=8 testers).
|
||||
- 신뢰도 B (small sample, internal).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — playtest findings + action items 의 structured |
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
---
|
||||
id: wiki-2026-0508-2026-04-26-skybound-stage-minibo
|
||||
title: Skybound — Stage Miniboss Pattern Differentiation
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Miniboss Patterns, Stage-Specific Miniboss]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [skybound, gamedev, miniboss-design, attack-pattern, stage-identity]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: phaser3
|
||||
---
|
||||
|
||||
# Skybound — Stage Miniboss Pattern Differentiation
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 8 stage 의 miniboss 가 'pattern signature' 의 distinct 일 때 stage identity 의 안 collapse"**. 매 Skybound 의 4-26 작업은 stage 별 miniboss 의 attack pattern (radial / sweep / charge / split / chain / mirror / summoner / null-zone) 의 1:1 mapping 정립.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Stage-to-Pattern Mapping
|
||||
1. **Stage 1**: Radial — 8-direction burst, slow turn rate.
|
||||
2. **Stage 2**: Sweep — laser arc, telegraphed.
|
||||
3. **Stage 3**: Charge — dash line + recover stun.
|
||||
4. **Stage 4**: Split — projectile fragments mid-flight.
|
||||
5. **Stage 5**: Chain — bounce projectiles between mobs.
|
||||
6. **Stage 6**: Mirror — clones player movement (lagged).
|
||||
7. **Stage 7**: Summoner — spawns minions in waves.
|
||||
8. **Stage 8**: Null-zone — area denial circles, forces movement.
|
||||
|
||||
### 매 Differentiation Rules
|
||||
- 매 pattern 의 unique tell (visual + audio).
|
||||
- 매 pattern 별 counter-play (positioning / timing / kiting).
|
||||
- 매 stage progression 의 pattern complexity escalation.
|
||||
|
||||
### 매 Phase Variants
|
||||
- 매 miniboss HP < 50% 시 pattern 의 modifier 추가 (faster, more, larger).
|
||||
- 매 enrage (HP < 20%) 의 mixed pattern.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Pattern Definition
|
||||
```typescript
|
||||
type AttackPattern =
|
||||
| { kind: 'radial'; bullets: number; bulletSpeed: number; rotateRate: number }
|
||||
| { kind: 'sweep'; arcDeg: number; durationMs: number; telegraphMs: number }
|
||||
| { kind: 'charge'; chargeSpeed: number; recoverMs: number }
|
||||
| { kind: 'split'; firstStage: { count: number; speed: number }; splitAt: number; splitInto: number }
|
||||
| { kind: 'chain'; bounces: number; targets: 'enemies' | 'walls' }
|
||||
| { kind: 'mirror'; lagMs: number }
|
||||
| { kind: 'summoner'; minionId: string; perWave: number; everyMs: number }
|
||||
| { kind: 'null-zone'; radius: number; lifespanMs: number; spawnEveryMs: number };
|
||||
|
||||
const STAGE_PATTERNS: Record<number, AttackPattern> = {
|
||||
1: { kind: 'radial', bullets: 8, bulletSpeed: 200, rotateRate: 0.5 },
|
||||
2: { kind: 'sweep', arcDeg: 120, durationMs: 1500, telegraphMs: 600 },
|
||||
3: { kind: 'charge', chargeSpeed: 600, recoverMs: 1200 },
|
||||
4: { kind: 'split', firstStage: { count: 3, speed: 240 }, splitAt: 0.5, splitInto: 5 },
|
||||
5: { kind: 'chain', bounces: 3, targets: 'enemies' },
|
||||
6: { kind: 'mirror', lagMs: 600 },
|
||||
7: { kind: 'summoner', minionId: 'drone-small', perWave: 4, everyMs: 4000 },
|
||||
8: { kind: 'null-zone', radius: 120, lifespanMs: 5000, spawnEveryMs: 2500 },
|
||||
};
|
||||
```
|
||||
|
||||
### Pattern Runner
|
||||
```typescript
|
||||
class MinibossController {
|
||||
pattern: AttackPattern;
|
||||
phase: 'p1' | 'enrage' = 'p1';
|
||||
|
||||
update(dt: number, hp: number, maxHp: number) {
|
||||
if (hp / maxHp < 0.2) this.phase = 'enrage';
|
||||
const speedMul = this.phase === 'enrage' ? 1.6 : 1;
|
||||
this.runPattern(this.pattern, dt, speedMul);
|
||||
}
|
||||
|
||||
private runPattern(p: AttackPattern, dt: number, mul: number) {
|
||||
switch (p.kind) {
|
||||
case 'radial': return this.runRadial(p, dt, mul);
|
||||
case 'sweep': return this.runSweep(p, dt, mul);
|
||||
case 'charge': return this.runCharge(p, dt, mul);
|
||||
case 'split': return this.runSplit(p, dt, mul);
|
||||
case 'chain': return this.runChain(p, dt, mul);
|
||||
case 'mirror': return this.runMirror(p, dt, mul);
|
||||
case 'summoner':return this.runSummoner(p, dt, mul);
|
||||
case 'null-zone': return this.runNullZone(p, dt, mul);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Radial Volley
|
||||
```typescript
|
||||
private runRadial(p: AttackPattern & { kind: 'radial' }, dt: number, mul: number) {
|
||||
this.cooldown -= dt;
|
||||
if (this.cooldown > 0) return;
|
||||
this.cooldown = 1.2 / mul;
|
||||
for (let i = 0; i < p.bullets; i++) {
|
||||
const angle = (i / p.bullets) * Math.PI * 2 + this.aimAngle;
|
||||
spawnProjectile(this.pos, angle, p.bulletSpeed * mul);
|
||||
}
|
||||
this.aimAngle += p.rotateRate * dt;
|
||||
}
|
||||
```
|
||||
|
||||
### Mirror (lagged player clone)
|
||||
```typescript
|
||||
private runMirror(p: AttackPattern & { kind: 'mirror' }, _dt: number, _mul: number) {
|
||||
const samples = this.playerHistory.samplesOlderThan(p.lagMs);
|
||||
const target = samples.lastBefore(p.lagMs);
|
||||
if (target) {
|
||||
this.moveToward(target.pos, this.speed);
|
||||
if (this.fireCooldown <= 0) {
|
||||
spawnProjectileAt(this.pos, target.pos.angleFrom(this.pos), 320);
|
||||
this.fireCooldown = 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Null-Zone Spawning
|
||||
```typescript
|
||||
private runNullZone(p: AttackPattern & { kind: 'null-zone' }, _dt: number, mul: number) {
|
||||
this.zoneCooldown -= _dt;
|
||||
if (this.zoneCooldown > 0) return;
|
||||
this.zoneCooldown = (p.spawnEveryMs / 1000) / mul;
|
||||
spawnNullZone({
|
||||
pos: this.playerPosOffsetRandom(200),
|
||||
radius: p.radius,
|
||||
lifespanMs: p.lifespanMs,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Patterns feel samey | 1 stage = 1 signature pattern |
|
||||
| Late-stage too easy | Enrage modifier (1.5-2x speed) |
|
||||
| Pattern unreadable | Telegraph + audio sting |
|
||||
| Mirror too punishing | Lag 600ms+ (player can outrun) |
|
||||
|
||||
**기본값**: 매 8-pattern enum, stage:pattern = 1:1, enrage 1.6x speed mul.
|
||||
|
||||
## 🔗 Graph
|
||||
- Adjacent: [[API Response & State Modeling|Discriminated-Unions]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: pattern enumeration, counter-play brainstorm, telegraph copy.
|
||||
**언제 X**: hitbox tuning, frame-perfect timing.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **All bosses radial**: identity collapse — 1:1 pattern.
|
||||
- **No enrage phase**: predictable end — 20% HP threshold modifier.
|
||||
- **Mirror lag too short**: unbeatable — 600ms+ lag.
|
||||
- **Telegraph 누락**: unfair — visual+audio always.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Touhou pattern taxonomy, Hades boss design).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 8-pattern stage:miniboss mapping 정리 |
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
id: wiki-2026-0508-accessibility-a11y
|
||||
title: Accessibility (A11y)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [A11y, Web Accessibility, WCAG]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [accessibility, a11y, wcag, aria, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: HTML/CSS/JS
|
||||
framework: WCAG 2.2
|
||||
---
|
||||
|
||||
# Accessibility (A11y)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 사용자가 매 콘텐츠에 매 접근 가능"**. A11y는 visual/auditory/motor/cognitive 장애 사용자도 web product를 사용할 수 있도록 design + implement 하는 practice. 2026 기준 WCAG 2.2가 standard, EU EAA 강제 발효(2025-06)로 commercial site 의 legal requirement.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4 원칙 (POUR)
|
||||
- **Perceivable**: 매 contrast, alt text, captions — 매 sense 통해 perceive 가능.
|
||||
- **Operable**: keyboard nav, focus management, no seizure-triggering content.
|
||||
- **Understandable**: clear language, predictable behavior, input help.
|
||||
- **Robust**: valid semantic HTML, ARIA correct, assistive tech compatible.
|
||||
|
||||
### 매 ARIA vs Semantic HTML
|
||||
- **First rule**: 매 native HTML element 가 있으면 ARIA 의 X. `<button>` > `<div role="button">`.
|
||||
- **ARIA 사용 case**: dynamic widget (combobox, tabpanel, dialog), live region, no native equivalent.
|
||||
|
||||
### 매 응용
|
||||
1. WCAG 2.2 AA conformance — most legal threshold.
|
||||
2. Screen reader testing (VoiceOver/NVDA/JAWS).
|
||||
3. Keyboard-only navigation flow.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Skip link
|
||||
```html
|
||||
<a href="#main" class="skip-link">Skip to main content</a>
|
||||
<style>
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
.skip-link:focus {
|
||||
left: 0; top: 0;
|
||||
background: #000; color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
<main id="main" tabindex="-1">...</main>
|
||||
```
|
||||
|
||||
### Accessible modal (focus trap)
|
||||
```tsx
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function Modal({ isOpen, onClose, children }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lastFocus = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
lastFocus.current = document.activeElement as HTMLElement;
|
||||
ref.current?.focus();
|
||||
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
lastFocus.current?.focus();
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<h2 id="modal-title">Confirm</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Live region for async updates
|
||||
```html
|
||||
<div aria-live="polite" aria-atomic="true" id="status"></div>
|
||||
<script>
|
||||
// 매 새로운 toast 매 polite 알림
|
||||
document.getElementById('status').textContent = 'Saved successfully';
|
||||
</script>
|
||||
```
|
||||
|
||||
### Form validation with aria-describedby
|
||||
```html
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-err"
|
||||
required
|
||||
/>
|
||||
<span id="email-err" role="alert">유효한 이메일 입력</span>
|
||||
```
|
||||
|
||||
### Visually hidden but screen-reader visible
|
||||
```css
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Color contrast check (WCAG AA = 4.5:1 for body)
|
||||
```ts
|
||||
function relLuminance(rgb: [number, number, number]) {
|
||||
const [r, g, b] = rgb.map(v => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
function contrast(a: [number,number,number], b: [number,number,number]) {
|
||||
const [L1, L2] = [relLuminance(a), relLuminance(b)].sort((x,y) => y-x);
|
||||
return (L1 + 0.05) / (L2 + 0.05);
|
||||
}
|
||||
```
|
||||
|
||||
### Reduced motion
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Custom widget | ARIA + keyboard handler |
|
||||
| Native equivalent 존재 | Use semantic HTML, no ARIA |
|
||||
| Async status | `aria-live="polite"` |
|
||||
| Critical alert | `role="alert"` (assertive) |
|
||||
| Modal | focus trap + `aria-modal="true"` |
|
||||
|
||||
**기본값**: semantic HTML first, ARIA only as supplement.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Frontend]] · [[WCAG]]
|
||||
- 변형: [[ARIA]] · [[Screen Reader]]
|
||||
- 응용: [[Focus Management]]
|
||||
- Adjacent: [[Semantic HTML]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: ARIA pattern lookup, WCAG criterion explanation, accessibility audit script generation.
|
||||
**언제 X**: real screen reader testing — manual + actual AT 사용 필수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **div soup**: 매 `<div onclick>` — keyboard 의 X.
|
||||
- **alt="image"**: meaningless alt — describe content or `alt=""` for decorative.
|
||||
- **Removed focus outline**: `outline:none` without replacement — keyboard user 의 lost.
|
||||
- **Color-only signal**: error 만 red — 매 color blind user invisible.
|
||||
- **ARIA overuse**: `role="button"` on `<button>` — redundant + harmful.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (WCAG 2.2 W3C Recommendation 2023, ARIA 1.2 spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — A11y 4 원칙 + ARIA pattern 정리 |
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
id: wiki-2026-0508-automatic-batching
|
||||
title: Automatic Batching (React 18+)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Batching, Concurrent Batching]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, batching, performance, rendering]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19
|
||||
---
|
||||
|
||||
# Automatic Batching (React 18+)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 setState 매 합쳐서 매 한 번 render"**. React 18에서 도입된 automatic batching은 모든 update (promise, setTimeout, native event)를 single re-render로 group한다. React 17 이전엔 React event handler 안에서만 batching이었음 — 이제 전부 자동.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 동작
|
||||
- 매 동일 tick 안의 setState calls → React가 모아서 하나의 commit 으로 처리.
|
||||
- 결과: less render, less DOM mutation, less component lifecycle invocation.
|
||||
- React 19도 동일 model 유지 + Compiler 가 추가 optimization.
|
||||
|
||||
### React 17 vs 18+
|
||||
- **17**: `onClick` 안 batching O / `setTimeout`, `Promise.then` 안 batching X.
|
||||
- **18+**: 매 모든 context batching O.
|
||||
- Opt-out: `flushSync()` 로 immediate render 강제.
|
||||
|
||||
### 매 응용
|
||||
1. Form state with multiple fields — 매 single render.
|
||||
2. Async fetch chain — 매 single render after Promise.
|
||||
3. Concurrent transitions — startTransition + batching.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React 18 default behavior
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
function Form() {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [age, setAge] = useState(0);
|
||||
|
||||
async function handleSubmit() {
|
||||
const res = await fetch('/api/me');
|
||||
const data = await res.json();
|
||||
// 18+: 매 3 setState 매 single render
|
||||
setName(data.name);
|
||||
setEmail(data.email);
|
||||
setAge(data.age);
|
||||
}
|
||||
|
||||
return <button onClick={handleSubmit}>Load</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### flushSync to opt out
|
||||
```tsx
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
function handleClick() {
|
||||
flushSync(() => {
|
||||
setCount(c => c + 1);
|
||||
});
|
||||
// 매 DOM 업데이트 매 done — measure 가능
|
||||
const h = listRef.current.scrollHeight;
|
||||
flushSync(() => {
|
||||
setHeight(h);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Custom hook with batched async
|
||||
```tsx
|
||||
function useAsyncForm<T>() {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
async function load(fn: () => Promise<T>) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fn();
|
||||
// 18+: 매 두 setState 매 single render
|
||||
setData(res);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { data, loading, error, load };
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer alternative
|
||||
```tsx
|
||||
const [state, dispatch] = useReducer(reducer, initial);
|
||||
// 매 single dispatch 매 multi-field update — natural batching
|
||||
dispatch({ type: 'SUBMIT_OK', payload: data });
|
||||
```
|
||||
|
||||
### Avoid stale closure with functional updates
|
||||
```tsx
|
||||
// 매 sequential update — 매 함수형 사용
|
||||
setCount(c => c + 1);
|
||||
setCount(c => c + 1); // → +2, not +1
|
||||
```
|
||||
|
||||
### Measuring render with Profiler
|
||||
```tsx
|
||||
import { Profiler } from 'react';
|
||||
|
||||
<Profiler
|
||||
id="Form"
|
||||
onRender={(id, phase, actual) => console.log(id, phase, actual)}
|
||||
>
|
||||
<Form />
|
||||
</Profiler>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Multi-field update | default (let batching work) |
|
||||
| DOM measurement between updates | `flushSync` |
|
||||
| Sequential counter update | functional updater |
|
||||
| Complex state graph | `useReducer` |
|
||||
| Concurrent UI | `startTransition` |
|
||||
|
||||
**기본값**: do nothing — automatic batching handles it.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[Concurrent Features|Concurrent Rendering]]
|
||||
- 변형: [[Batching]] · [[startTransition]]
|
||||
- Adjacent: [[flushSync]] · [[useReducer]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: explain when batching applies, refactor pre-18 code, debug "why is render double" question.
|
||||
**언제 X**: actual render count measurement — Profiler / DevTools.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **불필요한 flushSync**: 매 performance 의 hurt — 마지막 수단.
|
||||
- **Sequential setState with stale value**: `setCount(count+1); setCount(count+1)` → +1.
|
||||
- **Object identity reset**: `setData({...})` matter — same shallow ref skip.
|
||||
- **Mid-render setState**: trigger infinite loop.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 18 RFC, React 19 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — React 18+ batching pattern + flushSync |
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: wiki-2026-0508-bem
|
||||
title: BEM
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Block Element Modifier, BEM Methodology]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, bem, methodology, naming-convention]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: CSS
|
||||
framework: BEM
|
||||
---
|
||||
|
||||
# BEM
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Block, Element, Modifier 매 명시적 naming"**. BEM은 Yandex가 2009년 도입한 CSS naming methodology — `.block__element--modifier` 형식으로 component scope를 explicit encode하여 specificity war 없이 large CSS codebase를 maintain. 2026 기준 CSS Modules / Tailwind / CSS-in-JS에 밀렸으나 SSR-heavy + design-system context에서 여전히 active.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 3 entity
|
||||
- **Block**: 매 standalone component — `.button`, `.menu`, `.card`.
|
||||
- **Element**: 매 block 내부 part — `.card__title`, `.menu__item`.
|
||||
- **Modifier**: 매 variation/state — `.button--primary`, `.menu__item--active`.
|
||||
|
||||
### Naming rule
|
||||
- `.block`
|
||||
- `.block__element` (double underscore)
|
||||
- `.block--modifier` 또는 `.block__element--modifier` (double dash)
|
||||
- 매 hyphen 매 word separator: `.search-form__input--wide`.
|
||||
|
||||
### 매 응용
|
||||
1. Design system component class 명명.
|
||||
2. CSS Module 없이 SSR 환경 scope 격리.
|
||||
3. SCSS partial 으로 component file 별 분리.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic BEM
|
||||
```html
|
||||
<button class="button button--primary">Save</button>
|
||||
|
||||
<div class="card card--featured">
|
||||
<h3 class="card__title">제목</h3>
|
||||
<p class="card__body">본문</p>
|
||||
<a class="card__link card__link--external" href="...">More</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.button { padding: 0.5rem 1rem; }
|
||||
.button--primary { background: #0066cc; color: #fff; }
|
||||
.button--primary:hover { background: #0052a3; }
|
||||
|
||||
.card { border: 1px solid #ddd; border-radius: 4px; }
|
||||
.card--featured { border-color: gold; }
|
||||
.card__title { font-size: 1.25rem; }
|
||||
.card__body { color: #555; }
|
||||
.card__link--external::after { content: ' ↗'; }
|
||||
```
|
||||
|
||||
### SCSS with & parent selector
|
||||
```scss
|
||||
.menu {
|
||||
display: flex;
|
||||
|
||||
&__item {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&--active {
|
||||
background: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover { background: #f5f5f5; }
|
||||
}
|
||||
|
||||
&--vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BEM with React (JSX)
|
||||
```tsx
|
||||
import classNames from 'classnames';
|
||||
|
||||
function Button({ variant = 'default', size = 'md', children, disabled }) {
|
||||
const cls = classNames(
|
||||
'button',
|
||||
`button--${variant}`,
|
||||
`button--${size}`,
|
||||
{ 'button--disabled': disabled }
|
||||
);
|
||||
return <button className={cls} disabled={disabled}>{children}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### BEM helper utility
|
||||
```ts
|
||||
function bem(block: string) {
|
||||
return (element?: string, modifier?: string | string[]) => {
|
||||
const base = element ? `${block}__${element}` : block;
|
||||
if (!modifier) return base;
|
||||
const mods = (Array.isArray(modifier) ? modifier : [modifier])
|
||||
.filter(Boolean)
|
||||
.map(m => `${base}--${m}`)
|
||||
.join(' ');
|
||||
return `${base} ${mods}`;
|
||||
};
|
||||
}
|
||||
|
||||
const card = bem('card');
|
||||
card(); // 'card'
|
||||
card('title'); // 'card__title'
|
||||
card('title', 'large'); // 'card__title card__title--large'
|
||||
card(undefined, 'featured'); // 'card card--featured'
|
||||
```
|
||||
|
||||
### Don't nest blocks deeply
|
||||
```css
|
||||
/* 매 BEM rule: 매 element 의 element 의 X */
|
||||
/* X */
|
||||
.card__body__title
|
||||
|
||||
/* O — 매 flat */
|
||||
.card__title
|
||||
```
|
||||
|
||||
### Mix vs nest
|
||||
```html
|
||||
<!-- 매 mix: block 매 다른 block 의 element 의 same node -->
|
||||
<button class="button button--primary header__action"></button>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Server-rendered, no build | BEM |
|
||||
| React + build pipeline | CSS Modules / Tailwind |
|
||||
| Design system stable | BEM (predictable) |
|
||||
| Rapid prototyping | Tailwind |
|
||||
| Theme-heavy | CSS-in-JS / vanilla-extract |
|
||||
|
||||
**기본값**: 2026 신규 프로젝트 — Tailwind / CSS Modules. Legacy / framework-agnostic — BEM.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS 구조 설계 방식]]
|
||||
- 응용: [[Design System]] · [[SCSS (Sass)|SCSS]]
|
||||
- Adjacent: [[CSS Modules]] · [[CSS_Architecture_and_Styling|Tailwind CSS]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: convert legacy CSS to BEM, generate consistent class names, BEM rule lint.
|
||||
**언제 X**: 매 modern utility-first project — Tailwind 가 더 적합.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Element 의 element**: `.card__body__title` — flat 으로 변경.
|
||||
- **Modifier 만 사용**: `.large` — must be `.button--large`.
|
||||
- **Block 의 child selector**: `.card .card__title` — flat naming 의 point 의 lost.
|
||||
- **Camel case mix**: `.cardTitle` — 매 BEM 규칙 hyphen.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Yandex BEM official, getbem.com).
|
||||
- 신뢰도 A.
|
||||
- 매 동일 wiki에 [[BEM|BEM (Block Element Modifier)]] file 존재 — both kept as alias siblings.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — BEM 3 entity + helper pattern 정리 |
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
id: wiki-2026-0508-batching
|
||||
title: Batching
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Update Batching, Render Batching]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [batching, performance, rendering, reactive]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React/Vue/Svelte
|
||||
---
|
||||
|
||||
# Batching
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 여러 update 매 한 번 처리"**. Batching은 multiple state changes를 single update cycle로 묶어 redundant rendering/computation을 줄이는 reactive UI 의 core optimization. 2026 모든 mainstream framework (React, Vue, Svelte, Solid) 에서 default behavior.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 batching 이 필요한 이유
|
||||
- 매 setState 매 separate render → DOM mutation N times.
|
||||
- 매 batching → microtask/tick 까지 모아 single commit → DOM mutation 1 time.
|
||||
- Layout thrashing 방지, paint 횟수 감소.
|
||||
|
||||
### Framework comparison
|
||||
- **React 18+**: 매 모든 context automatic.
|
||||
- **Vue 3**: nextTick scheduler — sync write, async render.
|
||||
- **Svelte 5 (runes)**: microtask flush.
|
||||
- **Solid**: `batch()` explicit + signal-based fine-grained update.
|
||||
|
||||
### 매 응용
|
||||
1. Form multi-field update.
|
||||
2. Async fetch result write.
|
||||
3. Animation frame scheduling.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React 18+ implicit batching
|
||||
```tsx
|
||||
function update() {
|
||||
setA(1);
|
||||
setB(2);
|
||||
setC(3);
|
||||
// 매 single render
|
||||
}
|
||||
```
|
||||
|
||||
### Vue 3 batched watcher
|
||||
```ts
|
||||
import { ref, watchEffect, nextTick } from 'vue';
|
||||
|
||||
const count = ref(0);
|
||||
watchEffect(() => console.log(count.value));
|
||||
|
||||
count.value++;
|
||||
count.value++;
|
||||
count.value++;
|
||||
await nextTick();
|
||||
// 매 console.log 매 한 번
|
||||
```
|
||||
|
||||
### Solid explicit batch
|
||||
```tsx
|
||||
import { batch, createSignal } from 'solid-js';
|
||||
|
||||
const [a, setA] = createSignal(0);
|
||||
const [b, setB] = createSignal(0);
|
||||
|
||||
batch(() => {
|
||||
setA(1);
|
||||
setB(2);
|
||||
}); // 매 single update
|
||||
```
|
||||
|
||||
### Custom batcher (vanilla JS)
|
||||
```ts
|
||||
class Batcher {
|
||||
private queue = new Set<() => void>();
|
||||
private scheduled = false;
|
||||
|
||||
schedule(fn: () => void) {
|
||||
this.queue.add(fn);
|
||||
if (!this.scheduled) {
|
||||
this.scheduled = true;
|
||||
queueMicrotask(() => this.flush());
|
||||
}
|
||||
}
|
||||
|
||||
private flush() {
|
||||
for (const fn of this.queue) fn();
|
||||
this.queue.clear();
|
||||
this.scheduled = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RAF-based animation batching
|
||||
```ts
|
||||
let pending: (() => void)[] = [];
|
||||
let frameId: number | null = null;
|
||||
|
||||
function scheduleAnimation(fn: () => void) {
|
||||
pending.push(fn);
|
||||
if (frameId === null) {
|
||||
frameId = requestAnimationFrame(() => {
|
||||
const fns = pending;
|
||||
pending = [];
|
||||
frameId = null;
|
||||
for (const f of fns) f();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database write batching
|
||||
```ts
|
||||
class WriteBatcher {
|
||||
private buffer: Record[] = [];
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
|
||||
add(r: Record) {
|
||||
this.buffer.push(r);
|
||||
if (!this.timer) {
|
||||
this.timer = setTimeout(() => this.flush(), 50);
|
||||
}
|
||||
if (this.buffer.length >= 1000) this.flush();
|
||||
}
|
||||
|
||||
private async flush() {
|
||||
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
||||
const batch = this.buffer.splice(0);
|
||||
if (batch.length) await db.insertMany(batch);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| React UI | Default automatic batching |
|
||||
| Solid signal | Explicit `batch()` |
|
||||
| Vanilla DOM | `queueMicrotask` 또는 RAF |
|
||||
| API write throttling | timer + size threshold |
|
||||
| Need immediate flush | `flushSync` (React) / explicit await |
|
||||
|
||||
**기본값**: framework default, only override when necessary.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Performance]]
|
||||
- 변형: [[Automatic Batching]]
|
||||
- Adjacent: [[Throttling]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: framework batching behavior 설명, custom batcher prototyping, batching vs debouncing 구분.
|
||||
**언제 X**: real perf measurement — Profiler / Chrome DevTools.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Force flush 남용**: synchronous render 강제 → 매 batching benefit 의 lost.
|
||||
- **State variable explosion**: 5+ useState → useReducer / Object state.
|
||||
- **Async loop 안 setState**: 매 await 마다 매 separate batch — group with Promise.all.
|
||||
- **No throttle on rapid event**: scroll/resize raw — RAF / debounce.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 18 RFC, Vue 3 reactivity guide, Solid docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — generic batching across frameworks |
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
id: wiki-2026-0508-branded-types-for-nominal-typing
|
||||
title: Branded Types for Nominal Typing
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Nominal Types TypeScript, Opaque Types, Branded Types]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, types, nominal-typing, type-safety]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: TS 5.x
|
||||
---
|
||||
|
||||
# Branded Types for Nominal Typing
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 string 매 매 string 의 X"**. Branded types는 TypeScript의 structural type system 안에 nominal distinction을 흉내내는 trick — 동일한 underlying type (예: string)을 `UserId` vs `OrderId`처럼 incompatible 하게 만들어 mix-up을 compile time에 잡는다. 2026 거의 모든 fintech / id-heavy domain code에 standard.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 동기
|
||||
- TS는 structural — `{ name: string }` 두 개는 호환.
|
||||
- Domain-specific id (UserId, OrderId, Email) 가 모두 plain string → 매 swap mistake 매 compile O.
|
||||
- Brand 추가 → structurally distinct.
|
||||
|
||||
### 매 구현 패턴
|
||||
- **Symbol-based brand**: `string & { readonly [BrandKey]: 'UserId' }`.
|
||||
- **Intersection brand**: simple intersection.
|
||||
- **TypeScript 5.x `unique symbol`**: 매 compile-only field — runtime cost zero.
|
||||
|
||||
### 매 응용
|
||||
1. ID 매 strong typing (UserId vs OrderId vs SessionId).
|
||||
2. Validated string (Email, URL) — runtime check 후 brand.
|
||||
3. Unit type (Meter, Second) — physical unit safety.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic brand utility
|
||||
```ts
|
||||
declare const __brand: unique symbol;
|
||||
type Brand<T, B> = T & { readonly [__brand]: B };
|
||||
|
||||
type UserId = Brand<string, 'UserId'>;
|
||||
type OrderId = Brand<string, 'OrderId'>;
|
||||
|
||||
function userId(s: string): UserId { return s as UserId; }
|
||||
function orderId(s: string): OrderId { return s as OrderId; }
|
||||
|
||||
function getUser(id: UserId) { /* ... */ }
|
||||
|
||||
const u = userId('u_123');
|
||||
const o = orderId('o_456');
|
||||
|
||||
getUser(u); // OK
|
||||
getUser(o); // Error: OrderId not assignable to UserId
|
||||
getUser('u_123'); // Error: string not assignable to UserId
|
||||
```
|
||||
|
||||
### Branded with validation (smart constructor)
|
||||
```ts
|
||||
type Email = Brand<string, 'Email'>;
|
||||
|
||||
function parseEmail(s: string): Email | null {
|
||||
return /^[^@]+@[^@]+\.[^@]+$/.test(s) ? (s as Email) : null;
|
||||
}
|
||||
|
||||
function sendMail(to: Email, subj: string) { /* ... */ }
|
||||
|
||||
const e = parseEmail('a@b.com');
|
||||
if (e) sendMail(e, 'Hi'); // 매 narrowed Email
|
||||
```
|
||||
|
||||
### Unit-of-measure brand
|
||||
```ts
|
||||
type Meter = Brand<number, 'Meter'>;
|
||||
type Second = Brand<number, 'Second'>;
|
||||
type MeterPerSecond = Brand<number, 'm/s'>;
|
||||
|
||||
function speed(d: Meter, t: Second): MeterPerSecond {
|
||||
return (d / t) as MeterPerSecond;
|
||||
}
|
||||
|
||||
const d = 100 as Meter;
|
||||
const t = 9.58 as Second;
|
||||
const v = speed(d, t);
|
||||
// speed(t, d) — Error: Second not assignable to Meter
|
||||
```
|
||||
|
||||
### Branded primitive with helper
|
||||
```ts
|
||||
function makeBrand<B extends string>() {
|
||||
return {
|
||||
of<T>(v: T): Brand<T, B> { return v as Brand<T, B>; },
|
||||
is(_v: unknown): _v is Brand<unknown, B> { return true; }
|
||||
};
|
||||
}
|
||||
|
||||
const UserId = makeBrand<'UserId'>();
|
||||
const OrderId = makeBrand<'OrderId'>();
|
||||
|
||||
const id = UserId.of('u_1');
|
||||
```
|
||||
|
||||
### Zod integration
|
||||
```ts
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserIdSchema = z.string().regex(/^u_/).brand<'UserId'>();
|
||||
type UserId = z.infer<typeof UserIdSchema>;
|
||||
|
||||
function fetchUser(id: UserId) { /* ... */ }
|
||||
|
||||
const parsed = UserIdSchema.parse(req.params.id);
|
||||
fetchUser(parsed);
|
||||
```
|
||||
|
||||
### Discriminated brand for state machine
|
||||
```ts
|
||||
type Pending = Brand<{ status: 'pending'; id: string }, 'Pending'>;
|
||||
type Approved = Brand<{ status: 'approved'; id: string; by: string }, 'Approved'>;
|
||||
type Rejected = Brand<{ status: 'rejected'; id: string; reason: string }, 'Rejected'>;
|
||||
type Order = Pending | Approved | Rejected;
|
||||
|
||||
function approve(o: Pending, by: string): Approved {
|
||||
return { ...o, status: 'approved', by } as Approved;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: opaque type with module
|
||||
```ts
|
||||
// userId.ts
|
||||
declare const brand: unique symbol;
|
||||
export type UserId = string & { readonly [brand]: 'UserId' };
|
||||
export const UserId = (s: string): UserId => {
|
||||
if (!s.startsWith('u_')) throw new Error('invalid');
|
||||
return s as UserId;
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| ID type collision risk | Brand 매 essential |
|
||||
| Internal-only domain | Brand optional |
|
||||
| Runtime validation needed | Smart constructor + brand |
|
||||
| Schema parsing (Zod/Effect) | `.brand<'Name'>()` |
|
||||
| Unit safety | Brand on number |
|
||||
|
||||
**기본값**: external boundary (DB, API) 에 entry point 에서 brand, internal logic은 branded type만 받음.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type System]]
|
||||
- 변형: [[Opaque Types (TypeScript)]]
|
||||
- Adjacent: [[Zod]] · [[Effect TS]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: domain model schema generation, brand utility scaffolding, smart constructor pattern.
|
||||
**언제 X**: runtime brand check 매 X (compile-only) — runtime 필요 시 class / Symbol.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Brand 추가 후 무한 cast**: `as UserId` 남발 → safety 매 lost. Smart constructor 사용.
|
||||
- **Brand on every type**: 매 friction 매 high — 매 boundary type 만.
|
||||
- **Brand with mutable object**: 매 객체 매 변형 후 brand mismatch — readonly 사용.
|
||||
- **Non-unique brand key**: `'id'` — collision risk. unique symbol 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Effect-TS docs, Zod 3.x, TypeScript handbook).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — branded type pattern + Zod integration |
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
id: wiki-2026-0508-bridgeless-new-architecture
|
||||
title: Bridgeless New Architecture
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Native New Architecture, Fabric+TurboModules]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react-native, architecture, jsi, fabric, turbomodules]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: react-native
|
||||
---
|
||||
|
||||
# Bridgeless New Architecture
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 async-bridge serialization 의 제거 — JS 와 native 의 direct memory call"**. 매 React Native 0.68 (Fabric, TurboModules) 부터 시작 → 0.74 default 로 출하된 architectural rewrite. 매 JSI (JavaScript Interface) 기반 synchronous interop 으로 60Hz/120Hz UI 동기화.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Old Bridge 의 한계
|
||||
- 매 모든 native↔JS 호출 의 JSON serialize → batched async queue.
|
||||
- 매 startup 에 module table 전체 의 eager registration.
|
||||
- 매 60fps 의 16.67ms budget 에 round-trip ≥ 2 frames 의 발생.
|
||||
- 매 frame-locked animation/gesture 의 jank.
|
||||
|
||||
### 매 New Architecture 의 3 pillars
|
||||
1. **JSI**: C++ host object 의 JS-runtime direct binding. Hermes/JSC interchangeable.
|
||||
2. **TurboModules**: native module 의 lazy load + sync invocation.
|
||||
3. **Fabric**: shadow tree 의 C++ rewrite — concurrent rendering + thread-safe layout.
|
||||
|
||||
### 매 Codegen 의 역할
|
||||
- 매 TS/Flow spec → C++/Java/ObjC scaffolding 의 자동 생성.
|
||||
- 매 type safety + boilerplate 의 elimination.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### TurboModule spec (TS)
|
||||
```typescript
|
||||
// NativeCalculator.ts
|
||||
import type { TurboModule } from 'react-native';
|
||||
import { TurboModuleRegistry } from 'react-native';
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
add(a: number, b: number): number; // sync!
|
||||
fetchUserAsync(id: string): Promise<{name: string}>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');
|
||||
```
|
||||
|
||||
### Fabric component spec
|
||||
```typescript
|
||||
// MyViewNativeComponent.ts
|
||||
import { codegenNativeComponent, ViewProps } from 'react-native';
|
||||
import type { Int32 } from 'react-native/Libraries/Types/CodegenTypes';
|
||||
|
||||
interface NativeProps extends ViewProps {
|
||||
intensity: Int32;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default codegenNativeComponent<NativeProps>('MyView');
|
||||
```
|
||||
|
||||
### JSI host function (C++)
|
||||
```cpp
|
||||
// 매 native side 에서 JS function 의 expose
|
||||
auto add = jsi::Function::createFromHostFunction(
|
||||
rt, jsi::PropNameID::forAscii(rt, "add"), 2,
|
||||
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t) {
|
||||
return jsi::Value(args[0].asNumber() + args[1].asNumber());
|
||||
});
|
||||
rt.global().setProperty(rt, "add", std::move(add));
|
||||
```
|
||||
|
||||
### Bridgeless mode 활성화
|
||||
```javascript
|
||||
// react-native.config.js
|
||||
module.exports = {
|
||||
project: {
|
||||
ios: {},
|
||||
android: {},
|
||||
},
|
||||
// 매 RN 0.74+ 부터 default true
|
||||
reactNativeArchitectures: 'arm64-v8a,x86_64',
|
||||
};
|
||||
```
|
||||
|
||||
```ruby
|
||||
# ios/Podfile
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:fabric_enabled => true,
|
||||
:new_arch_enabled => true,
|
||||
)
|
||||
```
|
||||
|
||||
### 매 sync layout measurement
|
||||
```typescript
|
||||
import { findNodeHandle, UIManager } from 'react-native';
|
||||
|
||||
// 매 Fabric 에서 sync measure 의 가능
|
||||
const handle = findNodeHandle(ref.current);
|
||||
UIManager.measureInWindow(handle, (x, y, w, h) => {
|
||||
// 매 동일 frame 안에 layout 완료 의 보장
|
||||
});
|
||||
```
|
||||
|
||||
### Reanimated 3 worklet (JSI 활용)
|
||||
```typescript
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||
|
||||
const offset = useSharedValue(0);
|
||||
const style = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: withSpring(offset.value) }],
|
||||
}));
|
||||
// 매 UI thread 에서 worklet 의 directly run — bridge 없음
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 신규 RN app (2026) | Bridgeless default — 매 그대로 사용 |
|
||||
| 매 legacy native module 다수 | Interop layer 의 활용 (자동 shim) |
|
||||
| 매 60Hz+ animation 필요 | Reanimated 3 + Fabric 의 mandatory |
|
||||
| 매 startup 시간 critical | TurboModule lazy init 의 활용 |
|
||||
|
||||
**기본값**: 매 RN 0.76+ 의 Bridgeless on by default — 매 비활성화 의 X.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React_Native]] · [[React_Native_New_Architecture]]
|
||||
- 변형: [[Fabric]] · [[Fabric_Renderer]] · [[TurboModules]] · [[JSI (JavaScript Interface)]]
|
||||
- 응용: [[Hermes]] · [[Hermes_Engine]] · [[Codegen]]
|
||||
- Adjacent: [[Cross-platform_Mobile_Development_Frameworks]] · [[Flutter]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 RN 0.74+ green-field project, 매 native↔JS round-trip 의 perf bottleneck 진단, 매 Reanimated/Skia/Gesture-Handler 의 깊은 통합.
|
||||
**언제 X**: 매 Expo Go (legacy bridge only) 환경, 매 unmaintained native module 에 의존하는 코드베이스.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 mixed arch leak**: 매 NEW_ARCH_ENABLED 의 절반만 활성화 → 매 일부 module 의 silent fallback.
|
||||
- **매 sync abuse**: 매 모든 호출 의 sync 화 → 매 JS thread 의 block.
|
||||
- **매 Codegen skip**: 매 manual binding 의 작성 — 매 type drift 의 발생.
|
||||
- **매 bridge pattern 재현**: 매 NativeEventEmitter 의 남용 — 매 JSI host object 의 selection.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React Native 공식 docs reactnative.dev/architecture, RN 0.76 release notes 2024-10).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Bridgeless 의 JSI/Fabric/TurboModule pillars 정리 |
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
id: wiki-2026-0508-buffer-allocation
|
||||
title: Buffer Allocation
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [ArrayBuffer Allocation, Typed Array Pool]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [performance, memory, buffer, typed-array, gc]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/TypeScript
|
||||
framework: Browser/Node/WebGPU
|
||||
---
|
||||
|
||||
# Buffer Allocation
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 buffer 매 reuse 매 GC 의 X"**. Buffer allocation은 binary data buffer (ArrayBuffer / Typed Array)의 effective lifecycle 관리 — naive `new Uint8Array(N)` per operation은 GC churn을 유발해 frame budget을 깬다. 2026 WebGPU / WebCodecs / canvas heavy app 의 must-skill.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 cost source
|
||||
- **Allocation**: V8/SpiderMonkey backing-store malloc + zero-fill.
|
||||
- **GC**: large allocation → Old-gen → expensive sweep.
|
||||
- **Cache miss**: 매 fresh memory 의 cold cache.
|
||||
- **Fragmentation**: many sizes → heap 의 fragmented.
|
||||
|
||||
### 매 strategy
|
||||
- **Pool / freelist**: 매 size class 매 reuse.
|
||||
- **Subarray view**: single big buffer + slice views.
|
||||
- **Pre-allocation**: peak size 부터 allocate.
|
||||
- **SharedArrayBuffer**: cross-thread, no copy.
|
||||
|
||||
### 매 응용
|
||||
1. Audio / video frame buffer.
|
||||
2. WebGL / WebGPU vertex / index buffer.
|
||||
3. WebSocket binary message parser.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Simple buffer pool
|
||||
```ts
|
||||
class BufferPool {
|
||||
private pool: Uint8Array[] = [];
|
||||
constructor(private size: number, private max = 32) {}
|
||||
|
||||
acquire(): Uint8Array {
|
||||
return this.pool.pop() ?? new Uint8Array(this.size);
|
||||
}
|
||||
|
||||
release(buf: Uint8Array) {
|
||||
if (this.pool.length < this.max) {
|
||||
buf.fill(0);
|
||||
this.pool.push(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pool = new BufferPool(4096);
|
||||
const b = pool.acquire();
|
||||
// use ...
|
||||
pool.release(b);
|
||||
```
|
||||
|
||||
### Size-class pool
|
||||
```ts
|
||||
class SizeClassPool {
|
||||
private classes = new Map<number, Uint8Array[]>();
|
||||
|
||||
private classOf(n: number): number {
|
||||
return Math.pow(2, Math.ceil(Math.log2(Math.max(n, 16))));
|
||||
}
|
||||
|
||||
acquire(n: number): Uint8Array {
|
||||
const cls = this.classOf(n);
|
||||
const list = this.classes.get(cls);
|
||||
return (list?.pop() ?? new Uint8Array(cls)).subarray(0, n);
|
||||
}
|
||||
|
||||
release(buf: Uint8Array) {
|
||||
const cls = buf.buffer.byteLength;
|
||||
if (!this.classes.has(cls)) this.classes.set(cls, []);
|
||||
this.classes.get(cls)!.push(new Uint8Array(buf.buffer));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Single backing buffer + views
|
||||
```ts
|
||||
const big = new ArrayBuffer(1024 * 1024); // 1MB
|
||||
const headers = new Uint32Array(big, 0, 256); // 1KB header
|
||||
const body = new Uint8Array(big, 1024, 1024 * 1023); // remainder
|
||||
```
|
||||
|
||||
### WebSocket binary parsing — no copy
|
||||
```ts
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onmessage = (e: MessageEvent<ArrayBuffer>) => {
|
||||
const dv = new DataView(e.data);
|
||||
const type = dv.getUint8(0);
|
||||
const length = dv.getUint32(1, true);
|
||||
const payload = new Uint8Array(e.data, 5, length);
|
||||
handle(type, payload);
|
||||
};
|
||||
```
|
||||
|
||||
### SharedArrayBuffer ring buffer (worker comms)
|
||||
```ts
|
||||
// main
|
||||
const sab = new SharedArrayBuffer(1024);
|
||||
const view = new Int32Array(sab);
|
||||
worker.postMessage(sab);
|
||||
|
||||
// worker — Atomics 매 lock-free synchronization
|
||||
self.onmessage = (e) => {
|
||||
const view = new Int32Array(e.data);
|
||||
Atomics.store(view, 0, 42);
|
||||
Atomics.notify(view, 0, 1);
|
||||
};
|
||||
```
|
||||
|
||||
### Reusable canvas pixel buffer
|
||||
```ts
|
||||
class FrameRecycler {
|
||||
private buffers: ImageData[] = [];
|
||||
acquire(w: number, h: number) {
|
||||
return this.buffers.find(b => b.width === w && b.height === h)
|
||||
?? new ImageData(w, h);
|
||||
}
|
||||
release(b: ImageData) {
|
||||
if (this.buffers.length < 4) this.buffers.push(b);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Detecting allocation hot spots
|
||||
```ts
|
||||
// 매 dev-only — 매 wrap allocator 매 trace
|
||||
const _orig = Uint8Array;
|
||||
let count = 0;
|
||||
(globalThis as any).Uint8Array = function (...args: any[]) {
|
||||
count++;
|
||||
if (count % 1000 === 0) console.warn('Uint8Array allocations:', count);
|
||||
return new _orig(...args);
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Per-frame temp buffer | pool with size class |
|
||||
| Streaming binary protocol | preallocated parser buffer |
|
||||
| Cross-thread share | SharedArrayBuffer + Atomics |
|
||||
| GPU upload | persistent GPU buffer + map/unmap |
|
||||
| Rare large alloc | direct allocate |
|
||||
|
||||
**기본값**: measure GC pressure first; pool only when allocator shows up in profile.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Memory Management]] · [[Performance]]
|
||||
- 변형: [[Object Pool]]
|
||||
- 응용: [[WebGPU]]
|
||||
- Adjacent: [[SharedArrayBuffer]] · [[TypedArray]] · [[Garbage Collection]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: pool implementation scaffold, view layout calculation, ring buffer design.
|
||||
**언제 X**: 매 GC actual measurement — Chrome Memory profiler.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Premature pool**: 매 micro-opt — measure first.
|
||||
- **Pool 무제한**: 매 leak — max size cap.
|
||||
- **Subarray after detach**: transferred buffer — invalid.
|
||||
- **Concurrent writes without Atomics**: SharedArrayBuffer 매 race.
|
||||
- **Forget to zero-fill on release**: 매 stale data leak across owners.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (V8 blog, MDN ArrayBuffer, WebGPU spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — buffer pool + view layout pattern |
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
id: wiki-2026-0508-bundle-size-optimization
|
||||
title: Bundle Size Optimization
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [JS Bundle Optimization, Web Bundle Reduction]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [bundle, performance, webpack, vite, tree-shaking]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/TypeScript
|
||||
framework: Vite/Rollup/Webpack
|
||||
---
|
||||
|
||||
# Bundle Size Optimization
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 byte 매 less 매 user time 매 less"**. Bundle size optimization은 production JS/CSS payload를 줄여 LCP/INP/TBT 개선 + mobile-first user 의 perceived speed 개선. 2026 standard tooling: Vite + Rollup tree-shaking, modern bundle analysis (Bundle Buddy, esbuild-visualizer), bundle budgets enforcement.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4 lever
|
||||
- **Tree shaking**: ESM only, sideEffects:false, no re-export wildcards.
|
||||
- **Code splitting**: route / component lazy import.
|
||||
- **Compression**: brotli > gzip; precompress at build.
|
||||
- **Dependency surgery**: heavy lib → lighter alt or self-implement.
|
||||
|
||||
### 매 측정 우선
|
||||
- Bundle visualizer (rollup-plugin-visualizer, source-map-explorer).
|
||||
- Bundle budget in CI (e.g., size-limit, bundlesize).
|
||||
- Real device testing (slow 3G profile).
|
||||
|
||||
### 매 응용
|
||||
1. Lazy-load route chunks.
|
||||
2. Remove unused locales (date-fns, moment).
|
||||
3. Replace lodash with native / lodash-es.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Vite + visualizer
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
visualizer({ filename: 'stats.html', gzipSize: true, brotliSize: true })
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
react: ['react', 'react-dom'],
|
||||
vendor: ['date-fns', 'zustand']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Route-level code split (React)
|
||||
```tsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
const Dashboard = lazy(() => import('./Dashboard'));
|
||||
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Dashboard />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### Dynamic import for rare path
|
||||
```ts
|
||||
async function exportToPDF(data: Item[]) {
|
||||
const { jsPDF } = await import('jspdf');
|
||||
const doc = new jsPDF();
|
||||
doc.text(JSON.stringify(data), 10, 10);
|
||||
doc.save('out.pdf');
|
||||
}
|
||||
```
|
||||
|
||||
### Replace heavy lib
|
||||
```ts
|
||||
// X moment (~290KB)
|
||||
import moment from 'moment';
|
||||
moment().format('YYYY-MM-DD');
|
||||
|
||||
// O Intl (built-in, 0KB)
|
||||
new Intl.DateTimeFormat('en-CA').format(new Date());
|
||||
|
||||
// O date-fns/format (tree-shakeable, ~3KB)
|
||||
import { format } from 'date-fns/format';
|
||||
format(new Date(), 'yyyy-MM-dd');
|
||||
```
|
||||
|
||||
### sideEffects flag
|
||||
```json
|
||||
// package.json — library author 측
|
||||
{
|
||||
"name": "my-lib",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### size-limit budget enforcement
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"size": "size-limit"
|
||||
},
|
||||
"size-limit": [
|
||||
{ "path": "dist/index.js", "limit": "50 KB" },
|
||||
{ "path": "dist/vendor.js", "limit": "120 KB" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compression at build (brotli)
|
||||
```ts
|
||||
import compression from 'vite-plugin-compression2';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
compression({ algorithm: 'brotliCompress', exclude: [/\.(br)$/, /\.(gz)$/] })
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Server: strip locales from dayjs
|
||||
```ts
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/ko'; // 매 필요한 것만
|
||||
dayjs.locale('ko');
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Initial bundle > 200KB | route split + lazy load |
|
||||
| Single heavy lib | replace 또는 dynamic import |
|
||||
| Multi-tenant build | per-tenant treeshake config |
|
||||
| Library publish | ESM + `sideEffects:false` |
|
||||
| Edge runtime | bundle ≤ 1MB 가까이 strict budget |
|
||||
|
||||
**기본값**: measure first → split → compress → swap heavy deps.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Frontend Performance]]
|
||||
- 변형: [[Code Splitting]]
|
||||
- 응용: [[Core Web Vitals Optimization (INP, LCP, CLS)|Core Web Vitals]] · [[LCP]]
|
||||
- Adjacent: [[Vite]] · [[Rollup]] · [[esbuild]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: webpack/vite config audit, lib alternative suggestion, bundle analyzer interpretation.
|
||||
**언제 X**: 매 production 매 swap deploy — actual measurement 필수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **CommonJS lib import**: tree shaking blocked — ESM 사용.
|
||||
- **`import * as foo`**: bundler 매 mark 매 모든 export used.
|
||||
- **Polyfill 전체**: target browser baseline + browserslist으로 narrow.
|
||||
- **Single chunk all**: SPA → 매 long initial — split per route.
|
||||
- **Dev source maps in prod**: ship source map only via separate URL or skip.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev bundle size guide, Vite docs, size-limit GitHub).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — bundle optim 4 lever + Vite/size-limit pattern |
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
id: wiki-2026-0508-cpu-overhead
|
||||
title: CPU Overhead
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [CPU Cost, JS Main Thread Cost]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [performance, cpu, main-thread, profiling]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: Browser/Node
|
||||
---
|
||||
|
||||
# CPU Overhead
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 main thread 매 free 매 fast UI"**. CPU overhead는 JS execution / parsing / hydration / re-render에 소비되는 main-thread time — 이게 길어지면 INP가 무너지고 user input이 lag한다. 2026 INP가 LCP를 대체한 third Core Web Vital이 되어 CPU profile + scheduling이 frontend 의 first concern.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 source
|
||||
- **Parse + compile**: download된 JS bytes → AST → bytecode (V8 측정 시 KB 당 ~1ms low-end mobile).
|
||||
- **Hydration**: SSR HTML 위 React/Vue 매 attach.
|
||||
- **Re-render**: state change → diff → DOM commit.
|
||||
- **Long task** (>50ms): block input.
|
||||
|
||||
### 매 측정
|
||||
- Chrome Performance panel — flame chart, "Long tasks".
|
||||
- `PerformanceObserver` API on `longtask`.
|
||||
- React Profiler / Vue Devtools timeline.
|
||||
- Web Vitals — INP, TBT.
|
||||
|
||||
### 매 응용
|
||||
1. JS payload 줄이기 → less parse.
|
||||
2. Hydration partial / streaming.
|
||||
3. Heavy work → Web Worker / requestIdleCallback / startTransition.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Long task observer
|
||||
```ts
|
||||
const obs = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > 50) {
|
||||
console.warn(`Long task: ${entry.duration.toFixed(0)}ms`, entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
obs.observe({ type: 'longtask', buffered: true });
|
||||
```
|
||||
|
||||
### Yield to main thread
|
||||
```ts
|
||||
function yieldToMain() {
|
||||
return new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
async function processChunks(items: Item[]) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
process(items[i]);
|
||||
if (i % 100 === 0) await yieldToMain();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### scheduler.yield (Chrome 129+)
|
||||
```ts
|
||||
async function processBig(items: Item[]) {
|
||||
for (const item of items) {
|
||||
process(item);
|
||||
if ('scheduler' in window && 'yield' in (window as any).scheduler) {
|
||||
await (window as any).scheduler.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Web Worker offload
|
||||
```ts
|
||||
// worker.ts
|
||||
self.onmessage = (e) => {
|
||||
const result = heavyTransform(e.data);
|
||||
self.postMessage(result);
|
||||
};
|
||||
|
||||
// main.ts
|
||||
const w = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
||||
w.postMessage(largeData);
|
||||
w.onmessage = (e) => render(e.data);
|
||||
```
|
||||
|
||||
### React startTransition
|
||||
```tsx
|
||||
import { startTransition, useState } from 'react';
|
||||
|
||||
function Search() {
|
||||
const [q, setQ] = useState('');
|
||||
const [results, setResults] = useState<Item[]>([]);
|
||||
|
||||
function onChange(e) {
|
||||
setQ(e.target.value); // urgent
|
||||
startTransition(() => {
|
||||
setResults(filter(allItems, e.target.value)); // background
|
||||
});
|
||||
}
|
||||
return <input value={q} onChange={onChange} />;
|
||||
}
|
||||
```
|
||||
|
||||
### useDeferredValue
|
||||
```tsx
|
||||
function Page({ filter }) {
|
||||
const deferredFilter = useDeferredValue(filter);
|
||||
const items = useMemo(() => filterBig(deferredFilter), [deferredFilter]);
|
||||
return <List items={items} />;
|
||||
}
|
||||
```
|
||||
|
||||
### requestIdleCallback for non-critical
|
||||
```ts
|
||||
const work = [...];
|
||||
function schedule() {
|
||||
if (!work.length) return;
|
||||
requestIdleCallback((deadline) => {
|
||||
while (work.length && deadline.timeRemaining() > 0) {
|
||||
doOne(work.shift());
|
||||
}
|
||||
schedule();
|
||||
});
|
||||
}
|
||||
schedule();
|
||||
```
|
||||
|
||||
### Avoid layout thrash
|
||||
```ts
|
||||
// X — 매 read after write 매 force reflow
|
||||
els.forEach(el => {
|
||||
el.style.width = '100px';
|
||||
console.log(el.offsetWidth); // forced sync layout
|
||||
});
|
||||
|
||||
// O — 매 batch read, batch write
|
||||
const widths = els.map(el => el.offsetWidth);
|
||||
els.forEach((el, i) => el.style.width = widths[i] + 1 + 'px');
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Heavy compute (parse/transform) | Web Worker |
|
||||
| Long list render | virtualization (TanStack Virtual) |
|
||||
| Filter on input | useDeferredValue / startTransition |
|
||||
| Background prefetch | requestIdleCallback |
|
||||
| Animation | CSS / RAF, no JS-driven layout |
|
||||
|
||||
**기본값**: measure → smallest fix → re-measure.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Frontend Performance]] · [[Core Web Vitals Optimization (INP, LCP, CLS)|Core Web Vitals]]
|
||||
- 변형: [[INP]]
|
||||
- 응용: [[Web Worker]] · [[Concurrent Features|Concurrent Rendering]]
|
||||
- Adjacent: [[Bundle Size Optimization]] · [[Hydration]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: long task identification, scheduler API generation, INP debug script.
|
||||
**언제 X**: real device profiling — DevTools / WebPageTest 필수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **JS-driven animation**: setInterval + style 변경 — RAF / CSS 사용.
|
||||
- **Sync XMLHttpRequest**: 매 main block — fetch async 사용.
|
||||
- **Force layout in loop**: read after write — batch.
|
||||
- **Hydration of static page**: islands / partial hydration.
|
||||
- **Massive context provider**: 매 모든 child re-render — split context.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev INP guide, Chrome scheduler API, React 19 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — CPU overhead pattern + scheduler API |
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
id: wiki-2026-0508-css-grid
|
||||
title: CSS Grid
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Grid Layout, CSS Grid Layout]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, layout, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: css
|
||||
framework: none
|
||||
---
|
||||
|
||||
# CSS Grid
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 2D layout 의 native primitive"**. CSS Grid 매 row + column 동시 control 의 layout system — 매 Flexbox 의 1D 보완. 매 2026 현재 매 모든 modern browser 매 stable, 매 `subgrid` + `masonry` (experimental) 까지 지원, 매 page-level 부터 component-level 까지 default tool.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 모델
|
||||
- **Grid container** (`display: grid`) — 매 parent.
|
||||
- **Grid items** — 매 direct children, 매 line-based positioning.
|
||||
- **Tracks** — rows + columns, 매 `fr` unit (fractional space) + `minmax()` + `auto`.
|
||||
- **Areas** — 매 named regions, 매 semantic layout description.
|
||||
|
||||
### 매 vs Flexbox
|
||||
- **Grid**: 매 2D (row + column 동시), 매 layout-first (parent decides).
|
||||
- **Flexbox**: 매 1D (single axis), 매 content-first (children sizing).
|
||||
- 매 함께 mix — 매 outer Grid + inner Flex 매 common.
|
||||
|
||||
### 매 응용
|
||||
1. Page layout (header / sidebar / main / footer).
|
||||
2. Card grid 매 responsive (`auto-fill` + `minmax`).
|
||||
3. Dashboard 매 named-area complex layout.
|
||||
4. Image gallery 매 masonry.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Holy Grail layout
|
||||
```css
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 200px;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"nav main aside"
|
||||
"footer footer footer";
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header { grid-area: header; }
|
||||
.nav { grid-area: nav; }
|
||||
.main { grid-area: main; }
|
||||
.aside { grid-area: aside; }
|
||||
.footer { grid-area: footer; }
|
||||
```
|
||||
|
||||
### Responsive card grid (`auto-fill`)
|
||||
```css
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
### Subgrid (nested alignment)
|
||||
```css
|
||||
.outer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-rows: subgrid; /* inherit parent row tracks */
|
||||
grid-row: span 3;
|
||||
}
|
||||
```
|
||||
|
||||
### Line-based placement
|
||||
```css
|
||||
.item {
|
||||
grid-column: 2 / span 3; /* col 2 to 4 */
|
||||
grid-row: 1 / -1; /* full height */
|
||||
}
|
||||
```
|
||||
|
||||
### Masonry (2026 experimental)
|
||||
```css
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-template-rows: masonry; /* Firefox + Safari TP */
|
||||
gap: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
### Implicit grid + `dense` packing
|
||||
```css
|
||||
.feed {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-flow: dense; /* fill holes */
|
||||
grid-auto-rows: 100px;
|
||||
}
|
||||
.feed > .featured {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Page-level 2D layout | **Grid** |
|
||||
| Component 매 1D row/column | Flex |
|
||||
| Aligned nested grids | **Grid + subgrid** |
|
||||
| Dynamic content count, fluid wrap | Grid `auto-fill` |
|
||||
| Dense irregular packing | Grid `auto-flow: dense` |
|
||||
|
||||
**기본값**: 매 layout-first 결정 — Grid. 매 content-first sizing — Flex.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Layout]]
|
||||
- 변형: [[Flexbox]] · [[Subgrid]] · [[컨테이너 쿼리 (Container Queries)|Container Queries]]
|
||||
- 응용: [[Responsive Design]]
|
||||
- Adjacent: [[CSS Grid 및 Flexbox]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 2D layout, named areas semantic intent, responsive 매 `auto-fill`.
|
||||
**언제 X**: 1D 매 simple row of buttons — Flexbox 가 더 ergonomic.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모두 1D 에 Grid 매 사용**: 매 Flex 보다 verbose.
|
||||
- **Pixel-only tracks**: 매 `fr` + `minmax` 매 사용 — 매 fluid.
|
||||
- **`!important` 으로 children 매 overriding**: 매 grid-area 매 misaligned signal.
|
||||
- **Subgrid 매 ignore**: nested grids 매 line-up 가 필요할 때 매 `subgrid` 가 정답.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (MDN CSS Grid Layout 2026, CSS Working Group Level 3).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Grid 2D layout + subgrid + masonry + responsive patterns |
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: wiki-2026-0508-css-media-queries
|
||||
title: CSS Media Queries
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Media Query, "@media"]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, responsive, media-query, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: CSS
|
||||
framework: none
|
||||
---
|
||||
|
||||
# CSS Media Queries
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 viewport / user preference 의 conditional CSS"**. Media Queries 는 device width, color scheme, motion preference 등 의 condition 의 styling 의 적용. 2026 Container Queries 의 출현 에도 viewport-level adaptation 의 표준.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 query types
|
||||
- **Width**: `min-width`, `max-width` — viewport 크기
|
||||
- **Orientation**: `portrait`, `landscape`
|
||||
- **User preference**: `prefers-color-scheme`, `prefers-reduced-motion`, `prefers-contrast`
|
||||
- **Resolution**: `min-resolution: 2dppx` (Retina)
|
||||
- **Hover capability**: `hover: hover` vs `hover: none`
|
||||
- **Pointer**: `pointer: fine` vs `coarse`
|
||||
|
||||
### 매 Range syntax (2026 표준)
|
||||
```css
|
||||
@media (width >= 768px) { ... }
|
||||
@media (400px <= width <= 800px) { ... }
|
||||
```
|
||||
|
||||
### 매 Mobile-first vs Desktop-first
|
||||
- **Mobile-first** (권장): base = mobile, `min-width` 의 add — 매 cascade 친화적
|
||||
- **Desktop-first**: base = desktop, `max-width` 의 override — 매 legacy
|
||||
|
||||
### 매 응용
|
||||
1. Responsive breakpoint.
|
||||
2. Dark mode (`prefers-color-scheme`).
|
||||
3. Accessibility (`prefers-reduced-motion`).
|
||||
4. Print stylesheet (`@media print`).
|
||||
5. High-DPI image (Retina).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Mobile-first breakpoints
|
||||
```css
|
||||
/* base: mobile */
|
||||
.card { padding: 1rem; font-size: 14px; }
|
||||
|
||||
@media (width >= 640px) { .card { padding: 1.5rem; } }
|
||||
@media (width >= 1024px) { .card { font-size: 16px; } }
|
||||
@media (width >= 1440px) { .card { padding: 2rem; } }
|
||||
```
|
||||
|
||||
### Dark mode
|
||||
```css
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--fg: #111;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--fg: #f5f5f5;
|
||||
}
|
||||
}
|
||||
body { background: var(--bg); color: var(--fg); }
|
||||
```
|
||||
|
||||
### Reduced motion (a11y)
|
||||
```css
|
||||
.fade { transition: opacity 0.4s ease; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fade { transition: none; }
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hover-only enhancements
|
||||
```css
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.btn:hover { background: #eee; transform: scale(1.02); }
|
||||
}
|
||||
```
|
||||
|
||||
### Print stylesheet
|
||||
```css
|
||||
@media print {
|
||||
nav, .ads, .comments { display: none; }
|
||||
body { font-size: 12pt; color: #000; background: #fff; }
|
||||
a::after { content: " (" attr(href) ")"; }
|
||||
}
|
||||
```
|
||||
|
||||
### JS 의 matchMedia
|
||||
```js
|
||||
const dark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
dark.addEventListener('change', (e) => {
|
||||
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
|
||||
});
|
||||
```
|
||||
|
||||
### Container Query (2026 표준, MQ 와 의 보완)
|
||||
```css
|
||||
.card-host { container-type: inline-size; }
|
||||
|
||||
@container (width >= 400px) {
|
||||
.card { display: grid; grid-template-columns: 120px 1fr; }
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Viewport-level layout | Media Query |
|
||||
| Component-level adaptation | Container Query |
|
||||
| 사용자 preference (dark/motion) | `prefers-*` MQ |
|
||||
| JS 의 query check | `matchMedia` |
|
||||
| Print styling | `@media print` |
|
||||
|
||||
**기본값**: mobile-first + range syntax (`width >=`).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Responsive Design]]
|
||||
- 변형: [[컨테이너 쿼리 (Container Queries)|Container Queries]]
|
||||
- 응용: [[Dark Mode]] · [[Accessibility (A11y)|Accessibility]]
|
||||
- Adjacent: [[CSS Grid]] · [[Flexbox]] · [[CSS_Architecture_and_Styling|CSS Variables]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: responsive breakpoint, dark mode, a11y motion handling 의 generation.
|
||||
**언제 X**: component-level adaptation (Container Query 의 사용).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Device-targeted MQ**: `iPhone`, `iPad` 의 device sniff — 매 width-based 의 사용.
|
||||
- **너무 many breakpoints**: 5개 이상 의 maintenance hell — 매 3-4개 의 기본.
|
||||
- **`prefers-reduced-motion` 의 무시**: a11y 위반.
|
||||
- **`max-width` desktop-first**: cascade 의 복잡 — 매 mobile-first.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (MDN Media Queries, CSS Media Queries Level 4/5).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — MQ patterns + range syntax + container queries |
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
id: wiki-2026-0508-css-modules
|
||||
title: CSS Modules
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [CSS Module, Locally-Scoped CSS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, frontend, scoping, build-tool]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: css
|
||||
framework: webpack/vite
|
||||
---
|
||||
|
||||
# CSS Modules
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 class 가 file-locally scoped"**. CSS Modules 매 build-time transform 으로 매 class name 을 unique hash 로 rewriting — 매 global namespace pollution 의 elimination + component-level encapsulation 의 enable. 매 2026 현재 Vite/Webpack/Next.js 매 native support, 매 CSS-in-JS runtime cost 의 alternative 로 주류.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 작동 원리
|
||||
- `Button.module.css` 매 import 시 매 bundler 가 매 class 를 `Button_primary__a3fG2` 로 rename.
|
||||
- 매 import 결과 매 object — `{ primary: 'Button_primary__a3fG2' }`.
|
||||
- 매 component 매 `styles.primary` 로 reference — 매 collision-free.
|
||||
|
||||
### 매 vs alternatives
|
||||
- **vs global CSS**: 매 scoping 자동, 매 BEM 매 manual convention 의 replacement.
|
||||
- **vs CSS-in-JS**: 매 zero runtime, 매 build-time only — 매 bundle size + perf 우위.
|
||||
- **vs Tailwind**: 매 component-local custom design 매 적합, Tailwind 매 utility-first.
|
||||
|
||||
### 매 응용
|
||||
1. Component library (Button, Input) 매 encapsulated styling.
|
||||
2. Next.js 매 default-supported pattern — `*.module.css` 매 convention.
|
||||
3. Design system 매 token + component 매 layered structure.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic usage
|
||||
```css
|
||||
/* Button.module.css */
|
||||
.primary {
|
||||
background: #0070f3;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Button.tsx
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export function Button({ disabled, children }: Props) {
|
||||
return (
|
||||
<button
|
||||
className={`${styles.primary} ${disabled ? styles.disabled : ''}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Composition (`composes`)
|
||||
```css
|
||||
/* base.module.css */
|
||||
.button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Button.module.css */
|
||||
.primary {
|
||||
composes: button from './base.module.css';
|
||||
background: #0070f3;
|
||||
}
|
||||
```
|
||||
|
||||
### `clsx` 와 conditional classes
|
||||
```tsx
|
||||
import clsx from 'clsx';
|
||||
import styles from './Card.module.css';
|
||||
|
||||
export function Card({ variant, active }: Props) {
|
||||
return (
|
||||
<div className={clsx(styles.card, styles[variant], active && styles.active)}>
|
||||
...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript typed module
|
||||
```ts
|
||||
// Button.module.css.d.ts (auto-generated by typescript-plugin-css-modules)
|
||||
declare const styles: {
|
||||
readonly primary: string;
|
||||
readonly disabled: string;
|
||||
};
|
||||
export default styles;
|
||||
```
|
||||
|
||||
### `:global` escape hatch
|
||||
```css
|
||||
/* Layout.module.css */
|
||||
.root :global(.markdown) h1 {
|
||||
/* unscoped — for third-party HTML */
|
||||
font-size: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
### Vite config
|
||||
```ts
|
||||
// vite.config.ts
|
||||
export default {
|
||||
css: {
|
||||
modules: {
|
||||
localsConvention: 'camelCaseOnly',
|
||||
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Component-scoped styling, zero runtime | **CSS Modules** |
|
||||
| Dynamic styles (props-driven) | CSS-in-JS (vanilla-extract, styled-components) |
|
||||
| Utility-first, rapid prototyping | Tailwind |
|
||||
| Server Components 매 styling | CSS Modules (Tailwind 도 OK) |
|
||||
|
||||
**기본값**: Next.js / Vite 매 component-level styling 의 first choice 로 **CSS Modules**.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[CSS_Architecture_and_Styling|CSS-in-JS]] · [[CSS_Architecture_and_Styling|Tailwind CSS]] · [[BEM]]
|
||||
- 응용: [[Next.js]] · [[Vite]] · [[React]]
|
||||
- Adjacent: [[Vanilla-Extract]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: component encapsulation 매 필요, runtime cost 의 회피, TypeScript-friendly typing.
|
||||
**언제 X**: 매 dynamic theming 매 heavy (variant explosion), 매 design token 매 runtime mutation 매 필요 — vanilla-extract / CSS variables.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`:global` 매 남용**: 매 scoping benefit 의 nullification.
|
||||
- **String concatenation 매 raw**: 매 `clsx` 의 사용 — 매 readability + falsy handling.
|
||||
- **`styles['kebab-case']` access 매 unnecessarily**: 매 `localsConvention: 'camelCaseOnly'` 매 설정.
|
||||
- **Module 1개 매 100+ classes**: 매 split — 매 component-per-module.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Next.js docs 2026, Vite CSS Modules guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — CSS Modules build-time scoping + composition + Vite/Next 통합 |
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
id: wiki-2026-0508-cssom
|
||||
title: CSSOM
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [CSS Object Model, "CSSOM(CSS Object Model)"]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, dom, browser, frontend, render]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: none
|
||||
---
|
||||
|
||||
# CSSOM
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CSS 의 JS-accessible object tree"**. CSSOM (CSS Object Model) 은 browser 가 parsed CSS 의 representation 의 object 의 노출 의 API. DOM 의 sibling 의 매 render tree 의 input. 2026 modern browser 의 매 `getComputedStyle`, CSS Typed OM (Houdini) 의 안정 사용.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 CSSOM 구조
|
||||
- `document.styleSheets` — `StyleSheetList`
|
||||
- 각 `CSSStyleSheet` → `cssRules` (CSSRule list)
|
||||
- `CSSStyleRule` → `selectorText`, `style` (CSSStyleDeclaration)
|
||||
- `element.style` — inline style 의 declaration
|
||||
- `getComputedStyle(el)` — cascaded + inherited final value
|
||||
|
||||
### 매 Render pipeline 의 위치
|
||||
1. HTML → DOM tree
|
||||
2. CSS → CSSOM tree
|
||||
3. **DOM + CSSOM → Render tree**
|
||||
4. Layout (reflow) → Paint → Composite
|
||||
|
||||
### 매 CSSOM 조작 의 cost
|
||||
- `style` 읽기/쓰기: 매 cheap (inline 만)
|
||||
- `getComputedStyle`: 매 expensive (force layout)
|
||||
- `cssRules` 의 mutation: 매 sheet 전체 invalidation
|
||||
|
||||
### 매 응용
|
||||
1. Theme switching (CSS variable 의 JS 조작).
|
||||
2. Animation (Web Animations API).
|
||||
3. Style introspection (DevTools, testing).
|
||||
4. Houdini (CSS Typed OM, Paint Worklet).
|
||||
5. Stylesheet 의 동적 generation.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### CSS Variable 의 JS 조작
|
||||
```js
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--accent', '#0070f3');
|
||||
|
||||
// 읽기
|
||||
const accent = getComputedStyle(root)
|
||||
.getPropertyValue('--accent')
|
||||
.trim();
|
||||
```
|
||||
|
||||
### getComputedStyle (final value)
|
||||
```js
|
||||
const el = document.querySelector('.card');
|
||||
const styles = getComputedStyle(el);
|
||||
console.log(styles.fontSize); // "16px"
|
||||
console.log(styles.color); // "rgb(17, 17, 17)"
|
||||
console.log(styles.gridTemplateColumns); // resolved tracks
|
||||
```
|
||||
|
||||
### Inline style 직접
|
||||
```js
|
||||
el.style.transform = 'translateX(100px)';
|
||||
el.style.setProperty('--x', '100px');
|
||||
```
|
||||
|
||||
### CSSStyleSheet API (constructable)
|
||||
```js
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(`
|
||||
.toast { background: #111; color: #fff; padding: 1rem; }
|
||||
`);
|
||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
||||
```
|
||||
|
||||
### CSS Typed OM (Houdini)
|
||||
```js
|
||||
const el = document.querySelector('.box');
|
||||
el.attributeStyleMap.set('width', CSS.px(200));
|
||||
const w = el.computedStyleMap().get('width');
|
||||
console.log(w.value, w.unit); // 200, "px"
|
||||
```
|
||||
|
||||
### Stylesheet 의 inspection
|
||||
```js
|
||||
for (const sheet of document.styleSheets) {
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
if (rule.type === CSSRule.STYLE_RULE) {
|
||||
console.log(rule.selectorText, rule.style.cssText);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// CORS-blocked external sheet
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Web Animations API (CSSOM 기반)
|
||||
```js
|
||||
el.animate(
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 300, easing: 'ease-out', fill: 'forwards' }
|
||||
);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Final computed value 필요 | `getComputedStyle` |
|
||||
| CSS variable toggle | `style.setProperty('--x', ...)` |
|
||||
| Dynamic stylesheet | `CSSStyleSheet` + `adoptedStyleSheets` |
|
||||
| Type-safe value | CSS Typed OM (`CSS.px()`) |
|
||||
| 1회 animation | Web Animations API |
|
||||
| 큰 DOM 의 style read | batch — RAF 의 sync |
|
||||
|
||||
**기본값**: CSS variable + `setProperty` (가장 cheap + composable).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[DOM]] · [[Browser Rendering]]
|
||||
- 응용: [[Theme Switching]] · [[Web Animations API]]
|
||||
- Adjacent: [[Render Tree]] · [[Critical Rendering Path (CRP)|Critical Rendering Path]] · [[CSS_Architecture_and_Styling|CSS Variables]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: theme system, dynamic styling, JS-driven animation 코드 generation.
|
||||
**언제 X**: 매 declarative CSS 의 sufficient — 매 JS 의 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Layout thrashing**: read → write → read pattern — 매 batch read first.
|
||||
- **`style.cssText` 의 overwrite**: 기존 style 의 destroy — 매 individual property.
|
||||
- **`!important` 의 JS 의 abuse**: cascade 의 deadlock.
|
||||
- **모든 frame 의 `getComputedStyle`**: layout force — RAF 의 sync.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (W3C CSSOM spec, MDN CSSOM, Houdini).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — CSSOM API + Typed OM + WAAPI |
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
id: wiki-2026-0508-cache-miss-rates
|
||||
title: Cache Miss Rates
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Cache Miss Rate, Cache Hit Rate, Cache Efficiency]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [performance, cache, profiling, cpu, memory]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: agnostic
|
||||
framework: agnostic
|
||||
---
|
||||
|
||||
# Cache Miss Rates
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 cache lookup 의 fail (data 의 cache 외 fetch) ratio"**. CPU L1/L2/L3 cache, browser HTTP cache, application-level memoization, CDN edge cache 의 universal metric — `miss / (hit + miss)`. 2026 perspective: Apple Silicon M4 의 huge L2 (16MB+), 매 Cloudflare/Fastly tiered cache, 매 LLM prompt-cache (Anthropic) 의 cost-driven optimization.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 hierarchy (CPU)
|
||||
- **L1**: 32-128KB / core, ~1-4 cycles.
|
||||
- **L2**: 256KB-2MB / core, ~10-15 cycles.
|
||||
- **L3**: 8-128MB shared, ~40 cycles.
|
||||
- **DRAM**: >100ns, ~200-300 cycles. 매 miss 의 huge cost.
|
||||
|
||||
### 매 miss types (3C)
|
||||
- **Compulsory** (cold): 매 first access.
|
||||
- **Capacity**: 매 working set > cache size.
|
||||
- **Conflict**: 매 set-associative collision.
|
||||
- (+) **Coherence**: multi-core invalidation.
|
||||
|
||||
### 매 measurement
|
||||
- **CPU**: `perf stat -e cache-misses,cache-references` (Linux).
|
||||
- **HTTP**: `cf-cache-status: HIT/MISS` headers.
|
||||
- **App**: hit/miss counter on cache wrapper.
|
||||
|
||||
### 매 응용
|
||||
1. 매 hot loop 의 data layout (SoA vs AoS) tuning.
|
||||
2. 매 CDN cache key strategy.
|
||||
3. 매 LLM prompt-cache 의 prefix stability.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Pattern 1 — 매 cache-friendly layout (SoA)
|
||||
```ts
|
||||
// 매 BAD (AoS) — 매 padding / strided access
|
||||
const particles = [{ x: 0, y: 0, vx: 1, vy: 1 }, /* ... */];
|
||||
|
||||
// 매 GOOD (SoA) — 매 sequential, cache-line dense
|
||||
const xs = new Float32Array(N), ys = new Float32Array(N);
|
||||
const vxs = new Float32Array(N), vys = new Float32Array(N);
|
||||
for (let i = 0; i < N; i++) {
|
||||
xs[i] += vxs[i];
|
||||
ys[i] += vys[i];
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2 — 매 LRU cache (memoization)
|
||||
```ts
|
||||
class LRU<K, V> {
|
||||
private map = new Map<K, V>();
|
||||
constructor(private capacity: number) {}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
if (!this.map.has(key)) return;
|
||||
const v = this.map.get(key)!;
|
||||
this.map.delete(key);
|
||||
this.map.set(key, v); // refresh
|
||||
return v;
|
||||
}
|
||||
set(key: K, val: V) {
|
||||
if (this.map.has(key)) this.map.delete(key);
|
||||
else if (this.map.size >= this.capacity) {
|
||||
this.map.delete(this.map.keys().next().value!);
|
||||
}
|
||||
this.map.set(key, val);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3 — 매 hit rate metric
|
||||
```ts
|
||||
class CountingCache<K, V> {
|
||||
private hits = 0;
|
||||
private misses = 0;
|
||||
constructor(private inner: Map<K, V>, private load: (k: K) => V) {}
|
||||
|
||||
get(k: K): V {
|
||||
if (this.inner.has(k)) { this.hits++; return this.inner.get(k)!; }
|
||||
this.misses++;
|
||||
const v = this.load(k);
|
||||
this.inner.set(k, v);
|
||||
return v;
|
||||
}
|
||||
hitRate(): number { return this.hits / (this.hits + this.misses || 1); }
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4 — 매 HTTP cache header
|
||||
```http
|
||||
GET /api/posts/123
|
||||
Cache-Control: public, max-age=300, stale-while-revalidate=86400
|
||||
ETag: "abc123"
|
||||
|
||||
# CDN response
|
||||
cf-cache-status: HIT
|
||||
age: 42
|
||||
```
|
||||
|
||||
### Pattern 5 — 매 stable key (CDN)
|
||||
```ts
|
||||
// 매 BAD — query order varies
|
||||
fetch(`/api/list?b=2&a=1`);
|
||||
fetch(`/api/list?a=1&b=2`); // 매 different cache key
|
||||
|
||||
// 매 GOOD — canonical
|
||||
const params = new URLSearchParams();
|
||||
[...Object.entries(args)].sort().forEach(([k, v]) => params.set(k, v));
|
||||
fetch(`/api/list?${params}`);
|
||||
```
|
||||
|
||||
### Pattern 6 — 매 LLM prompt cache (Anthropic)
|
||||
```python
|
||||
# 매 stable system prefix → cache hit (90% cost reduction)
|
||||
client.messages.create(
|
||||
model="claude-opus-4-7",
|
||||
system=[
|
||||
{"type": "text", "text": LARGE_STABLE_PREFIX,
|
||||
"cache_control": {"type": "ephemeral"}},
|
||||
],
|
||||
messages=[{"role": "user", "content": query}],
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 7 — 매 CPU perf measure
|
||||
```bash
|
||||
perf stat -e cache-references,cache-misses,L1-dcache-load-misses ./app
|
||||
# 매 miss rate = misses / references
|
||||
```
|
||||
|
||||
### Pattern 8 — 매 prefetch hint
|
||||
```ts
|
||||
// 매 link prefetch (browser)
|
||||
<link rel="prefetch" href="/next-page" as="document">
|
||||
|
||||
// 매 software prefetch (WASM/native via SIMD intrinsics)
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 hot loop slow | profile cache-misses, switch to SoA. |
|
||||
| 매 CDN low hit rate | normalize key, raise max-age. |
|
||||
| 매 LLM cost high | prompt-cache stable prefix. |
|
||||
| 매 working set > cache | partition (tiling), reduce. |
|
||||
| 매 frequent-but-changing | LRU + TTL. |
|
||||
|
||||
**기본값**: 매 measure first (perf, CDN headers, app counter), 매 90%+ hit rate target on hot caches.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Performance]]
|
||||
- 변형: [[CDN]] · [[Memoization]]
|
||||
- 응용: [[Prompt Cache]]
|
||||
- Adjacent: [[CPU Overhead]] · [[Buffer Allocation]] · [[Batching]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 cache strategy design, hit rate target setting, prompt-cache prefix structuring.
|
||||
**언제 X**: 매 hardware-specific cache line size assumption — vendor docs 의.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 cache everything**: 매 cold data cache space waste.
|
||||
- **매 unstable cache key**: 매 hit rate near zero.
|
||||
- **매 no eviction**: unbounded memory.
|
||||
- **매 measuring without baseline**: 매 hit rate alone meaningless — 매 latency / cost outcome 의.
|
||||
- **매 caching mutable data without invalidation**: stale read bug.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Reference (Hennessy & Patterson, Cloudflare cache docs, Anthropic prompt caching docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — cache hierarchy + LRU + HTTP + prompt-cache patterns |
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
id: wiki-2026-0508-case-study-kiwi-com-frontend-mig
|
||||
title: Case Study — Kiwi.com Frontend Migration
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Kiwi.com Migration, Kiwi Frontend Rewrite]
|
||||
duplicate_of: none
|
||||
source_trust_level: B
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [case-study, frontend, migration, react, monorepo]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react/nextjs
|
||||
---
|
||||
|
||||
# Case Study — Kiwi.com Frontend Migration
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 legacy → modern stack 매 incremental migration 의 textbook"**. Kiwi.com 매 travel booking platform 매 PHP/jQuery → React/TypeScript/Next.js 매 multi-year migration 의 case study. 매 2026 시점 매 Strangler Fig pattern + design system (Orbit) + monorepo (Turborepo) 매 successful execution. 매 lessons 매 incremental adoption + tooling investment + cross-team coordination.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 starting state (pre-migration)
|
||||
- PHP server-rendered + jQuery sprinkles.
|
||||
- 매 100+ engineers, 매 single repo.
|
||||
- 매 inconsistent UI, 매 design 매 ad-hoc.
|
||||
- 매 search → booking funnel 매 monolith.
|
||||
|
||||
### 매 target state
|
||||
- React + TypeScript SPA / SSR (Next.js).
|
||||
- Orbit design system (open-sourced) 매 single source of truth.
|
||||
- Monorepo (Turborepo) 매 shared package.
|
||||
- Apollo / GraphQL gateway.
|
||||
|
||||
### 매 migration playbook
|
||||
1. **Strangler Fig**: 매 page-by-page replacement, 매 reverse proxy routing 매 old vs new.
|
||||
2. **Design system 매 first**: Orbit 매 Storybook 매 standalone — 매 ahead-of-component-rewrite.
|
||||
3. **Type safety**: GraphQL codegen → TypeScript type 매 API contract.
|
||||
4. **Feature flags**: 매 traffic gradient — 1% → 10% → 100%.
|
||||
5. **Performance budget**: LCP / TTI 매 SLO, 매 regression CI gate.
|
||||
|
||||
### 매 응용 (lessons)
|
||||
1. 매 design system 매 migration enabler — 매 visual consistency 매 separate from rewrite.
|
||||
2. 매 monorepo 매 shared util / type / config 매 amortize.
|
||||
3. 매 reverse proxy split 매 risk-isolation — 매 rollback 매 instant.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Strangler Fig (reverse proxy routing)
|
||||
```nginx
|
||||
# nginx.conf — split traffic by route
|
||||
location /search/new {
|
||||
proxy_pass http://nextjs-upstream;
|
||||
}
|
||||
location /search {
|
||||
# legacy PHP
|
||||
proxy_pass http://php-upstream;
|
||||
}
|
||||
```
|
||||
|
||||
### Feature flag rollout
|
||||
```ts
|
||||
import { useFlag } from '@kiwicom/feature-flags';
|
||||
|
||||
export function Search() {
|
||||
const newSearch = useFlag('search.v2', { default: false });
|
||||
return newSearch ? <SearchV2 /> : <SearchLegacy />;
|
||||
}
|
||||
```
|
||||
|
||||
### GraphQL codegen pipeline
|
||||
```yaml
|
||||
# codegen.yml
|
||||
schema: https://api.kiwi.com/graphql
|
||||
documents: 'src/**/*.graphql'
|
||||
generates:
|
||||
src/__generated__/types.ts:
|
||||
plugins: [typescript, typescript-operations, typescript-react-apollo]
|
||||
config:
|
||||
withHooks: true
|
||||
```
|
||||
|
||||
### Monorepo workspace (Turborepo)
|
||||
```json
|
||||
// turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
|
||||
"test": { "dependsOn": ["^build"] },
|
||||
"lint": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
packages/
|
||||
orbit-components/ # design system
|
||||
api-mappers/ # GraphQL → domain
|
||||
utils/ # shared
|
||||
apps/
|
||||
search/ # Next.js
|
||||
booking/ # Next.js
|
||||
account/ # Next.js
|
||||
```
|
||||
|
||||
### Performance budget CI gate
|
||||
```ts
|
||||
// lighthouse-budget.json
|
||||
[{
|
||||
"path": "/search",
|
||||
"resourceSizes": [{ "resourceType": "script", "budget": 250 }],
|
||||
"timings": [{ "metric": "interactive", "budget": 3500 }]
|
||||
}]
|
||||
```
|
||||
|
||||
### Storybook-first component
|
||||
```tsx
|
||||
// Button.stories.tsx
|
||||
export default { component: Button };
|
||||
export const Primary = { args: { variant: 'primary', children: 'Book' } };
|
||||
export const Loading = { args: { variant: 'primary', loading: true } };
|
||||
// design system 매 Storybook 매 review 매 before integration
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Legacy app, 매 high-traffic | **Strangler Fig** — page-by-page |
|
||||
| Visual inconsistency 매 root issue | Design system 매 first investment |
|
||||
| Type drift 매 API ↔ frontend | GraphQL + codegen |
|
||||
| 매 monorepo coordination overhead | Turborepo / Nx 매 caching |
|
||||
| 매 risky launch | Feature flag + percentage rollout |
|
||||
|
||||
**기본값**: 매 incremental, 매 design-system-first, 매 telemetry-gated rollout.
|
||||
|
||||
## 🔗 Graph
|
||||
- 응용: [[Design System]] · [[Turborepo]] · [[Feature Flags]]
|
||||
- Adjacent: [[Micro-frontends]] · [[Monorepo]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: large legacy frontend rewrite 매 planning, 매 design system priority 의 justification.
|
||||
**언제 X**: greenfield app — Kiwi case 매 migration constraint 매 specific.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Big bang rewrite**: 매 multi-year freeze, 매 product velocity zero — Kiwi 가 explicitly avoided.
|
||||
- **Design system 매 after**: 매 visual drift 매 unfixable late.
|
||||
- **No telemetry**: 매 regression 매 invisible until users complain.
|
||||
- **Feature flag 매 abandoned**: 매 code path 매 dead, 매 cleanup 매 backlog.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Kiwi.com engineering blog, Orbit design system OSS — github.com/kiwicom/orbit).
|
||||
- 신뢰도 B (case study 매 specific, 매 generalizable lessons).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Strangler Fig + Orbit + monorepo + feature flag rollout |
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
id: wiki-2026-0508-cesiumjs
|
||||
title: CesiumJS
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Cesium, Cesium.js]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [3d, geospatial, webgl, frontend, mapping]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript / TypeScript
|
||||
framework: WebGL2 / WebGPU
|
||||
---
|
||||
|
||||
# CesiumJS
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 browser 의 3D digital globe + geospatial visualization 의 standard"**. CesiumJS 는 WGS84 ellipsoid 의 정확 globe 의 WebGL/WebGPU 렌더링, 3D Tiles spec 의 reference impl. 2026 도시 digital twin, drone telemetry, satellite tracking, defense visualization 의 dominant choice.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 concept
|
||||
- **Viewer**: 모든 widget 의 포함 의 main container
|
||||
- **Scene**: 3D world (globe, sky, atmosphere)
|
||||
- **Camera**: position + heading/pitch/roll, FlyTo 의 지원
|
||||
- **Entity**: high-level data-driven object (point, polygon, model)
|
||||
- **Primitive**: low-level direct GPU 의 access
|
||||
- **3D Tiles**: streaming hierarchical 3D dataset (city, photogrammetry)
|
||||
- **Terrain**: heightmap (Cesium World Terrain, custom)
|
||||
- **Imagery**: tile basemap (Bing, Mapbox, OSM)
|
||||
|
||||
### 매 좌표 system
|
||||
- `Cartesian3`: ECEF (meters from Earth center)
|
||||
- `Cartographic`: lon/lat/height (radians)
|
||||
- `Cesium.Math.toRadians/toDegrees` 의 변환
|
||||
|
||||
### 매 응용
|
||||
1. 도시 3D digital twin (Photogrammetry / CityGML).
|
||||
2. Drone / vehicle real-time tracking.
|
||||
3. Satellite orbit visualization (CZML).
|
||||
4. Construction BIM overlay.
|
||||
5. Disaster response (flood, wildfire).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Setup (Vite + npm)
|
||||
```ts
|
||||
import { Viewer, Cartesian3, Math as CMath, Ion } from 'cesium';
|
||||
import 'cesium/Build/Cesium/Widgets/widgets.css';
|
||||
|
||||
Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_ION_TOKEN;
|
||||
|
||||
const viewer = new Viewer('cesiumContainer', {
|
||||
terrainProvider: await Cesium.createWorldTerrainAsync(),
|
||||
});
|
||||
|
||||
viewer.camera.flyTo({
|
||||
destination: Cartesian3.fromDegrees(-122.4194, 37.7749, 5000),
|
||||
orientation: { heading: 0, pitch: CMath.toRadians(-45), roll: 0 },
|
||||
});
|
||||
```
|
||||
|
||||
### Entity (point + label)
|
||||
```ts
|
||||
viewer.entities.add({
|
||||
position: Cartesian3.fromDegrees(127.0, 37.5, 100),
|
||||
point: { pixelSize: 12, color: Cesium.Color.YELLOW },
|
||||
label: {
|
||||
text: 'Seoul',
|
||||
font: '14px sans-serif',
|
||||
pixelOffset: new Cesium.Cartesian2(0, -20),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3D Tiles (Photogrammetry city)
|
||||
```ts
|
||||
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(96188);
|
||||
viewer.scene.primitives.add(tileset);
|
||||
await viewer.zoomTo(tileset);
|
||||
|
||||
// Style 의 적용
|
||||
tileset.style = new Cesium.Cesium3DTileStyle({
|
||||
color: 'color("white", 0.9)',
|
||||
show: '${Height} > 30',
|
||||
});
|
||||
```
|
||||
|
||||
### glTF model (vehicle)
|
||||
```ts
|
||||
const drone = viewer.entities.add({
|
||||
position: Cartesian3.fromDegrees(127.0, 37.5, 200),
|
||||
model: {
|
||||
uri: '/models/drone.glb',
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 64,
|
||||
},
|
||||
orientation: Cesium.Transforms.headingPitchRollQuaternion(
|
||||
Cartesian3.fromDegrees(127.0, 37.5, 200),
|
||||
new Cesium.HeadingPitchRoll(0, 0, 0),
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
### CZML (time-dynamic)
|
||||
```ts
|
||||
const dataSource = await Cesium.CzmlDataSource.load('/data/flight.czml');
|
||||
viewer.dataSources.add(dataSource);
|
||||
viewer.clock.shouldAnimate = true;
|
||||
viewer.trackedEntity = dataSource.entities.values[0];
|
||||
```
|
||||
|
||||
### Real-time WebSocket update
|
||||
```ts
|
||||
const drone = viewer.entities.add({
|
||||
id: 'drone-1',
|
||||
position: new Cesium.SampledPositionProperty(),
|
||||
point: { pixelSize: 8, color: Cesium.Color.RED },
|
||||
});
|
||||
|
||||
const ws = new WebSocket('wss://api/telemetry');
|
||||
ws.onmessage = (e) => {
|
||||
const { lon, lat, alt, t } = JSON.parse(e.data);
|
||||
drone.position.addSample(
|
||||
Cesium.JulianDate.fromIso8601(t),
|
||||
Cartesian3.fromDegrees(lon, lat, alt),
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Pick (click → entity)
|
||||
```ts
|
||||
viewer.screenSpaceEventHandler.setInputAction((click) => {
|
||||
const picked = viewer.scene.pick(click.position);
|
||||
if (Cesium.defined(picked) && picked.id) {
|
||||
console.log('Clicked entity:', picked.id.id);
|
||||
}
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
```
|
||||
|
||||
### React 의 통합 (Resium)
|
||||
```tsx
|
||||
import { Viewer, Entity, PointGraphics } from 'resium';
|
||||
import { Cartesian3 } from 'cesium';
|
||||
|
||||
export function Globe() {
|
||||
return (
|
||||
<Viewer full>
|
||||
<Entity position={Cartesian3.fromDegrees(127, 37.5, 100)}>
|
||||
<PointGraphics pixelSize={10} />
|
||||
</Entity>
|
||||
</Viewer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 정확 WGS84 globe | CesiumJS |
|
||||
| 2D map only | Mapbox / MapLibre |
|
||||
| Photogrammetry city | 3D Tiles + Ion |
|
||||
| Time-series telemetry | CZML + SampledPositionProperty |
|
||||
| React app | Resium wrapper |
|
||||
| Custom shader | Primitive + Material |
|
||||
|
||||
**기본값**: Viewer + Entity (90% case 의 충분).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Geospatial]]
|
||||
- 변형: [[Three.js]]
|
||||
- 응용: [[Digital Twin]]
|
||||
- Adjacent: [[WebGL]] · [[WebGPU]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 3D globe, photogrammetry city, satellite/drone tracking 코드 generation.
|
||||
**언제 X**: 단순 2D map (Mapbox/MapLibre 의 lighter).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Entity 의 수만 개 add**: performance 의 죽음 — 매 Primitive / Cluster 의 사용.
|
||||
- **모든 frame 의 entity 의 recreate**: 매 GC pressure — `position` 의 update.
|
||||
- **Bundle 전체 import**: Cesium 의 매 huge — tree-shake / `cesium-vite` plugin.
|
||||
- **`viewer.scene.requestRender` 의 무시 in `requestRenderMode`**: 매 frame 의 frozen.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Cesium docs, 3D Tiles OGC spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — CesiumJS patterns + 3D Tiles + Resium |
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
id: wiki-2026-0508-chrome
|
||||
title: Chrome
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Google Chrome, Chromium]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [browser, chromium, devtools, frontend, web-platform]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: V8 / Blink
|
||||
---
|
||||
|
||||
# Chrome
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 web platform 의 de-facto reference engine"**. Chrome 은 Blink rendering + V8 JS + multi-process architecture 의 매 dominant browser (2026 ~65% global share). 매 web standard 의 첫 ship 자, DevTools 의 industry-standard 디버깅 환경, Chromium 의 매 Edge / Brave / Arc / Opera 의 기반.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 architecture
|
||||
- **Multi-process**: browser, renderer (per site), GPU, network, utility — sandbox isolation
|
||||
- **Site Isolation**: each site → separate process (Spectre 방어)
|
||||
- **Blink**: rendering engine (WebKit fork)
|
||||
- **V8**: JS engine (TurboFan, Maglev, Sparkplug, Ignition tiers)
|
||||
- **Skia**: 2D graphics
|
||||
- **Mojo**: IPC
|
||||
|
||||
### 매 DevTools panel
|
||||
- **Elements**: DOM + CSS inspect, computed, layout
|
||||
- **Console**: REPL, log, error
|
||||
- **Sources**: debugger, breakpoint, snippet
|
||||
- **Network**: request waterfall, throttling
|
||||
- **Performance**: flame chart, FPS, layout shift
|
||||
- **Application**: storage, service worker, cache, manifest
|
||||
- **Lighthouse**: audit (perf, a11y, SEO, PWA)
|
||||
- **Recorder**: user flow capture + replay
|
||||
|
||||
### 매 key feature (2026)
|
||||
- WebGPU (stable)
|
||||
- View Transitions API
|
||||
- Speculation Rules (prerender)
|
||||
- Origin Trials (experimental opt-in)
|
||||
- Built-in Gemini Nano (on-device LLM)
|
||||
- WebAssembly GC + Tail Call
|
||||
|
||||
### 매 응용
|
||||
1. Web 앱 dev (DevTools).
|
||||
2. Extension dev (MV3).
|
||||
3. Headless automation (Puppeteer, Playwright).
|
||||
4. Performance profiling (Lighthouse CI).
|
||||
5. PWA distribution.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### DevTools console snippet
|
||||
```js
|
||||
// 모든 image lazy 의 강제
|
||||
$$('img').forEach(img => img.loading = 'lazy');
|
||||
|
||||
// Performance mark
|
||||
performance.mark('start');
|
||||
// ... work
|
||||
performance.mark('end');
|
||||
performance.measure('work', 'start', 'end');
|
||||
console.table(performance.getEntriesByName('work'));
|
||||
```
|
||||
|
||||
### Lighthouse CI
|
||||
```yaml
|
||||
# .github/workflows/lighthouse.yml
|
||||
- uses: treosh/lighthouse-ci-action@v12
|
||||
with:
|
||||
urls: |
|
||||
https://example.com/
|
||||
https://example.com/about
|
||||
uploadArtifacts: true
|
||||
temporaryPublicStorage: true
|
||||
```
|
||||
|
||||
### Puppeteer (headless)
|
||||
```ts
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const browser = await puppeteer.launch({ headless: 'new' });
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://example.com');
|
||||
const title = await page.title();
|
||||
await page.screenshot({ path: 'shot.png', fullPage: true });
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
### Extension (MV3) — Service Worker
|
||||
```json
|
||||
// manifest.json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Tab Counter",
|
||||
"version": "1.0",
|
||||
"permissions": ["tabs", "storage"],
|
||||
"action": { "default_popup": "popup.html" },
|
||||
"background": { "service_worker": "sw.js" }
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// sw.js
|
||||
chrome.tabs.onCreated.addListener(async () => {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
await chrome.action.setBadgeText({ text: String(tabs.length) });
|
||||
});
|
||||
```
|
||||
|
||||
### Speculation Rules (prerender)
|
||||
```html
|
||||
<script type="speculationrules">
|
||||
{
|
||||
"prerender": [{
|
||||
"where": { "href_matches": "/products/*" },
|
||||
"eagerness": "moderate"
|
||||
}]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### chrome:// internals (debug)
|
||||
```
|
||||
chrome://inspect — devtools for remote / node
|
||||
chrome://flags — experimental feature toggle
|
||||
chrome://net-internals — network event log
|
||||
chrome://gpu — GPU + WebGPU status
|
||||
chrome://tracing — perfetto trace
|
||||
chrome://version — V8 / Blink version
|
||||
```
|
||||
|
||||
### CDP (Chrome DevTools Protocol)
|
||||
```ts
|
||||
import CDP from 'chrome-remote-interface';
|
||||
|
||||
const client = await CDP();
|
||||
const { Network, Page } = client;
|
||||
await Network.enable();
|
||||
await Page.enable();
|
||||
Network.requestWillBeSent((params) => console.log(params.request.url));
|
||||
await Page.navigate({ url: 'https://example.com' });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Web app dev | DevTools (Elements, Network, Performance) |
|
||||
| Perf audit | Lighthouse / Lighthouse CI |
|
||||
| E2E test | Playwright (Chrome / Firefox / WebKit) |
|
||||
| Scraping / automation | Puppeteer (Chromium native) |
|
||||
| Cross-browser test | Playwright + BrowserStack |
|
||||
| Privacy-focused user | Brave / Firefox |
|
||||
|
||||
**기본값**: Chrome stable + Lighthouse CI + Playwright.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Chromium]]
|
||||
- 변형: [[Arc]]
|
||||
- 응용: [[Lighthouse]]
|
||||
- Adjacent: [[V8]] · [[Blink]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: DevTools workflow, extension scaffolding, headless automation 의 generation.
|
||||
**언제 X**: 매 cross-browser compat 매 critical (Firefox / Safari 의 also test).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Chrome-only 가정**: vendor-prefixed `-webkit-` API 의 의존 — 매 standard 의 사용.
|
||||
- **Manifest V2**: deprecated — 매 MV3 + service worker.
|
||||
- **DevTools throttling 의 무시**: prod 의 slow 3G 시 불상.
|
||||
- **`console.log` 의 prod ship**: bundle bloat + privacy — 매 strip plugin.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Chrome Developers docs, Chromium source, web.dev).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Chrome architecture + DevTools + MV3 + 2026 features |
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: wiki-2026-0508-client-components
|
||||
title: Client Components
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [RSC Client Components, "use client"]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, nextjs, rsc, frontend, hydration]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react/nextjs
|
||||
---
|
||||
|
||||
# Client Components
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 interactive boundary"**. Client Components 매 React Server Components (RSC) 매 architecture 의 매 interactive half — `'use client'` directive 매 매 module-level boundary marker, 매 hydration + state + browser API 매 가능한 영역. 매 2026 현재 Next.js 13+ App Router / Remix Single Fetch / TanStack Start 의 default model.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 boundary model
|
||||
- `'use client'` 매 file-top directive — 매 module 부터 dependent tree 매 client bundle 에 포함.
|
||||
- Server Component (default) 매 server-only render — 매 zero JS shipped.
|
||||
- Client Component 매 hydrate — `useState` / `useEffect` / event handler / browser API 매 가능.
|
||||
|
||||
### 매 핵심 properties
|
||||
- **Composability**: Server → Client 매 OK (props 통해), Client → Server 매 NOT (children prop slot 만 OK).
|
||||
- **Serialization**: Server → Client props 매 serializable 만 (no functions, classes, Date OK via 2026 RSC payload).
|
||||
- **Bundle**: 매 leaf client component 만 ship — 매 root 에서 'use client' 매 X.
|
||||
|
||||
### 매 응용
|
||||
1. Form 매 controlled input + validation.
|
||||
2. Animation / transition (Framer Motion, View Transitions API).
|
||||
3. Browser API (geolocation, clipboard, IndexedDB).
|
||||
4. Real-time (WebSocket, SSE consumer).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic client component
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Counter() {
|
||||
const [n, setN] = useState(0);
|
||||
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server Component → Client Component (props)
|
||||
```tsx
|
||||
// page.tsx (Server)
|
||||
import { getProducts } from '@/lib/db';
|
||||
import { ProductGrid } from './ProductGrid';
|
||||
|
||||
export default async function Page() {
|
||||
const products = await getProducts(); // server fetch
|
||||
return <ProductGrid initial={products} />;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ProductGrid.tsx (Client — interactive filter)
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ProductGrid({ initial }: { initial: Product[] }) {
|
||||
const [filter, setFilter] = useState('');
|
||||
const visible = initial.filter(p => p.name.includes(filter));
|
||||
return (
|
||||
<>
|
||||
<input value={filter} onChange={e => setFilter(e.target.value)} />
|
||||
{visible.map(p => <Card key={p.id} {...p} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Client Component 안 Server Component (children slot)
|
||||
```tsx
|
||||
// Layout.tsx (Client — needs onClick)
|
||||
'use client';
|
||||
export function Sidebar({ children }: { children: ReactNode }) {
|
||||
return <aside onClick={...}>{children}</aside>;
|
||||
}
|
||||
|
||||
// page.tsx (Server)
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { ServerProfile } from './ServerProfile';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Sidebar>
|
||||
<ServerProfile /> {/* Server component as children — OK */}
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action 호출 (Client → Server mutation)
|
||||
```tsx
|
||||
'use client';
|
||||
import { createPost } from './actions'; // 'use server' file
|
||||
|
||||
export function NewPostForm() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<input name="title" />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Suspense + Streaming
|
||||
```tsx
|
||||
// page.tsx (Server)
|
||||
import { Suspense } from 'react';
|
||||
import { Comments } from './Comments';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Article />
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Comments /> {/* streamed in */}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Static rendering, data fetching | **Server Component** (default) |
|
||||
| State / event / effect / browser API | **Client Component** |
|
||||
| SEO + interactive (form) | Server shell + Client island |
|
||||
| 매 entire page interactive (dashboard) | Mostly client, server outer layout |
|
||||
|
||||
**기본값**: 매 default Server Component — 매 boundary 를 leaf 에 push, 매 'use client' 매 minimum.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React Server Components — 경계 의식]]
|
||||
- 변형: [[Modern_Web_Rendering_and_Optimization|Server Components]] · [[Server Actions]]
|
||||
- 응용: [[Hydration]] · [[Suspense]] · [[Streaming SSR]]
|
||||
- Adjacent: [[Islands Architecture]] · [[Astro]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: interactivity 매 필요한 leaf component, browser-only API, 매 form / input control.
|
||||
**언제 X**: data fetching 매 only, static content — Server Component 가 더 light.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Root layout 매 'use client'**: 매 entire app 매 client bundle — 매 RSC benefit 의 destruction.
|
||||
- **Server-only data 매 props 로 큰 객체 pass**: 매 RSC payload bloat.
|
||||
- **Client component 안 server-only import** (e.g., `fs`, `db`): 매 build error / leak risk.
|
||||
- **Server Component 안 useState**: 매 build error — 매 boundary 의 misunderstanding.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React docs RSC 2026, Next.js 15 App Router guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 'use client' boundary + composition rules + Server Action |
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
id: wiki-2026-0508-client-side-rendering-csr
|
||||
title: Client-Side Rendering (CSR)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [CSR, SPA Rendering]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [rendering, csr, spa, frontend, performance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React / Vue / Svelte
|
||||
---
|
||||
|
||||
# Client-Side Rendering (CSR)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 browser 가 매 HTML 을 그린다"**. CSR 은 server 가 빈 shell + JS bundle 만 보내고, browser 가 fetch + render 모두 수행 — 매 SPA 의 default mode, interactive app 에 강하나 매 first paint / SEO 매 weak.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 lifecycle
|
||||
1. Browser → server: `GET /` → minimal HTML + `<script src="bundle.js">`.
|
||||
2. Browser parses HTML → fetches JS bundle.
|
||||
3. JS executes → mounts framework → fetches data → renders DOM.
|
||||
4. User interacts.
|
||||
|
||||
### 매 trade-off
|
||||
| Pros | Cons |
|
||||
|---|---|
|
||||
| Rich interactivity | Slow TTI (특히 mobile) |
|
||||
| Server cost low | SEO 매 needs hydration tricks |
|
||||
| Client routing fast | Blank screen until JS loads |
|
||||
| Offline-capable (PWA) | Bundle size matters a lot |
|
||||
|
||||
### 매 CSR vs SSR vs RSC (2026)
|
||||
- CSR: dashboard, internal tool, app-like UX.
|
||||
- SSR: marketing, blog, e-commerce.
|
||||
- RSC: hybrid — server-render with client islands.
|
||||
- SSG: docs, blog (rebuild on content change).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Vite + React CSR baseline
|
||||
```tsx
|
||||
// main.tsx
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
```
|
||||
```html
|
||||
<!-- index.html -->
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
```
|
||||
|
||||
### Route-based code splitting
|
||||
```tsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Routes, Route } from 'react-router';
|
||||
|
||||
const Dashboard = lazy(() => import('./Dashboard'));
|
||||
const Settings = lazy(() => import('./Settings'));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Skeleton-first paint (perceived perf)
|
||||
```tsx
|
||||
function UsersList() {
|
||||
const { data, isLoading } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
|
||||
if (isLoading) return <UsersSkeleton rows={10} />;
|
||||
return <ul>{data!.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-fetch on hover (link prefetch)
|
||||
```tsx
|
||||
<Link
|
||||
to="/dashboard"
|
||||
onMouseEnter={() => queryClient.prefetchQuery({ queryKey: ['dashboardData'], queryFn })}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Service Worker for offline shell
|
||||
```ts
|
||||
// sw.ts
|
||||
self.addEventListener('install', (e: any) => {
|
||||
e.waitUntil(caches.open('shell-v1').then((c) => c.addAll(['/', '/main.js', '/main.css'])));
|
||||
});
|
||||
self.addEventListener('fetch', (e: any) => {
|
||||
e.respondWith(caches.match(e.request).then((r) => r ?? fetch(e.request)));
|
||||
});
|
||||
```
|
||||
|
||||
### Bundle budget enforcement
|
||||
```js
|
||||
// vite.config.ts
|
||||
export default {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: { vendor: ['react', 'react-dom'] },
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 200, // KB
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### SEO via prerender (when CSR + needed)
|
||||
```bash
|
||||
# Use prerender for marketing routes only
|
||||
npx prerender-spa-plugin --routes /,/about,/pricing
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Render mode |
|
||||
|---|---|
|
||||
| Auth-walled dashboard | CSR |
|
||||
| Marketing site | SSG or SSR |
|
||||
| Mixed app (e-commerce) | RSC / SSR + islands |
|
||||
| Rich realtime (Figma-like) | CSR + WebSocket |
|
||||
|
||||
**기본값**: 매 user-app (login wall 뒤) → CSR. 매 public content → SSR/SSG/RSC.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Rendering-Strategies]] · [[프론트엔드 및 UIUX 표준|Frontend-Architecture]]
|
||||
- 변형: [[React Server Components — 경계 의식]]
|
||||
- Adjacent: [[Core Web Vitals Optimization (INP, LCP, CLS)|Core-Web-Vitals]] · [[Code Splitting]] · [[Hydration]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: app-like UX, auth-protected, heavy client interactivity.
|
||||
**언제 X**: 매 SEO-critical public page, low-end device 가 주 audience.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Mega-bundle**: 매 single chunk 5MB → split routes / vendor.
|
||||
- **No skeleton / loading state**: 매 blank screen 매 perceived as broken.
|
||||
- **CSR for blog/docs**: 매 SEO/perf 매 모두 lose — SSG choice.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev / React docs / Vite docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — CSR fundamentals + tradeoffs + patterns |
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
id: wiki-2026-0508-code-splitting
|
||||
title: Code Splitting
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Bundle Splitting, Dynamic Import]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [bundling, performance, webpack, vite, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Vite / Webpack / Next.js
|
||||
---
|
||||
|
||||
# Code Splitting
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 bundle 의 chunk 의 분리, on-demand load 의 first-paint 단축"**. Code Splitting 은 dynamic `import()` + bundler chunking 의 결합 의 매 monolithic JS bundle 의 분해. 2026 Vite/Rspack/Turbopack 시대 에 route-level + component-level + vendor split 의 매 표준.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 splitting 의 axis
|
||||
- **Route-based**: 각 page 의 separate chunk — 매 SPA / Next.js 의 default
|
||||
- **Component-based**: heavy component 의 lazy (Modal, Chart, Editor)
|
||||
- **Vendor**: `node_modules` 의 separate chunk — long-term cache
|
||||
- **Dynamic feature**: locale, A/B variant 의 conditional load
|
||||
|
||||
### 매 mechanism
|
||||
- **Dynamic `import()`**: ECMAScript spec — Promise 의 return
|
||||
- **Bundler 의 split point detection**: `import()` 호출 의 chunk boundary
|
||||
- **Manifest**: hashed filename 의 mapping
|
||||
- **Preload / prefetch**: `<link rel="preload/prefetch">` hint
|
||||
|
||||
### 매 cost trade-off
|
||||
- **Pro**: initial bundle 의 작아짐 → faster TTI
|
||||
- **Con**: extra HTTP request, waterfall 위험 — preload 의 mitigate
|
||||
|
||||
### 매 응용
|
||||
1. Route-level lazy (React Router, Next.js).
|
||||
2. Modal / dialog (open 시 load).
|
||||
3. Heavy editor (Monaco, CodeMirror).
|
||||
4. Chart library (Recharts, ECharts).
|
||||
5. i18n locale chunk.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React.lazy + Suspense
|
||||
```tsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const Settings = lazy(() => import('./Settings'));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js dynamic
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Chart = dynamic(() => import('@/components/Chart'), {
|
||||
loading: () => <p>Loading chart...</p>,
|
||||
ssr: false, // browser-only
|
||||
});
|
||||
|
||||
export default function Dashboard() {
|
||||
return <Chart data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Route-level (React Router v6)
|
||||
```tsx
|
||||
import { lazy } from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: '/', lazy: () => import('./routes/Home') },
|
||||
{ path: '/about', lazy: () => import('./routes/About') },
|
||||
{ path: '/admin/*', lazy: () => import('./routes/Admin') },
|
||||
]);
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Vite의 manualChunks
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
react: ['react', 'react-dom'],
|
||||
ui: ['@radix-ui/react-dialog', '@radix-ui/react-tabs'],
|
||||
charts: ['recharts', 'd3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Webpack magic comment
|
||||
```ts
|
||||
const Editor = () => import(
|
||||
/* webpackChunkName: "editor" */
|
||||
/* webpackPrefetch: true */
|
||||
'./Editor'
|
||||
);
|
||||
```
|
||||
|
||||
### Conditional import (locale)
|
||||
```ts
|
||||
async function loadLocale(lang: string) {
|
||||
const messages = await import(`./locales/${lang}.json`);
|
||||
return messages.default;
|
||||
}
|
||||
```
|
||||
|
||||
### Preload critical chunk
|
||||
```html
|
||||
<link rel="modulepreload" href="/assets/Settings-abc123.js">
|
||||
```
|
||||
|
||||
```tsx
|
||||
// React: preload on hover
|
||||
<Link
|
||||
to="/settings"
|
||||
onMouseEnter={() => import('./Settings')}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Module Federation (micro-frontend)
|
||||
```ts
|
||||
// host webpack.config.js
|
||||
new ModuleFederationPlugin({
|
||||
remotes: {
|
||||
checkout: 'checkout@https://cdn/checkout/remoteEntry.js',
|
||||
},
|
||||
});
|
||||
|
||||
// usage
|
||||
const Checkout = lazy(() => import('checkout/Cart'));
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| SPA, multi-page | Route-based split |
|
||||
| Heavy modal / editor | `lazy()` + Suspense |
|
||||
| Vendor lib stable | manualChunks vendor split |
|
||||
| SSR + browser-only | `dynamic({ ssr: false })` |
|
||||
| Independent deploy | Module Federation |
|
||||
| Hover-triggered | preload on intent |
|
||||
|
||||
**기본값**: route-based + heavy-component lazy + vendor chunk.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]]
|
||||
- 변형: [[Module Federation]]
|
||||
- 응용: [[Lazy-Loading-Strategies|Lazy Loading]]
|
||||
- Adjacent: [[Vite]] · [[Next.js]] · [[Suspense]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: route lazy, heavy component 의 split, vendor chunking 코드 generation.
|
||||
**언제 X**: 매 small app (10kb 의 split 의 overhead 가 더 큼).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **너무 fine-grained split**: 100개 chunk → request flood — 매 ~10-30 chunk 의 sweet spot.
|
||||
- **Waterfall**: A → B → C 의 sequential — 매 parallel preload.
|
||||
- **Vendor chunk 의 무한 growth**: dependency 추가 시 cache invalidation — 매 stable lib 만 vendor.
|
||||
- **Suspense 의 boundary 누락**: error / blank screen.
|
||||
- **`ssr: false` 의 abuse**: SEO 손실.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vite docs, Webpack docs, Next.js docs, React 19).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — splitting strategies + bundler config + MF |
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
id: wiki-2026-0508-codegen
|
||||
title: Codegen (GraphQL, OpenAPI, Schema-driven)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Code Generation, GraphQL Codegen, OpenAPI Codegen]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [codegen, graphql, openapi, typescript, tooling]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: GraphQL Code Generator / openapi-typescript
|
||||
---
|
||||
|
||||
# Codegen (Schema-driven Code Generation)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 schema 가 single source of truth"**. Codegen 은 GraphQL/OpenAPI/Protobuf schema 에서 type 과 client code 를 자동 생성 — 매 manual sync 의 X, drift 의 zero, refactor 매 compile-time safe.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 주요 도구 (2026)
|
||||
- **GraphQL**: `@graphql-codegen/cli` (client preset), `gql.tada` (zero-config inline).
|
||||
- **OpenAPI**: `openapi-typescript` (types), `openapi-fetch` (typed client), `orval` (React Query hooks).
|
||||
- **gRPC/Protobuf**: `bufbuild/protobuf-es`, `connect-es`.
|
||||
- **DB schema**: `kysely-codegen`, `drizzle-kit`, Prisma.
|
||||
|
||||
### 매 출력 종류
|
||||
- Type definitions only (lightweight).
|
||||
- Typed client (operations + types).
|
||||
- Hooks/composables (React Query, SWR, urql).
|
||||
- Server stubs (resolvers, controllers).
|
||||
|
||||
### 매 watch vs build-time
|
||||
- Dev: watch mode → 매 save 시 regen.
|
||||
- CI: `--check` 로 매 schema/code drift 검출.
|
||||
- Pre-commit: 매 generated files 매 staged 보장.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### GraphQL client preset (@graphql-codegen/cli)
|
||||
```yaml
|
||||
# codegen.ts
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: 'https://api.example.com/graphql',
|
||||
documents: ['src/**/*.{ts,tsx}'],
|
||||
generates: {
|
||||
'src/gql/': {
|
||||
preset: 'client',
|
||||
config: { useTypeImports: true, scalars: { DateTime: 'string' } },
|
||||
},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Using generated `graphql()` (typed documents)
|
||||
```ts
|
||||
import { graphql } from '@/gql';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const FlightSearch = graphql(`
|
||||
query FlightSearch($from: String!, $to: String!) {
|
||||
flights(from: $from, to: $to) { id price airline }
|
||||
}
|
||||
`);
|
||||
|
||||
export function useFlights(vars: { from: string; to: string }) {
|
||||
return useQuery({
|
||||
queryKey: ['flights', vars],
|
||||
queryFn: () => request('/graphql', FlightSearch, vars),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### gql.tada (zero-config alternative)
|
||||
```ts
|
||||
import { graphql } from 'gql.tada';
|
||||
|
||||
const Query = graphql(`
|
||||
query Me { me { id email } }
|
||||
`);
|
||||
// 매 inferred 매 fully typed — no codegen step
|
||||
```
|
||||
|
||||
### openapi-typescript + openapi-fetch
|
||||
```bash
|
||||
npx openapi-typescript ./openapi.yaml -o ./src/api/schema.ts
|
||||
```
|
||||
```ts
|
||||
import createClient from 'openapi-fetch';
|
||||
import type { paths } from './api/schema';
|
||||
|
||||
const api = createClient<paths>({ baseUrl: 'https://api.example.com' });
|
||||
|
||||
const { data, error } = await api.GET('/users/{id}', {
|
||||
params: { path: { id: '42' } },
|
||||
});
|
||||
// data 매 매 fully typed
|
||||
```
|
||||
|
||||
### orval — React Query hooks from OpenAPI
|
||||
```ts
|
||||
// orval.config.ts
|
||||
export default {
|
||||
api: {
|
||||
input: './openapi.yaml',
|
||||
output: {
|
||||
target: './src/api/generated.ts',
|
||||
client: 'react-query',
|
||||
mode: 'tags-split',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Drizzle Kit — DB schema introspection
|
||||
```bash
|
||||
drizzle-kit introspect:pg --connectionString=$DATABASE_URL --out=./src/db
|
||||
```
|
||||
|
||||
### CI drift check
|
||||
```yaml
|
||||
# .github/workflows/codegen-check.yml
|
||||
- run: pnpm codegen
|
||||
- run: git diff --exit-code src/gql src/api
|
||||
```
|
||||
|
||||
### Husky pre-commit
|
||||
```sh
|
||||
# .husky/pre-commit
|
||||
pnpm codegen && git add src/gql src/api
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | 도구 |
|
||||
|---|---|
|
||||
| GraphQL + React/Vue | `@graphql-codegen` client preset |
|
||||
| GraphQL + want zero step | `gql.tada` |
|
||||
| OpenAPI types only | `openapi-typescript` |
|
||||
| OpenAPI + hooks | `orval` |
|
||||
| DB-first | Drizzle / Prisma / Kysely codegen |
|
||||
|
||||
**기본값**: GraphQL → client preset; REST → `openapi-typescript` + `openapi-fetch`.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Type-Safety]]
|
||||
- 변형: [[OpenAPI]] · [[gRPC]]
|
||||
- 응용: [[React-Query]]
|
||||
- Adjacent: [[Contract-Testing]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: schema-driven API integration / 매 type drift 매 painful 한 monorepo.
|
||||
**언제 X**: 매 throwaway prototype, schema 매 unstable.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Hand-writing GraphQL types**: 매 drift 의 zero-day issue.
|
||||
- **Committing nothing then expecting safety**: 매 generated files 매 commit + CI check 둘 다 필요.
|
||||
- **Running codegen only locally**: 매 CI drift check 의 X 면 PR 매 깨짐.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (the-guild.dev / openapi-ts.dev / orval.dev official docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — codegen tools (GraphQL/OpenAPI) with patterns |
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
id: wiki-2026-0508-component-composition
|
||||
title: Component Composition (React)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Composition, Compound Components]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, composition, component-design, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19
|
||||
---
|
||||
|
||||
# Component Composition (React)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 inheritance 의 X, composition 의 O"**. React 의 design principle — UI 를 작은 composable component 로 분해, props.children + slot pattern + compound API 로 매 flexible 한 reuse 달성.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 idioms
|
||||
- `props.children` (default slot).
|
||||
- Named slots (props as render fn / ReactNode).
|
||||
- Compound components (parent + children share context).
|
||||
- Render props / function-as-children.
|
||||
- Polymorphic `as` prop.
|
||||
- `React.cloneElement` / `Slot` (Radix).
|
||||
|
||||
### 매 vs Inheritance
|
||||
- React 매 class extension 의 X — 매 wrapper component.
|
||||
- 매 `<Parent>` + `<Parent.Item>` style 매 implicit relation expression.
|
||||
|
||||
### 매 응용
|
||||
1. Modal / Dialog (Header / Body / Footer slots).
|
||||
2. Form fields (Field / Label / Input / Error).
|
||||
3. Menu / Combobox (Radix-style headless).
|
||||
4. Layout primitives (Stack, Grid).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Children as default slot
|
||||
```tsx
|
||||
function Card({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card">{children}</div>;
|
||||
}
|
||||
// <Card><h2>Hi</h2><p>Body</p></Card>
|
||||
```
|
||||
|
||||
### Named slots via props
|
||||
```tsx
|
||||
type Props = {
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function Page({ header, footer, children }: Props) {
|
||||
return (
|
||||
<>
|
||||
{header && <header>{header}</header>}
|
||||
<main>{children}</main>
|
||||
{footer && <footer>{footer}</footer>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Compound components with Context
|
||||
```tsx
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const TabsCtx = createContext<{ active: string; set: (v: string) => void } | null>(null);
|
||||
|
||||
function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
|
||||
const [active, set] = useState(defaultValue);
|
||||
return <TabsCtx value={{ active, set }}>{children}</TabsCtx>;
|
||||
}
|
||||
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
const { active, set } = useContext(TabsCtx)!;
|
||||
return <button data-active={active === value} onClick={() => set(value)}>{children}</button>;
|
||||
}
|
||||
function Panel({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
const { active } = useContext(TabsCtx)!;
|
||||
return active === value ? <div>{children}</div> : null;
|
||||
}
|
||||
Tabs.Tab = Tab;
|
||||
Tabs.Panel = Panel;
|
||||
export { Tabs };
|
||||
|
||||
// Usage:
|
||||
// <Tabs defaultValue="a"><Tabs.Tab value="a">A</Tabs.Tab><Tabs.Panel value="a">…</Tabs.Panel></Tabs>
|
||||
```
|
||||
|
||||
### Render prop
|
||||
```tsx
|
||||
function Toggle({ children }: { children: (state: { on: boolean; toggle: () => void }) => React.ReactNode }) {
|
||||
const [on, setOn] = useState(false);
|
||||
return <>{children({ on, toggle: () => setOn((v) => !v) })}</>;
|
||||
}
|
||||
// <Toggle>{({ on, toggle }) => <button onClick={toggle}>{on ? 'on' : 'off'}</button>}</Toggle>
|
||||
```
|
||||
|
||||
### Polymorphic `as` prop
|
||||
```tsx
|
||||
type AsProp<T extends React.ElementType> = { as?: T } & React.ComponentPropsWithoutRef<T>;
|
||||
|
||||
function Box<T extends React.ElementType = 'div'>({ as, ...rest }: AsProp<T>) {
|
||||
const Tag = as ?? 'div';
|
||||
return <Tag {...rest} />;
|
||||
}
|
||||
// <Box as="section" className="x">…</Box>
|
||||
```
|
||||
|
||||
### Radix Slot pattern (asChild)
|
||||
```tsx
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
function Button({ asChild, ...props }: { asChild?: boolean } & React.ComponentProps<'button'>) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return <Comp className="btn" {...props} />;
|
||||
}
|
||||
// <Button asChild><a href="/x">link styled as button</a></Button>
|
||||
```
|
||||
|
||||
### Layout primitives (Stack)
|
||||
```tsx
|
||||
function Stack({ gap = 8, children }: { gap?: number; children: React.ReactNode }) {
|
||||
return <div style={{ display: 'flex', flexDirection: 'column', gap }}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Pattern |
|
||||
|---|---|
|
||||
| Single content area | `children` |
|
||||
| Multiple structured slots | Named props or compound |
|
||||
| Tightly-coupled siblings | Compound + context |
|
||||
| Behavior + UI separation | Render props / hook |
|
||||
| Style polymorphism | `as` prop or `Slot` |
|
||||
|
||||
**기본값**: 매 children prop 으로 시작 → 복잡해지면 compound + context.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]]
|
||||
- 변형: [[Web-Components]]
|
||||
- 응용: [[Radix-UI]] · [[Headless-UI]] · [[shadcn]]
|
||||
- Adjacent: [[Server Components]] · [[Composition API]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: design system 구축, library API 설계, complex form/dialog UI.
|
||||
**언제 X**: 매 trivial leaf component — over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Prop explosion**: 매 20+ props → 매 compound 로 분해.
|
||||
- **`cloneElement` everywhere**: 매 fragile, prefer context.
|
||||
- **Deep prop drilling**: 매 context or compound 로 해결.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (react.dev / Radix UI patterns / Kent Dodds blog).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — React composition idioms + compound pattern |
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
id: wiki-2026-0508-composables
|
||||
title: Composables (Vue)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Vue Composables, useX functions]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [vue, composition-api, composables, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Vue 3
|
||||
---
|
||||
|
||||
# Composables (Vue)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 reactive logic 의 reusable function"**. Composable 은 Vue 3 Composition API 의 stateful logic 을 추출한 `useX()` 함수 — React Hooks 와 유사하나 매 reactivity primitive (`ref`, `reactive`, `computed`) 기반이라 caller 매 free of dependency arrays.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 정의
|
||||
- `use*` prefix 매 convention.
|
||||
- Returns reactive refs / computed / methods.
|
||||
- Side-effects via `onMounted` / `onScopeDispose` (`watchEffect` cleanup).
|
||||
- `effectScope` 매 manual lifecycle (e.g., outside components).
|
||||
|
||||
### 매 vs React Hooks
|
||||
| Aspect | Vue Composable | React Hook |
|
||||
|---|---|---|
|
||||
| Re-run | 매 once on setup | every render |
|
||||
| Deps | reactive auto-track | manual array |
|
||||
| Cleanup | `onScopeDispose` | return fn |
|
||||
| Conditional call | 허용 (with caveats) | 금지 |
|
||||
|
||||
### 매 응용
|
||||
1. `useFetch` / `useAsyncData` — async + cancellation.
|
||||
2. `useElementSize`, `useEventListener` — DOM bindings (VueUse).
|
||||
3. `useStore`, `useFeatureFlag` — cross-cutting state.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic counter composable
|
||||
```ts
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function useCounter(initial = 0) {
|
||||
const count = ref(initial);
|
||||
const isZero = computed(() => count.value === 0);
|
||||
const inc = () => count.value++;
|
||||
const dec = () => count.value--;
|
||||
return { count, isZero, inc, dec };
|
||||
}
|
||||
```
|
||||
|
||||
### useFetch with abort + reactive URL
|
||||
```ts
|
||||
import { ref, watchEffect, onScopeDispose, type MaybeRefOrGetter, toValue } from 'vue';
|
||||
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null);
|
||||
const error = ref<Error | null>(null);
|
||||
const loading = ref(false);
|
||||
let ctrl: AbortController | null = null;
|
||||
|
||||
watchEffect(async () => {
|
||||
ctrl?.abort();
|
||||
ctrl = new AbortController();
|
||||
loading.value = true;
|
||||
try {
|
||||
const r = await fetch(toValue(url), { signal: ctrl.signal });
|
||||
data.value = await r.json();
|
||||
} catch (e) {
|
||||
if ((e as Error).name !== 'AbortError') error.value = e as Error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onScopeDispose(() => ctrl?.abort());
|
||||
return { data, error, loading };
|
||||
}
|
||||
```
|
||||
|
||||
### useEventListener (auto cleanup)
|
||||
```ts
|
||||
import { onMounted, onScopeDispose, type Ref } from 'vue';
|
||||
|
||||
export function useEventListener<K extends keyof WindowEventMap>(
|
||||
target: Window | Ref<HTMLElement | null>,
|
||||
event: K,
|
||||
handler: (e: WindowEventMap[K]) => void,
|
||||
) {
|
||||
onMounted(() => {
|
||||
const el = 'value' in target ? target.value : target;
|
||||
el?.addEventListener(event, handler as EventListener);
|
||||
});
|
||||
onScopeDispose(() => {
|
||||
const el = 'value' in target ? target.value : target;
|
||||
el?.removeEventListener(event, handler as EventListener);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### useLocalStorage (sync ref ↔ storage)
|
||||
```ts
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
export function useLocalStorage<T>(key: string, initial: T) {
|
||||
const stored = localStorage.getItem(key);
|
||||
const value = ref<T>(stored ? JSON.parse(stored) : initial);
|
||||
watch(value, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true });
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### useDebouncedRef
|
||||
```ts
|
||||
import { customRef } from 'vue';
|
||||
|
||||
export function useDebouncedRef<T>(value: T, delay = 300) {
|
||||
let t: ReturnType<typeof setTimeout>;
|
||||
return customRef<T>((track, trigger) => ({
|
||||
get() { track(); return value; },
|
||||
set(v) {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => { value = v; trigger(); }, delay);
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Detached scope (composable outside component)
|
||||
```ts
|
||||
import { effectScope } from 'vue';
|
||||
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
const counter = useCounter();
|
||||
// ... use anywhere
|
||||
});
|
||||
// later
|
||||
scope.stop();
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Component-local state | `ref` directly |
|
||||
| Logic reused in 2+ components | Composable |
|
||||
| Global state (auth, theme) | Pinia store (composable underneath) |
|
||||
| DOM API integration | VueUse composable or custom |
|
||||
|
||||
**기본값**: 매 reusable reactive logic → composable. Single-use → inline.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Composition API]] · [[Vue 3]]
|
||||
- 응용: [[Pinia]]
|
||||
- Adjacent: [[Component-Composition]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: stateful logic 매 2+ components 에서 사용 / DOM·async 의 lifecycle wrapping.
|
||||
**언제 X**: 매 pure function (no reactivity) — 매 plain util 로 충분.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Returning reactive() with destructure**: loses reactivity → use `toRefs`.
|
||||
- **Global side-effects in composable body**: 매 `onMounted` 안에 넣을 것.
|
||||
- **Naming without `use` prefix**: 매 convention break, lint rule 매 fail.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vue.js docs / VueUse source).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Vue 3 composables with patterns |
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
id: wiki-2026-0508-composition-api
|
||||
title: Composition API (Vue 3)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Vue Composition API, setup script]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [vue, composition-api, reactivity, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Vue 3
|
||||
---
|
||||
|
||||
# Composition API (Vue 3)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 reactive primitive 으로 logic 을 조합한다"**. Composition API 는 Vue 3 의 `setup()` / `<script setup>` 기반 model — `ref`, `reactive`, `computed`, `watch` 를 직접 import 하여 매 logic 조각을 자유 조합, 매 Options API 의 `data/methods/computed` partition 을 대체.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 primitives
|
||||
- `ref(v)`: 매 wraps any value, `.value` access.
|
||||
- `reactive(obj)`: deep proxy — object/array 만.
|
||||
- `computed(fn)`: derived ref, lazy + cached.
|
||||
- `watch(src, cb)`: explicit deps + cb.
|
||||
- `watchEffect(fn)`: auto-track, eager.
|
||||
- `effectScope()`: manual lifecycle group.
|
||||
|
||||
### 매 vs Options API
|
||||
| Aspect | Options | Composition |
|
||||
|---|---|---|
|
||||
| Logic reuse | mixins (collision-prone) | composables (clean) |
|
||||
| TypeScript | OK | excellent |
|
||||
| File length scaling | grows by category | grows by feature |
|
||||
| Learning curve | gentle | steeper but worth it |
|
||||
|
||||
### 매 `<script setup>` perks
|
||||
- Top-level await.
|
||||
- Auto-expose for template.
|
||||
- `defineProps`, `defineEmits`, `defineModel`, `defineExpose` macros.
|
||||
- Compile-time optimizations (no `setup()` boilerplate).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic setup with ref + computed
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const count = ref(0);
|
||||
const double = computed(() => count.value * 2);
|
||||
const inc = () => count.value++;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="inc">{{ count }} (×2 = {{ double }})</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Typed props + emits + v-model
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ initial?: number }>();
|
||||
const emit = defineEmits<{ change: [value: number] }>();
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### reactive with toRefs (avoid losing reactivity)
|
||||
```ts
|
||||
import { reactive, toRefs } from 'vue';
|
||||
|
||||
export function useUser() {
|
||||
const state = reactive({ name: 'Ada', age: 36 });
|
||||
return { ...toRefs(state) }; // 매 destructurable + reactive
|
||||
}
|
||||
```
|
||||
|
||||
### watch with explicit source
|
||||
```ts
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const query = ref('');
|
||||
watch(query, async (q, _old, onCleanup) => {
|
||||
const ctrl = new AbortController();
|
||||
onCleanup(() => ctrl.abort());
|
||||
const r = await fetch(`/search?q=${q}`, { signal: ctrl.signal });
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### watchEffect (auto-track)
|
||||
```ts
|
||||
import { ref, watchEffect } from 'vue';
|
||||
|
||||
const userId = ref(1);
|
||||
watchEffect(async () => {
|
||||
const r = await fetch(`/api/users/${userId.value}`);
|
||||
user.value = await r.json();
|
||||
});
|
||||
```
|
||||
|
||||
### Provide / inject (typed)
|
||||
```ts
|
||||
// keys.ts
|
||||
import type { InjectionKey, Ref } from 'vue';
|
||||
export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme');
|
||||
|
||||
// Parent
|
||||
import { provide, ref } from 'vue';
|
||||
const theme = ref<'light' | 'dark'>('dark');
|
||||
provide(ThemeKey, theme);
|
||||
|
||||
// Child
|
||||
import { inject } from 'vue';
|
||||
const theme = inject(ThemeKey)!;
|
||||
```
|
||||
|
||||
### Async setup with Suspense
|
||||
```vue
|
||||
<!-- Parent -->
|
||||
<Suspense>
|
||||
<UserProfile :id="42" />
|
||||
<template #fallback><Skeleton /></template>
|
||||
</Suspense>
|
||||
|
||||
<!-- UserProfile.vue -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id: number }>();
|
||||
const user = await (await fetch(`/users/${props.id}`)).json();
|
||||
</script>
|
||||
```
|
||||
|
||||
### Lifecycle hooks
|
||||
```ts
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
onMounted(() => console.log('mounted'));
|
||||
onUnmounted(() => console.log('cleanup'));
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New Vue 3 project | Composition + `<script setup>` |
|
||||
| Migrating from Vue 2 | Options 유지 → 점진 conversion |
|
||||
| Logic reuse needed | Composable function |
|
||||
| Simple 1-off component | Either OK, prefer setup for TS |
|
||||
|
||||
**기본값**: `<script setup>` + Composition API. Options API 는 legacy maintenance only.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Vue 3]]
|
||||
- 변형: [[Vue Options API]] · [[Solid-Signals]]
|
||||
- 응용: [[Composables]] · [[Pinia]] · [[Nuxt]]
|
||||
- Adjacent: [[Component-Composition]] · [[TypeScript]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Vue 3 component 작성, composable 추출, TS 강한 typing 필요.
|
||||
**언제 X**: Vue 2.7 이전 — 매 backport limited.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Mixing reactive() destructure without toRefs**: loses reactivity silently.
|
||||
- **Using `ref.value` in template**: 매 unwrap 자동, `.value` 의 X.
|
||||
- **Excessive `watch`**: 매 computed 로 충분한 경우 매 prefer computed.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (vuejs.org official guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Composition API primitives + setup patterns |
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
id: wiki-2026-0508-computational-geometry
|
||||
title: Computational Geometry (Frontend)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [2D Geometry, Geometric Algorithms]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [geometry, algorithms, canvas, frontend, math]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Canvas / SVG / WebGL
|
||||
---
|
||||
|
||||
# Computational Geometry (Frontend)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 pixel 의 math 가 geometry 다"**. Hit-testing, polygon clipping, convex hull, spatial index — 매 canvas/SVG/Figma-style editor 의 core, 매 numerical robustness + spatial accel structure 가 핵심.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 fundamental ops
|
||||
- Point-in-polygon (ray casting / winding number).
|
||||
- Line-line / segment-segment intersection.
|
||||
- Polygon-polygon clipping (Sutherland-Hodgman, Weiler-Atherton).
|
||||
- Convex hull (Graham scan, Andrew's monotone chain).
|
||||
- Bounding box / sphere.
|
||||
- Distance: point-segment, point-polygon.
|
||||
|
||||
### 매 spatial accel
|
||||
- Quadtree — 2D static/dynamic.
|
||||
- R-tree — bbox-based, dynamic insert/delete (rbush lib).
|
||||
- Spatial hash — uniform grid.
|
||||
- BVH — for raycasting in 2D/3D.
|
||||
|
||||
### 매 numerical pitfalls
|
||||
- Floating-point cross product near zero → epsilon checks.
|
||||
- Robust orientation predicates (Shewchuk).
|
||||
- 매 SVG path parsing → degenerate cubic 처리.
|
||||
|
||||
### 매 응용
|
||||
1. Figma-style hit-testing.
|
||||
2. Map polygon labeling / clipping.
|
||||
3. Drag-select rectangle vs many shapes.
|
||||
4. Snap-to-grid / snap-to-edge.
|
||||
5. Boolean ops on shapes (union/intersect/difference).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Point-in-polygon (ray casting)
|
||||
```ts
|
||||
type Pt = { x: number; y: number };
|
||||
|
||||
export function pointInPolygon(p: Pt, poly: Pt[]): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
const a = poly[i], b = poly[j];
|
||||
const intersect =
|
||||
(a.y > p.y) !== (b.y > p.y) &&
|
||||
p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
```
|
||||
|
||||
### Segment-segment intersection
|
||||
```ts
|
||||
export function segIntersect(p1: Pt, p2: Pt, p3: Pt, p4: Pt): Pt | null {
|
||||
const d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
|
||||
if (Math.abs(d) < 1e-10) return null; // parallel
|
||||
const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d;
|
||||
const u = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / d;
|
||||
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
||||
return { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) };
|
||||
}
|
||||
```
|
||||
|
||||
### Convex hull (Andrew's monotone chain)
|
||||
```ts
|
||||
export function convexHull(pts: Pt[]): Pt[] {
|
||||
const p = [...pts].sort((a, b) => a.x - b.x || a.y - b.y);
|
||||
const cross = (o: Pt, a: Pt, b: Pt) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
||||
const lower: Pt[] = [];
|
||||
for (const pt of p) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pt) <= 0) lower.pop();
|
||||
lower.push(pt);
|
||||
}
|
||||
const upper: Pt[] = [];
|
||||
for (let i = p.length - 1; i >= 0; i--) {
|
||||
const pt = p[i];
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pt) <= 0) upper.pop();
|
||||
upper.push(pt);
|
||||
}
|
||||
upper.pop(); lower.pop();
|
||||
return lower.concat(upper);
|
||||
}
|
||||
```
|
||||
|
||||
### Bounding box
|
||||
```ts
|
||||
export function bbox(pts: Pt[]): { min: Pt; max: Pt } {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const { x, y } of pts) {
|
||||
if (x < minX) minX = x; if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x; if (y > maxY) maxY = y;
|
||||
}
|
||||
return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
|
||||
}
|
||||
```
|
||||
|
||||
### R-tree spatial index (rbush)
|
||||
```ts
|
||||
import RBush from 'rbush';
|
||||
|
||||
type Item = { minX: number; minY: number; maxX: number; maxY: number; id: string };
|
||||
const tree = new RBush<Item>();
|
||||
tree.load(items);
|
||||
|
||||
const hits = tree.search({ minX: 0, minY: 0, maxX: 100, maxY: 100 });
|
||||
```
|
||||
|
||||
### Point-segment distance
|
||||
```ts
|
||||
export function distPtSeg(p: Pt, a: Pt, b: Pt): number {
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const lenSq = dx * dx + dy * dy;
|
||||
if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
|
||||
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
||||
}
|
||||
```
|
||||
|
||||
### Polygon clipping (using martinez-polygon-clipping)
|
||||
```ts
|
||||
import * as martinez from 'martinez-polygon-clipping';
|
||||
|
||||
const subject = [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]];
|
||||
const clip = [[[5, 5], [15, 5], [15, 15], [5, 15], [5, 5]]];
|
||||
const intersection = martinez.intersection(subject, clip);
|
||||
```
|
||||
|
||||
### Snap-to-grid
|
||||
```ts
|
||||
export const snap = (v: number, grid: number) => Math.round(v / grid) * grid;
|
||||
export const snapPt = (p: Pt, g: number): Pt => ({ x: snap(p.x, g), y: snap(p.y, g) });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 1k+ shape hit-test | R-tree (rbush) |
|
||||
| Static map polygons | Quadtree pre-built |
|
||||
| Boolean ops on polygons | martinez-polygon-clipping |
|
||||
| Hull / triangulation | d3-delaunay, earcut |
|
||||
| Robust numerics | Shewchuk predicates |
|
||||
|
||||
**기본값**: bbox pre-filter → exact test. Spatial index 매 N>500.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[Computer-Graphics]] · [[GIS]]
|
||||
- 응용: [[SVG]] · [[WebGL]]
|
||||
- Adjacent: [[Collision-Detection]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: canvas editor, map UI, drag-select, snapping, boolean ops on shapes.
|
||||
**언제 X**: 매 trivial fixed UI — 매 CSS layout 으로 충분.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Naive O(N) hit-test on 10k shapes**: 매 lag — spatial index 필수.
|
||||
- **No epsilon in cross product**: 매 collinear 매 wrong branch.
|
||||
- **Re-building spatial tree per frame**: 매 only on data change, drag 매 incremental update.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (CGAL docs / "Computational Geometry: Algorithms and Applications" — de Berg et al.).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 2D geometry algorithms + spatial index patterns |
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: wiki-2026-0508-computed-properties-watchers
|
||||
title: Computed Properties & Watchers
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Vue computed, Vue watch, reactive-derivations]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [vue, reactivity, composition-api, computed, watch]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: vue3
|
||||
---
|
||||
|
||||
# Computed Properties & Watchers
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 derived state 의 declarative cache (`computed`), 매 side-effect 의 explicit subscription (`watch`)"**. 매 Vue 3.5 (2024) reactive system 의 두 축. 매 computed 는 pull-based memoization, watch 는 push-based callback.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 computed 의 본질
|
||||
- 매 dependency 의 자동 추적 → 매 read 시점 lazy evaluation.
|
||||
- 매 동일 input 의 cached return — Object.is 비교.
|
||||
- 매 ref 처럼 `.value` 의 unwrap.
|
||||
- 매 writable computed 의 setter 정의 가능.
|
||||
|
||||
### 매 watch / watchEffect 의 차이
|
||||
- `watch(source, cb)`: 매 explicit source — 매 old/new value 의 access.
|
||||
- `watchEffect(fn)`: 매 자동 dep tracking — 매 deps mutation 시 fn 의 re-run.
|
||||
- 매 flush timing: `'pre'` (default, before render) / `'post'` (after DOM) / `'sync'` (즉시).
|
||||
|
||||
### 매 Vue 3.5 reactivity 개선
|
||||
- 매 `computed` 의 lazy invalidation — 매 unused chain 의 skip.
|
||||
- 매 SSR friendly — server 에서 watch 의 no-op.
|
||||
- 매 onWatcherCleanup() 의 자동 cleanup hook.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic computed
|
||||
```typescript
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const firstName = ref('Yuna');
|
||||
const lastName = ref('Kim');
|
||||
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
||||
|
||||
console.log(fullName.value); // "Yuna Kim" — cached
|
||||
firstName.value = 'Jihoon';
|
||||
console.log(fullName.value); // 매 invalidate → recompute
|
||||
```
|
||||
|
||||
### Writable computed
|
||||
```typescript
|
||||
const count = ref(0);
|
||||
const double = computed({
|
||||
get: () => count.value * 2,
|
||||
set: (v) => { count.value = v / 2; },
|
||||
});
|
||||
|
||||
double.value = 10; // count.value === 5
|
||||
```
|
||||
|
||||
### watch (explicit source)
|
||||
```typescript
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const userId = ref(1);
|
||||
const user = ref<User | null>(null);
|
||||
|
||||
watch(userId, async (id, oldId, onCleanup) => {
|
||||
const ctrl = new AbortController();
|
||||
onCleanup(() => ctrl.abort()); // 매 stale request 의 cancel
|
||||
user.value = await fetchUser(id, { signal: ctrl.signal });
|
||||
}, { immediate: true });
|
||||
```
|
||||
|
||||
### watchEffect (auto-track)
|
||||
```typescript
|
||||
import { ref, watchEffect } from 'vue';
|
||||
|
||||
const query = ref('');
|
||||
const results = ref<Item[]>([]);
|
||||
|
||||
watchEffect(async (onCleanup) => {
|
||||
const ctrl = new AbortController();
|
||||
onCleanup(() => ctrl.abort());
|
||||
results.value = await searchAPI(query.value, { signal: ctrl.signal });
|
||||
});
|
||||
```
|
||||
|
||||
### Deep watch (object/array)
|
||||
```typescript
|
||||
const filters = reactive({ q: '', tags: [] });
|
||||
|
||||
watch(filters, (next) => {
|
||||
console.log('filters changed', JSON.parse(JSON.stringify(next)));
|
||||
}, { deep: true });
|
||||
```
|
||||
|
||||
### Multiple sources
|
||||
```typescript
|
||||
const x = ref(0), y = ref(0);
|
||||
watch([x, y], ([nx, ny], [ox, oy]) => {
|
||||
console.log(`(${ox},${oy}) → (${nx},${ny})`);
|
||||
});
|
||||
```
|
||||
|
||||
### Flush timing — DOM 접근
|
||||
```typescript
|
||||
const list = ref<string[]>([]);
|
||||
const containerRef = ref<HTMLElement>();
|
||||
|
||||
watch(list, () => {
|
||||
// 매 DOM 의 update 후 scrollTop 의 read
|
||||
containerRef.value!.scrollTop = containerRef.value!.scrollHeight;
|
||||
}, { flush: 'post' });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 derived value (template render) | `computed` |
|
||||
| 매 expensive computation + cache | `computed` |
|
||||
| 매 external side-effect (fetch, DOM) | `watch` / `watchEffect` |
|
||||
| 매 conditional dep tracking | `watch` (explicit source) |
|
||||
| 매 모든 reactive read 의 react | `watchEffect` |
|
||||
|
||||
**기본값**: 매 derive 는 `computed`, 매 effect 는 `watch` with explicit source.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Composition API]] · [[Vue_Single-File_Components_SFC]]
|
||||
- 변형: [[Composables]] · [[Options API]]
|
||||
- 응용: [[Pinia]] · [[Server_State_Management]]
|
||||
- Adjacent: [[State_Management]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 Vue 3 component 의 derived data 정의, 매 async data fetch 의 dependency 변경 추적, 매 form-validation reactive chain.
|
||||
**언제 X**: 매 React/Solid 의 코드 (각 framework 의 hook/signal 사용), 매 single-shot computation (그냥 함수).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 computed side-effect**: 매 getter 안에서 mutation/fetch — 매 cache invalidation 의 chaos.
|
||||
- **매 watch deep 의 남용**: 매 large object deep watch — 매 expensive equality check.
|
||||
- **매 immediate + cleanup 누락**: 매 첫 fetch 의 race — 매 onCleanup 의 mandatory.
|
||||
- **매 watchEffect 의 conditional dep**: 매 첫 run 에서 안 읽힌 ref 의 untracked.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vue 3.5 docs vuejs.org/api/reactivity-core, Evan You blog 2024).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — computed/watch 의 timing/cleanup 패턴 정리 |
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
id: wiki-2026-0508-concurrent-features
|
||||
title: Concurrent Features (React 18+)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Concurrent Mode, useTransition, useDeferredValue]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, concurrent, performance, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19
|
||||
---
|
||||
|
||||
# Concurrent Features (React 18+)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 render 매 interruptible 하다"**. React 18 부터 매 concurrent renderer — `useTransition`, `useDeferredValue`, `Suspense`, automatic batching, streaming SSR 매 모두 매 high-priority update 가 low-priority 를 preempt 가능한 architecture 위에 build.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 features
|
||||
- **Automatic batching**: promise / setTimeout / native event 모두 batch.
|
||||
- **`startTransition` / `useTransition`**: 매 non-urgent state update marking.
|
||||
- **`useDeferredValue`**: input 매 lag 없이 expensive list 매 deferred.
|
||||
- **`Suspense`**: data fetching boundary, fallback UI.
|
||||
- **Streaming SSR**: `renderToPipeableStream` (Node), `renderToReadableStream` (Edge).
|
||||
- **`useId`**: SSR-safe unique id.
|
||||
- **`use()` hook (React 19)**: unwrap promise/context inside render.
|
||||
|
||||
### 매 priority levels
|
||||
1. Sync / urgent (user input).
|
||||
2. Default (most state updates).
|
||||
3. Transition (`startTransition`).
|
||||
4. Idle.
|
||||
|
||||
### 매 응용
|
||||
1. 매 typeahead search — input snappy + results deferred.
|
||||
2. Tab switching — 매 stale UI keeps interactive.
|
||||
3. Streaming page render — 매 fastest paint then progressively fill.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### useTransition for non-urgent updates
|
||||
```tsx
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
function Filter({ items }: { items: Item[] }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [filtered, setFiltered] = useState(items);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const onChange = (v: string) => {
|
||||
setQuery(v); // urgent — input responds immediately
|
||||
startTransition(() => {
|
||||
setFiltered(items.filter((i) => i.name.includes(v))); // non-urgent
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={(e) => onChange(e.target.value)} />
|
||||
{isPending && <Spinner />}
|
||||
<List items={filtered} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useDeferredValue
|
||||
```tsx
|
||||
import { useState, useDeferredValue, memo } from 'react';
|
||||
|
||||
function Search({ items }: { items: Item[] }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const deferred = useDeferredValue(query);
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
<SlowList items={items} query={deferred} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SlowList = memo(({ items, query }: { items: Item[]; query: string }) => {
|
||||
return <ul>{items.filter((i) => i.name.includes(query)).map((i) => <li key={i.id}>{i.name}</li>)}</ul>;
|
||||
});
|
||||
```
|
||||
|
||||
### Suspense boundary
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<UserProfile id={42} />
|
||||
<Suspense fallback={<PostsSkeleton />}>
|
||||
<Posts userId={42} />
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### use() hook (React 19)
|
||||
```tsx
|
||||
import { use, Suspense } from 'react';
|
||||
|
||||
function Profile({ promise }: { promise: Promise<User> }) {
|
||||
const user = use(promise); // 매 unwrap inside render
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
|
||||
// caller
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Profile promise={fetchUser(42)} />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### Streaming SSR (Node)
|
||||
```ts
|
||||
import { renderToPipeableStream } from 'react-dom/server';
|
||||
|
||||
server.get('/', (req, res) => {
|
||||
const { pipe } = renderToPipeableStream(<App />, {
|
||||
bootstrapScripts: ['/main.js'],
|
||||
onShellReady() { res.statusCode = 200; pipe(res); },
|
||||
onError(err) { console.error(err); },
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Automatic batching across async
|
||||
```tsx
|
||||
function handleClick() {
|
||||
// React 18+: 매 둘 다 single render
|
||||
setTimeout(() => {
|
||||
setCount((c) => c + 1);
|
||||
setFlag((f) => !f);
|
||||
}, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### flushSync for opt-out
|
||||
```tsx
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
flushSync(() => setItems(next)); // 매 sync flush — DOM ready
|
||||
scrollToBottom(); // 매 새 item 매 already mounted
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Feature |
|
||||
|---|---|
|
||||
| Slow filter blocks input | `useTransition` or `useDeferredValue` |
|
||||
| Async data in component tree | `Suspense` + `use()` |
|
||||
| Need DOM after update | `flushSync` |
|
||||
| SSR slow first byte | streaming SSR |
|
||||
| Pre-React-18 codebase | upgrade first |
|
||||
|
||||
**기본값**: input UI 매 lag → `useDeferredValue`. Bigger state cascade → `useTransition`.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[Performance]]
|
||||
- 변형: [[Suspense]] · [[Server Components]] · [[Streaming SSR]]
|
||||
- 응용: [[Remix]]
|
||||
- Adjacent: [[Code Splitting]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 input lag, expensive list, async-heavy tree, SSR perf.
|
||||
**언제 X**: 매 trivial UI, 매 add-only sync state.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`startTransition` for urgent input**: 매 input 매 still feel laggy.
|
||||
- **No `Suspense` boundary 위 `use()`**: 매 throws 매 above-tree fallback 까지 propagate.
|
||||
- **Mixing `flushSync` everywhere**: 매 defeats concurrent benefits.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (react.dev official docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — React 18/19 concurrent features + patterns |
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: wiki-2026-0508-cross-platform-mobile-development-frameworks
|
||||
title: Cross-platform Mobile Development Frameworks
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [RN vs Flutter vs KMP, hybrid mobile]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [mobile, react-native, flutter, kmp, capacitor]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: multi
|
||||
framework: multi
|
||||
---
|
||||
|
||||
# Cross-platform Mobile Development Frameworks
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 single codebase → multiple OS targets — runtime tradeoff 의 spectrum"**. 매 2026 landscape 는 RN(JS+native), Flutter(Skia/Impeller), Kotlin Multiplatform(shared logic + native UI), Capacitor(WebView) 의 4 축. 매 native parity vs dev velocity vs binary size 의 결정.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4 가지 architectural model
|
||||
1. **Embedded JS runtime + native widgets**: React Native (Hermes/JSC + UIKit/Android View).
|
||||
2. **Embedded engine + own renderer**: Flutter (Dart AOT + Impeller GPU).
|
||||
3. **Shared logic + native UI**: Kotlin Multiplatform (KMP) — UI 는 SwiftUI/Compose.
|
||||
4. **WebView wrapper**: Capacitor / Tauri Mobile / PWA — 매 web stack 그대로.
|
||||
|
||||
### 매 2026 의 maturity 비교
|
||||
| 축 | RN 0.76+ | Flutter 3.27 | KMP 2.1 | Capacitor 7 |
|
||||
|---|---|---|---|---|
|
||||
| native look | 매 high | 매 own widgets | 매 native | 매 medium |
|
||||
| perf | 매 JSI direct | 매 Impeller GPU | 매 native | 매 WebView |
|
||||
| binary | ~25MB | ~20MB | ~8MB | ~10MB |
|
||||
| hot reload | 매 fast | 매 fastest | 매 medium | 매 instant |
|
||||
|
||||
### 매 선택의 실제 driver
|
||||
- 매 dev team 의 existing skill (TS vs Dart vs Kotlin).
|
||||
- 매 design 의 brand-custom 정도 (Flutter 우세).
|
||||
- 매 native API depth (RN 의 거대 ecosystem).
|
||||
- 매 backend 와의 code share (KMP 우세).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React Native (Bridgeless)
|
||||
```typescript
|
||||
// App.tsx
|
||||
import { View, Text, Pressable } from 'react-native';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<Pressable onPress={() => console.log('tap')}>
|
||||
<Text>Hello RN 0.76</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Flutter (Material 3)
|
||||
```dart
|
||||
// main.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
@override
|
||||
Widget build(BuildContext c) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(child: FilledButton(
|
||||
onPressed: () => debugPrint('tap'),
|
||||
child: const Text('Hello Flutter'),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Kotlin Multiplatform (shared module)
|
||||
```kotlin
|
||||
// shared/src/commonMain/kotlin/Greeting.kt
|
||||
class Greeting {
|
||||
fun greet(): String = "Hello from ${Platform().name}"
|
||||
}
|
||||
|
||||
expect class Platform() {
|
||||
val name: String
|
||||
}
|
||||
// androidMain: actual class Platform { actual val name = "Android" }
|
||||
// iosMain: actual class Platform { actual val name = UIDevice.currentDevice.systemName() }
|
||||
```
|
||||
|
||||
### Capacitor (web → native shell)
|
||||
```typescript
|
||||
import { Camera, CameraResultType } from '@capacitor/camera';
|
||||
|
||||
const photo = await Camera.getPhoto({
|
||||
resultType: CameraResultType.Uri,
|
||||
quality: 90,
|
||||
});
|
||||
console.log(photo.webPath);
|
||||
```
|
||||
|
||||
### 매 platform-specific code split (RN)
|
||||
```typescript
|
||||
// Button.ios.tsx / Button.android.tsx — Metro 의 자동 resolve
|
||||
import { Platform } from 'react-native';
|
||||
const haptic = Platform.select({
|
||||
ios: () => require('react-native-haptic-feedback').trigger('impactMedium'),
|
||||
android: () => require('react-native-vibration').vibrate(50),
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 web team 의 mobile 진입 | React Native |
|
||||
| 매 brand-custom UI + 60fps animation | Flutter |
|
||||
| 매 existing Android/iOS app 의 logic share | Kotlin Multiplatform |
|
||||
| 매 enterprise content app + minimal native | Capacitor / PWA |
|
||||
| 매 highest native parity 필요 | 매 native (Swift+Kotlin) |
|
||||
|
||||
**기본값**: 매 startup MVP 는 RN, 매 design-driven product 는 Flutter, 매 brownfield enterprise 는 KMP.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Frontend]]
|
||||
- 변형: [[React_Native]] · [[Flutter]] · [[Bridgeless_New_Architecture]]
|
||||
- 응용: [[Expo_Router]] · [[Hermes]] · [[Impeller]]
|
||||
- Adjacent: [[Dart]] · [[JavaScriptCore]] · [[Skia]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 framework 선택 의 decision 지원, 매 platform-specific limitation (image picker, BLE) lookup, 매 perf trade-off 의 정량 비교.
|
||||
**언제 X**: 매 native-only API (CarPlay, Vision Pro full) 가 critical 한 경우 — 매 native 의 권장.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 framework hopping**: 매 1년마다 RN→Flutter→KMP — 매 sunk cost 의 누적.
|
||||
- **매 WebView 로 모든 것**: 매 perf-critical screen 의 WebView 내장 — 매 jank 의 발생.
|
||||
- **매 native module 의 abandon**: 매 unmaintained community module 의 prod 배포.
|
||||
- **매 platform divergence 무시**: 매 iOS HIG vs Material 의 cross-paste — 매 user 위화감.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (RN 0.76 release 2024-10, Flutter 3.27 docs, KMP Stable 2023-11, Capacitor 7 release).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — RN/Flutter/KMP/Capacitor 의 4-axis comparison |
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
id: wiki-2026-0508-custom-hooks-patterns
|
||||
title: Custom Hooks Patterns
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Custom Hooks, useXxx Patterns]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, hooks, patterns]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19
|
||||
---
|
||||
|
||||
# Custom Hooks Patterns
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 custom hook = stateful logic 의 재사용 unit, JSX 의 X."**. React 19 (2024) `use()` + Server Components 시대에는 custom hook이 매 client-side state machine + side effect orchestration 의 primary abstraction. 매 naming은 `useXxx` — 매 ESLint react-hooks rule 의 enforce.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 규칙 (Rules of Hooks)
|
||||
- Top-level 만 호출 — 매 conditional / loop X.
|
||||
- React function / 매 다른 hook 안에서 만 — 매 일반 function X.
|
||||
- Naming `useXxx` — 매 lint 가 의존.
|
||||
|
||||
### 매 composition
|
||||
- Hook = 매 다른 hook 의 조합 — `useMutation` ← `useState` + `useCallback` + `useRef`.
|
||||
- 매 stateful logic 의 분리 + 매 testable 단위.
|
||||
|
||||
### 매 응용
|
||||
1. Data fetching — `useQuery`, `useSWR`.
|
||||
2. DOM observation — `useIntersectionObserver`, `useResizeObserver`.
|
||||
3. State machines — `useToggle`, `useReducer` wrapper.
|
||||
4. Browser API — `useLocalStorage`, `useMediaQuery`, `useGeolocation`.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. useToggle (state primitive)
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useToggle(initial = false) {
|
||||
const [on, setOn] = useState(initial);
|
||||
const toggle = useCallback(() => setOn((v) => !v), []);
|
||||
const setOnTrue = useCallback(() => setOn(true), []);
|
||||
const setOnFalse = useCallback(() => setOn(false), []);
|
||||
return { on, toggle, setOnTrue, setOnFalse };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. useDebouncedValue
|
||||
```typescript
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useDebouncedValue<T>(value: T, delayMs = 300): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => setDebounced(value), delayMs);
|
||||
return () => clearTimeout(id);
|
||||
}, [value, delayMs]);
|
||||
return debounced;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. useEventListener (typed)
|
||||
```typescript
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function useEventListener<K extends keyof WindowEventMap>(
|
||||
type: K,
|
||||
handler: (e: WindowEventMap[K]) => void,
|
||||
target: Window | HTMLElement = window,
|
||||
) {
|
||||
const handlerRef = useRef(handler);
|
||||
useEffect(() => { handlerRef.current = handler; });
|
||||
useEffect(() => {
|
||||
const listener = (e: Event) => handlerRef.current(e as WindowEventMap[K]);
|
||||
target.addEventListener(type, listener);
|
||||
return () => target.removeEventListener(type, listener);
|
||||
}, [type, target]);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. useIntersectionObserver
|
||||
```typescript
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useIntersectionObserver<T extends Element>(
|
||||
options: IntersectionObserverInit = {},
|
||||
) {
|
||||
const ref = useRef<T>(null);
|
||||
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const obs = new IntersectionObserver(([e]) => setEntry(e), options);
|
||||
obs.observe(ref.current);
|
||||
return () => obs.disconnect();
|
||||
}, [options.root, options.rootMargin, options.threshold]);
|
||||
return { ref, entry, isVisible: entry?.isIntersecting ?? false };
|
||||
}
|
||||
```
|
||||
|
||||
### 5. useLocalStorage (with sync)
|
||||
```typescript
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useLocalStorage<T>(key: string, initial: T) {
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
const raw = localStorage.getItem(key);
|
||||
return raw ? (JSON.parse(raw) as T) : initial;
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}, [key, value]);
|
||||
useEffect(() => {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue) setValue(JSON.parse(e.newValue));
|
||||
};
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => window.removeEventListener('storage', onStorage);
|
||||
}, [key]);
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. useFetch (with AbortController)
|
||||
```typescript
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useFetch<T>(url: string) {
|
||||
const [state, setState] = useState<{ data?: T; error?: Error; loading: boolean }>({ loading: true });
|
||||
useEffect(() => {
|
||||
const ac = new AbortController();
|
||||
setState({ loading: true });
|
||||
fetch(url, { signal: ac.signal })
|
||||
.then((r) => r.json())
|
||||
.then((data: T) => setState({ data, loading: false }))
|
||||
.catch((error: Error) => {
|
||||
if (error.name !== 'AbortError') setState({ error, loading: false });
|
||||
});
|
||||
return () => ac.abort();
|
||||
}, [url]);
|
||||
return state;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. useReducer composite (state machine)
|
||||
```typescript
|
||||
import { useReducer } from 'react';
|
||||
|
||||
type State = { status: 'idle' | 'loading' | 'success' | 'error'; data?: unknown; error?: Error };
|
||||
type Action = { type: 'fetch' } | { type: 'success'; data: unknown } | { type: 'error'; error: Error };
|
||||
|
||||
const reducer = (s: State, a: Action): State => {
|
||||
switch (a.type) {
|
||||
case 'fetch': return { status: 'loading' };
|
||||
case 'success': return { status: 'success', data: a.data };
|
||||
case 'error': return { status: 'error', error: a.error };
|
||||
}
|
||||
};
|
||||
|
||||
export const useAsync = () => useReducer(reducer, { status: 'idle' });
|
||||
```
|
||||
|
||||
### 8. React 19 `use()` for promises
|
||||
```typescript
|
||||
import { use, Suspense } from 'react';
|
||||
|
||||
function Profile({ promise }: { promise: Promise<User> }) {
|
||||
const user = use(promise); // suspends until resolved
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Local boolean | `useToggle`. |
|
||||
| Server data | `useQuery` (TanStack Query) — 매 wheel 재발명 X. |
|
||||
| 매 cross-tab sync | `useLocalStorage` + `storage` event. |
|
||||
| Async resource | React 19 `use()` + Suspense. |
|
||||
| 복잡 state | `useReducer` 매 state machine. |
|
||||
| 매 DOM measurement | `useResizeObserver`, `useIntersectionObserver`. |
|
||||
|
||||
**기본값**: 매 TanStack Query / Zustand 의 use — custom hook은 매 truly specific logic 만.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]]
|
||||
- 변형: [[Vue Composables]]
|
||||
- 응용: [[Component Library]] · [[Design System]]
|
||||
- Adjacent: [[useEffect]] · [[State Management]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 hook scaffolding, TS generic 작성, 매 cleanup logic.
|
||||
**언제 X**: 매 stale-closure / dep array 미세 bug — 매 manual review 필요.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Conditional hook call**: 매 `if (x) useFoo()` — 매 lint error.
|
||||
- **Stale closure in setInterval**: 매 ref pattern 또는 functional setState 사용.
|
||||
- **Effect for derived state**: 매 `useMemo` 또는 render 중 계산 — `useEffect` X.
|
||||
- **No cleanup**: 매 listener / subscription 미해제 — leak.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (react.dev, React 19 release notes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — React 19 hook patterns + use() |
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Custom Hooks
|
||||
description: "커스텀 훅(Custom Hooks)은 React 애플리케이션에서 상태 기반 로직을 추출하여 컴포넌트 간에 쉽게 공유할 수 있도록 고안된 함수형 디자인 패턴입니다 [1, 2]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Custom Hooks
|
||||
|
||||
## 📌 Brief Summary
|
||||
커스텀 훅(Custom Hooks)은 React 애플리케이션에서 상태 기반 로직을 추출하여 컴포넌트 간에 쉽게 공유할 수 있도록 고안된 함수형 디자인 패턴입니다 [1, 2]. `use`로 시작하는 명명 규칙을 따르며, 내부에 다른 React 훅들을 호출하여 새로운 맞춤형 기능을 조립할 수 있습니다 [1, 3]. 기존의 고차 컴포넌트(HOC)나 렌더 프로프(Render Props)가 유발하던 불필요한 컴포넌트 중첩(Wrapper Hell)을 제거하고, 함수 합성을 통해 깔끔하게 로직을 재사용할 수 있게 해줍니다 [1, 4].
|
||||
|
||||
## 📖 Core Content
|
||||
* **상태 기반 로직의 캡슐화 및 재사용**:
|
||||
커스텀 훅은 API 데이터 페칭, 폼 상태 관리, 반응형 디자인 등 UI 렌더링과 직접적인 관련이 없는 순수 비즈니스 및 상태 로직을 캡슐화하는 데 사용됩니다 [2, 5]. 이를 통해 여러 컴포넌트 사이에서 코드를 DRY(Don't Repeat Yourself)하게 유지하고 깔끔한 코드베이스를 작성할 수 있습니다 [5].
|
||||
* **함수 합성(Function Composition)을 통한 확장**:
|
||||
복잡한 행위들을 단순한 형태의 훅으로 쪼개고 이를 다시 합성하여 강력한 기능을 구현할 수 있습니다 [1, 6]. 예를 들어, 디바운싱 로직을 다루는 `useDebounce`와 데이터를 가져오는 `useFetch`, 검색 로직을 다루는 `useSearch`를 각각 만들고, 이들을 하나의 훅에서 조합하여 효율적으로 관리할 수 있습니다 [1, 6].
|
||||
* **타입스크립트(TypeScript)와의 시너지**:
|
||||
타입스크립트의 제네릭(Generics)을 커스텀 훅에 적용하면(`useFetch<T>` 등) 재사용성을 희생하지 않으면서도 강력한 타입 안전성(Type Safety)을 얻을 수 있습니다 [7]. 이는 엔터프라이즈 환경의 대규모 프로젝트에서 안전하고 견고한 코드를 구축하는 핵심 요소로 평가받습니다 [2, 8].
|
||||
* **독립적인 테스트와 관심사 분리**:
|
||||
각 커스텀 훅은 하나의 책임만을 가지도록 집중적으로 설계해야 합니다 [3]. 추출된 훅은 특정 UI에 종속되지 않기 때문에 `@testing-library/react`의 `renderHook`과 같은 도구를 사용하여 독립적이고 쉽게 테스트할 수 있습니다 [3, 8].
|
||||
* **필수적인 설계 모범 사례(Best Practices)**:
|
||||
훅을 작성할 때는 React 린팅 규칙이 의존하는 `use` 접두사를 반드시 사용해야 합니다 [3]. 다운스트림의 메모이제이션이 깨지는 것을 막기 위해 `useCallback`과 `useMemo`를 이용해 반환되는 참조를 안정적으로 유지하는 것이 중요합니다 [3]. 또한 이벤트나 외부 자원을 구독할 때에는 `useEffect`에서 항상 클린업(cleanup) 함수를 반환하여 누수를 방지해야 합니다 [3].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **엄격한 훅의 규칙(Rules of Hooks) 강제**:
|
||||
커스텀 훅을 사용할 때는 React의 훅 규칙을 반드시 준수해야 한다는 문법적, 구조적 제약이 따릅니다 [2, 9]. 이를 어길 경우 런타임 오류나 예상치 못한 상태 오류가 발생할 수 있습니다 [5].
|
||||
* **오래된 클로저(Stale Closure) 문제**:
|
||||
`useEffect` 등과 함께 커스텀 훅을 구현할 때, 참조되는 모든 값을 의존성 배열(dependency array)에 올바르게 명시하지 않으면 함수가 과거의 렌더링 변수 값에 갇히는 '오래된 클로저' 현상이 발생합니다 [7]. 변경 추적이 아닌 호출 시점에만 읽혀야 하는 값들은 `ref`를 사용해 우회해야 합니다 [7].
|
||||
* **성능 최적화 오용(Vibe Coding)의 함정**:
|
||||
성능을 개선한다는 맹목적인 믿음 하에 객관적인 성능 측정 없이 `useCallback`이나 `useMemo` 등을 무분별하게 추가하면 오히려 디버깅을 어렵게 만들고, 컴포넌트의 성능을 악화시키는 부작용을 초래할 수 있습니다 [10, 11].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: wiki-2026-0508-dart
|
||||
title: Dart
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Dart language, dart-lang]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [dart, flutter, language, aot, jit]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: dart
|
||||
framework: flutter
|
||||
---
|
||||
|
||||
# Dart
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 client-optimized language — JIT(dev) + AOT(prod) dual compile, sound null safety, isolate concurrency"**. 매 Google 의 Flutter 전용으로 설계 (2011→2018 pivot). 매 Dart 3.3+ (2024-2026) 의 records, patterns, sealed class 의 modern type system 도입.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 dual compilation
|
||||
- **JIT (development)**: 매 hot reload < 1s — Flutter dev 의 핵심.
|
||||
- **AOT (production)**: 매 native machine code — startup 빠름, deterministic perf.
|
||||
- **dart2js / dart2wasm**: 매 web target — Flutter Web 의 binary.
|
||||
|
||||
### 매 sound null safety (since 2.12)
|
||||
- 매 `String?` (nullable) vs `String` (non-null) 의 compile-time 보장.
|
||||
- 매 `late` keyword — 매 deferred init.
|
||||
- 매 `!` (bang) — 매 명시적 unwrap.
|
||||
|
||||
### 매 Dart 3 의 modern features
|
||||
- **Records**: `(int, String)` lightweight tuple.
|
||||
- **Patterns**: destructuring + switch expression.
|
||||
- **Sealed classes**: 매 exhaustive matching.
|
||||
- **Class modifiers**: `final`, `interface`, `base`, `sealed`.
|
||||
|
||||
### 매 isolate concurrency
|
||||
- 매 single-threaded event loop per isolate.
|
||||
- 매 message-passing (no shared memory).
|
||||
- 매 `Isolate.run()` (Dart 2.19+) 의 simple offload.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Null safety + late
|
||||
```dart
|
||||
class User {
|
||||
final String name;
|
||||
final String? nickname; // nullable
|
||||
late final int age; // deferred init
|
||||
|
||||
User(this.name, {this.nickname});
|
||||
|
||||
void setAge(int a) => age = a;
|
||||
}
|
||||
|
||||
final u = User('Yuna');
|
||||
print(u.nickname?.toUpperCase() ?? 'NO NICK');
|
||||
```
|
||||
|
||||
### Records (Dart 3)
|
||||
```dart
|
||||
(double, double) toPolar(double x, double y) =>
|
||||
(sqrt(x * x + y * y), atan2(y, x));
|
||||
|
||||
final (r, theta) = toPolar(3, 4);
|
||||
print('r=$r, θ=$theta');
|
||||
|
||||
// Named record
|
||||
({String name, int age}) profile = (name: 'Yuna', age: 28);
|
||||
print(profile.name);
|
||||
```
|
||||
|
||||
### Patterns + switch expression
|
||||
```dart
|
||||
sealed class Shape {}
|
||||
class Circle extends Shape { final double r; Circle(this.r); }
|
||||
class Square extends Shape { final double s; Square(this.s); }
|
||||
|
||||
double area(Shape sh) => switch (sh) {
|
||||
Circle(:final r) => 3.14159 * r * r,
|
||||
Square(:final s) => s * s,
|
||||
};
|
||||
```
|
||||
|
||||
### Async/await + Future
|
||||
```dart
|
||||
Future<User> fetchUser(int id) async {
|
||||
final res = await http.get(Uri.parse('https://api/u/$id'));
|
||||
if (res.statusCode != 200) throw Exception('fail');
|
||||
return User.fromJson(jsonDecode(res.body));
|
||||
}
|
||||
|
||||
// Stream — 매 multiple async value
|
||||
Stream<int> count(int n) async* {
|
||||
for (var i = 0; i < n; i++) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Isolate (compute)
|
||||
```dart
|
||||
import 'dart:isolate';
|
||||
|
||||
Future<int> heavyCompute(List<int> data) =>
|
||||
Isolate.run(() => data.fold(0, (a, b) => a + b * b));
|
||||
|
||||
// 매 main isolate 의 block 없이 background work
|
||||
final sum = await heavyCompute(List.generate(1_000_000, (i) => i));
|
||||
```
|
||||
|
||||
### Extension methods
|
||||
```dart
|
||||
extension StringX on String {
|
||||
String get capitalized => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||
}
|
||||
|
||||
print('hello'.capitalized); // "Hello"
|
||||
```
|
||||
|
||||
### Mixin
|
||||
```dart
|
||||
mixin Loggable {
|
||||
void log(String msg) => print('[${runtimeType}] $msg');
|
||||
}
|
||||
|
||||
class Service with Loggable {
|
||||
void doWork() => log('working...');
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 Flutter mobile/desktop app | Dart (의무) |
|
||||
| 매 CLI tool (cross-platform) | Dart AOT compile |
|
||||
| 매 backend (Aqueduct/Shelf) | 매 가능 — 매 ecosystem 작음 |
|
||||
| 매 web (no Flutter) | 매 비추천 — TS/JS 우세 |
|
||||
|
||||
**기본값**: 매 Flutter context 에서만 Dart 의 사용 — 매 standalone 은 Go/Rust/TS 의 권장.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Flutter]]
|
||||
- 변형: [[Flutter_AOT_Compilation_Dart]] · [[Dart_FFI_Foreign_Function_Interface]]
|
||||
- 응용: [[Flutter_Web_-_Desktop]] · [[Flutter_Impeller]]
|
||||
- Adjacent: [[TypeScript_Type_Safety]] · [[Cross-platform_Mobile_Development_Frameworks]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 Flutter widget tree 작성, 매 isolate 기반 background work, 매 records/patterns 활용 의 modern Dart.
|
||||
**언제 X**: 매 Flutter 외부 — 매 다른 ecosystem 에서 Dart 의 사용 의 의미 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 dynamic 의 남용**: 매 type system 의 우회 — 매 sound null safety 의 무력화.
|
||||
- **매 sync await chain**: 매 Future.wait 의 미사용 — 매 serialized 대기.
|
||||
- **매 isolate 와 closure**: 매 capture-by-reference 의 함정 (sendable 만 가능).
|
||||
- **매 print debug**: 매 debugPrint / logger 의 권장.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (dart.dev language tour, Dart 3.3 release 2024, Flutter 3.27).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Dart 3 records/patterns/sealed 정리 |
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
id: wiki-2026-0508-dart-ffi-foreign-function-interface
|
||||
title: Dart FFI (Foreign Function Interface)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [dart:ffi, native interop]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [dart, flutter, ffi, native, c-interop]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: dart
|
||||
framework: flutter
|
||||
---
|
||||
|
||||
# Dart FFI (Foreign Function Interface)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Dart ↔ C/C++ 의 zero-overhead direct call — `dart:ffi`"**. 매 Flutter 3.x 부터 stable + `ffigen` 의 자동 binding 생성. 매 native lib (SQLite, OpenCV, Rust crate) 의 직접 호출 — 매 platform channel 의 async overhead 우회.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 FFI 의 작동 원리
|
||||
- 매 `DynamicLibrary.open()` 의 .so/.dylib/.dll load.
|
||||
- 매 C ABI signature 의 Dart `Native<T>` typedef mapping.
|
||||
- 매 sync call — 매 Dart isolate 와 동일 thread 에서 즉시 실행.
|
||||
- 매 Pointer<T> 의 manual memory management.
|
||||
|
||||
### 매 vs Platform Channel
|
||||
| 축 | FFI | Platform Channel |
|
||||
|---|---|---|
|
||||
| latency | μs | ms |
|
||||
| sync | yes | no (async) |
|
||||
| target | C/C++/Rust | Java/Kotlin/Swift |
|
||||
| async-safe | 매 isolate | yes |
|
||||
|
||||
### 매 ffigen workflow
|
||||
- 매 C header (`.h`) → ffigen → Dart binding 자동 생성.
|
||||
- 매 manual typedef 작성 의 elimination.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic FFI call
|
||||
```dart
|
||||
// hello.dart
|
||||
import 'dart:ffi';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
typedef CAdd = Int32 Function(Int32, Int32);
|
||||
typedef DartAdd = int Function(int, int);
|
||||
|
||||
final lib = DynamicLibrary.open(
|
||||
Platform.isMacOS ? 'libmath.dylib' : 'libmath.so',
|
||||
);
|
||||
final add = lib.lookupFunction<CAdd, DartAdd>('add');
|
||||
|
||||
void main() {
|
||||
print(add(2, 3)); // 5
|
||||
}
|
||||
```
|
||||
|
||||
### Native annotation (Dart 3.3+)
|
||||
```dart
|
||||
import 'dart:ffi';
|
||||
|
||||
@Native<Int32 Function(Int32, Int32)>(symbol: 'add', isLeaf: true)
|
||||
external int add(int a, int b);
|
||||
|
||||
void main() => print(add(2, 3));
|
||||
```
|
||||
|
||||
### String marshalling
|
||||
```dart
|
||||
import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
typedef CGreet = Pointer<Utf8> Function(Pointer<Utf8>);
|
||||
typedef DartGreet = Pointer<Utf8> Function(Pointer<Utf8>);
|
||||
|
||||
final greet = lib.lookupFunction<CGreet, DartGreet>('greet');
|
||||
|
||||
final namePtr = 'Yuna'.toNativeUtf8();
|
||||
final resultPtr = greet(namePtr);
|
||||
final result = resultPtr.toDartString();
|
||||
calloc.free(namePtr); // 매 manual free 의 필수
|
||||
print(result);
|
||||
```
|
||||
|
||||
### Struct passing
|
||||
```dart
|
||||
final class Point extends Struct {
|
||||
@Double() external double x;
|
||||
@Double() external double y;
|
||||
}
|
||||
|
||||
typedef CDistance = Double Function(Pointer<Point>, Pointer<Point>);
|
||||
typedef DartDistance = double Function(Pointer<Point>, Pointer<Point>);
|
||||
|
||||
final dist = lib.lookupFunction<CDistance, DartDistance>('distance');
|
||||
final a = calloc<Point>()..ref.x = 0..ref.y = 0;
|
||||
final b = calloc<Point>()..ref.x = 3..ref.y = 4;
|
||||
print(dist(a, b)); // 5.0
|
||||
calloc.free(a); calloc.free(b);
|
||||
```
|
||||
|
||||
### ffigen 의 사용
|
||||
```yaml
|
||||
# pubspec.yaml
|
||||
dev_dependencies:
|
||||
ffigen: ^14.0.0
|
||||
|
||||
# ffigen.yaml
|
||||
name: NativeMath
|
||||
description: Bindings for libmath
|
||||
output: lib/src/math_bindings.dart
|
||||
headers:
|
||||
entry-points: ['native/math.h']
|
||||
```
|
||||
|
||||
```bash
|
||||
dart run ffigen --config ffigen.yaml
|
||||
```
|
||||
|
||||
### Async FFI (NativePort)
|
||||
```dart
|
||||
// 매 long-running C work 의 main isolate block 회피
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
final port = ReceivePort();
|
||||
final nativePort = port.sendPort.nativePort;
|
||||
|
||||
// C 측 에서 Dart_PostCObject_DL(nativePort, ...) 의 post
|
||||
port.listen((msg) => print('from native: $msg'));
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 sync μs-latency native call | FFI |
|
||||
| 매 platform-specific iOS/Android API | Platform Channel |
|
||||
| 매 large C/C++ codebase reuse | FFI + ffigen |
|
||||
| 매 Rust integration | FFI + flutter_rust_bridge |
|
||||
| 매 simple 1-shot file/permission | Pigeon plugin |
|
||||
|
||||
**기본값**: 매 native lib 의 binding → ffigen, 매 OS API 호출 → Pigeon/MethodChannel.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Flutter]] · [[Dart]]
|
||||
- 변형: [[Flutter_AOT_Compilation_Dart]]
|
||||
- 응용: [[Flutter_Impeller]] · [[Skia]]
|
||||
- Adjacent: [[WebAssembly]] · [[JSI (JavaScript Interface)]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 Flutter 의 native lib (sqlite, ffmpeg, opencv, ort) 통합, 매 Rust crate 의 mobile 노출, 매 perf-critical CPU work 의 offload.
|
||||
**언제 X**: 매 simple JSON-style platform API — 매 Pigeon 의 권장.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 free 누락**: 매 calloc 후 free X — 매 native heap leak.
|
||||
- **매 leaf 미표시**: 매 isLeaf=true 누락 — 매 GC safepoint overhead.
|
||||
- **매 isolate 안전 무시**: 매 long C call 의 main isolate block.
|
||||
- **매 manual binding**: 매 ffigen 의 skip — 매 type drift 의 발생.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (dart.dev/guides/libraries/c-interop, Dart 3.3 release 2024-02, ffigen 14 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — FFI 의 sync interop, ffigen workflow 정리 |
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
id: wiki-2026-0508-datacollector-knowledge-hub
|
||||
title: Datacollector Knowledge Hub
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Web Analytics Collector, Frontend Telemetry Pipeline]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [analytics, telemetry, observability, data-collection]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Browser/Edge
|
||||
---
|
||||
|
||||
# Datacollector Knowledge Hub
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 datacollector = browser event → edge ingest → warehouse 의 매 reliable pipeline."**. 매 frontend datacollector는 매 user behavior, performance (Web Vitals), error 의 capture → 매 batch / sendBeacon → edge function (Cloudflare Workers / Vercel) → 매 ClickHouse / BigQuery / Snowflake. 매 2026 perspective는 매 1st-party domain ingest + GDPR/ePrivacy 동의 + server-side GTM.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 collector responsibilities
|
||||
- **Capture**: page view, click, custom event, performance, error.
|
||||
- **Enrich**: session id, user id (consent-gated), referrer, UTM.
|
||||
- **Batch & Buffer**: idle batching, sendBeacon on `pagehide`.
|
||||
- **Privacy**: consent state, IP truncation, PII scrubbing.
|
||||
- **Transport**: 1st-party endpoint > 3rd-party (avoid adblock).
|
||||
|
||||
### 매 Web Vitals
|
||||
- LCP, INP, CLS, TTFB, FCP — `web-vitals` library.
|
||||
- 매 attribution build (`onINP({ reportAllChanges: true })`) 매 root-cause.
|
||||
|
||||
### 매 응용
|
||||
1. Product analytics (PostHog, Amplitude, Mixpanel, Segment).
|
||||
2. RUM (Datadog, Sentry, New Relic, SpeedCurve).
|
||||
3. A/B testing exposure logging.
|
||||
4. Funnel & cohort analysis.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Minimal collector (sendBeacon + queue)
|
||||
```typescript
|
||||
type Event = { type: string; ts: number; props?: Record<string, unknown> };
|
||||
const queue: Event[] = [];
|
||||
const ENDPOINT = '/collect'; // 1st-party
|
||||
|
||||
function track(type: string, props?: Event['props']) {
|
||||
queue.push({ type, ts: Date.now(), props });
|
||||
if (queue.length >= 20) flush();
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if (!queue.length) return;
|
||||
const batch = queue.splice(0, queue.length);
|
||||
const blob = new Blob([JSON.stringify(batch)], { type: 'application/json' });
|
||||
navigator.sendBeacon(ENDPOINT, blob) || fetch(ENDPOINT, { method: 'POST', body: blob, keepalive: true });
|
||||
}
|
||||
|
||||
addEventListener('pagehide', flush);
|
||||
addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') flush(); });
|
||||
```
|
||||
|
||||
### 2. Web Vitals capture
|
||||
```typescript
|
||||
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals';
|
||||
const send = (metric: { name: string; value: number; id: string }) =>
|
||||
track('vital', { name: metric.name, value: metric.value, id: metric.id });
|
||||
onLCP(send); onINP(send); onCLS(send); onTTFB(send);
|
||||
```
|
||||
|
||||
### 3. Error capture (global)
|
||||
```typescript
|
||||
window.addEventListener('error', (e) => {
|
||||
track('error', { msg: e.message, src: e.filename, line: e.lineno, col: e.colno, stack: e.error?.stack });
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
track('promise_rejection', { reason: String(e.reason) });
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Auto click tracking (delegated)
|
||||
```typescript
|
||||
document.addEventListener('click', (e) => {
|
||||
const t = (e.target as Element).closest('[data-track]');
|
||||
if (t instanceof HTMLElement) {
|
||||
track('click', { id: t.dataset.track, ...t.dataset });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Consent gating (TCFv2)
|
||||
```typescript
|
||||
function hasAnalyticsConsent(): boolean {
|
||||
const tcData = (window as any).__tcfapi;
|
||||
// simplified — production: subscribe to TCF api
|
||||
return localStorage.getItem('consent.analytics') === '1';
|
||||
}
|
||||
const origTrack = track;
|
||||
const trackGated = (type: string, props?: Event['props']) => {
|
||||
if (!hasAnalyticsConsent()) return;
|
||||
origTrack(type, props);
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Edge ingest (Cloudflare Worker)
|
||||
```typescript
|
||||
// worker.ts
|
||||
export default {
|
||||
async fetch(req: Request, env: Env): Promise<Response> {
|
||||
if (req.method !== 'POST') return new Response('', { status: 405 });
|
||||
const events = await req.json<Event[]>();
|
||||
const enriched = events.map((e) => ({
|
||||
...e,
|
||||
ip_country: req.cf?.country,
|
||||
ua: req.headers.get('user-agent'),
|
||||
ingest_ts: Date.now(),
|
||||
}));
|
||||
await env.QUEUE.send(enriched);
|
||||
return new Response('', { status: 204 });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 7. Server-side GTM forward
|
||||
```typescript
|
||||
// edge → GTM SS container
|
||||
await fetch('https://gtm.example.com/g/collect', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ events: enriched }),
|
||||
});
|
||||
```
|
||||
|
||||
### 8. PII scrub
|
||||
```typescript
|
||||
const EMAIL = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
|
||||
const PHONE = /\+?\d[\d \-]{8,}\d/g;
|
||||
function scrub<T extends Record<string, unknown>>(props: T): T {
|
||||
return JSON.parse(JSON.stringify(props).replace(EMAIL, '[email]').replace(PHONE, '[phone]'));
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Session identity
|
||||
```typescript
|
||||
const SESSION_KEY = 'sid';
|
||||
const TIMEOUT_MS = 30 * 60 * 1000;
|
||||
function getSession() {
|
||||
const raw = sessionStorage.getItem(SESSION_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : null;
|
||||
if (parsed && Date.now() - parsed.last < TIMEOUT_MS) {
|
||||
parsed.last = Date.now();
|
||||
} else {
|
||||
parsed?.id || (Object.assign({}, { id: crypto.randomUUID(), last: Date.now() }));
|
||||
}
|
||||
const out = parsed ?? { id: crypto.randomUUID(), last: Date.now() };
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(out));
|
||||
return out.id;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 product analytics | PostHog (OSS) or Amplitude. |
|
||||
| 매 RUM perf | Sentry / Datadog RUM. |
|
||||
| Privacy strict (EU) | 1st-party + consent + IP truncation. |
|
||||
| Adblock evasion | 매 1st-party domain reverse-proxy. |
|
||||
| 자체 build | sendBeacon + edge function + ClickHouse. |
|
||||
|
||||
**기본값**: `web-vitals` + 매 1st-party `/collect` + sendBeacon batch + edge ingest.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Observability]]
|
||||
- 응용: [[Sentry]]
|
||||
- Adjacent: [[Web Vitals]] · [[GDPR]] · [[ClickHouse]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 collector skeleton, consent gating, batch / flush logic.
|
||||
**언제 X**: 매 specific vendor SDK 의 detailed config — 매 vendor docs.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`fetch` without `keepalive`**: 매 unload 매 request drop.
|
||||
- **No batching**: 매 매 event = 매 request — flood.
|
||||
- **PII without consent**: 매 GDPR breach.
|
||||
- **3rd-party domain only**: 매 adblock 차단 — 매 1st-party proxy.
|
||||
- **Sync XHR on unload**: 매 deprecated — sendBeacon 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev web-vitals, IAB TCFv2, MDN sendBeacon).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — datacollector pipeline + Web Vitals + consent |
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
id: wiki-2026-0508-debugging-frontend-applications
|
||||
title: Debugging Frontend Applications
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Frontend Debugging, DevTools Workflow]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [debugging, devtools, performance, chrome]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/TypeScript
|
||||
framework: Browser DevTools
|
||||
---
|
||||
|
||||
# Debugging Frontend Applications
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 frontend 디버깅 = DevTools 의 mastery + reproducible state."**. Chrome DevTools (2024-2026)는 매 Performance Insights, AI assistance (Gemini Nano), Recorder, Memory profiler 의 통합. 매 핵심은 매 issue 의 reproduction → instrument → bisect → fix.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 DevTools panels
|
||||
- **Sources**: breakpoint, conditional bp, log point, blackbox.
|
||||
- **Performance**: flame chart, INP / LCP markers, Performance Insights.
|
||||
- **Network**: throttle, replay XHR, request blocking.
|
||||
- **Memory**: heap snapshot, allocation timeline.
|
||||
- **Application**: storage, service worker, cache.
|
||||
- **Lighthouse / Recorder**: automated audit + user flow capture.
|
||||
|
||||
### 매 reproduction
|
||||
- 매 hard bug = race / state / network / timing.
|
||||
- 매 deterministic repro = first 50% of debug.
|
||||
|
||||
### 매 응용
|
||||
1. Memory leak hunt — heap snapshot diff.
|
||||
2. Slow render trace — Performance flame.
|
||||
3. Network race — Replay & throttle.
|
||||
4. Production-only bug — source-mapped stack + Sentry replay.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Conditional breakpoint / log point
|
||||
```javascript
|
||||
// Sources panel: right-click line number
|
||||
// Conditional: user.id === 42
|
||||
// Log point: console.log('user', user) — 매 코드 수정 X
|
||||
```
|
||||
|
||||
### 2. debugger keyword + dynamic
|
||||
```javascript
|
||||
function process(items) {
|
||||
if (items.length > 1000) debugger; // pause when large
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. console.* advanced
|
||||
```javascript
|
||||
console.table(users);
|
||||
console.group('render');
|
||||
console.time('paint'); // ...
|
||||
console.timeEnd('paint');
|
||||
console.groupEnd();
|
||||
console.dir(node); // DOM as JS object
|
||||
console.trace();
|
||||
console.count('clicked');
|
||||
```
|
||||
|
||||
### 4. Performance.measure (User Timing)
|
||||
```javascript
|
||||
performance.mark('fetch-start');
|
||||
await fetch('/api/x');
|
||||
performance.mark('fetch-end');
|
||||
performance.measure('fetch', 'fetch-start', 'fetch-end');
|
||||
// shows in DevTools Performance + reportable to RUM
|
||||
```
|
||||
|
||||
### 5. Long Task observer
|
||||
```javascript
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > 50) console.warn('long task', entry);
|
||||
}
|
||||
}).observe({ type: 'longtask', buffered: true });
|
||||
```
|
||||
|
||||
### 6. Heap snapshot leak hunt
|
||||
```text
|
||||
1. DevTools → Memory → "Heap snapshot" (baseline).
|
||||
2. Trigger interaction (open/close modal 5x).
|
||||
3. Take 2nd snapshot.
|
||||
4. Compare: filter by "Objects allocated between snapshots".
|
||||
5. Look for retained DOM nodes / event listeners.
|
||||
```
|
||||
|
||||
### 7. Network throttle & replay
|
||||
```text
|
||||
- Network panel → "No throttling" → "Slow 4G" / Custom (300ms RTT).
|
||||
- Right-click request → "Replay XHR" / "Block request URL".
|
||||
- Override response: Sources → Overrides → Network → Save.
|
||||
```
|
||||
|
||||
### 8. Source map verification
|
||||
```bash
|
||||
# Verify upload
|
||||
curl -I https://cdn.example.com/main.js.map
|
||||
# In DevTools, check status: open Sources, file should show formatted code
|
||||
# If "missing source map", check //# sourceMappingURL comment + CORS
|
||||
```
|
||||
|
||||
### 9. React DevTools Profiler
|
||||
```text
|
||||
- Components tab — inspect props, hooks, set Suspense state.
|
||||
- Profiler tab — record interaction, view "Why did this render?" (commit reason).
|
||||
- Highlight updates when components render: settings cog → "Highlight updates".
|
||||
```
|
||||
|
||||
### 10. Chrome AI assistance (2024-)
|
||||
```text
|
||||
- DevTools Console → "Ask AI" (Gemini Nano integration).
|
||||
- "Why is this CSS not applying?" — pastes computed styles into prompt.
|
||||
- Performance Insights — auto-flagged INP / CLS culprits.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Slow render | Performance panel + React Profiler. |
|
||||
| Memory leak | Heap snapshot diff. |
|
||||
| Race condition | Network throttle + console.trace. |
|
||||
| Prod-only | Source map + Sentry Replay. |
|
||||
| CSS quirk | DevTools Computed + AI assistance. |
|
||||
| Long task | PerformanceObserver longtask. |
|
||||
|
||||
**기본값**: Repro locally → Performance flame → narrow → fix → regression test.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]]
|
||||
- Adjacent: [[Source Maps]] · [[Lighthouse]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 stack trace 의 explain, 매 console.error 의 plausible cause 의 list.
|
||||
**언제 X**: 매 timing-dependent / state-dependent bug — 매 actual repro 필수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`console.log` ship to prod**: 매 left-over noise.
|
||||
- **alert() debugging**: 매 modal blocks event loop.
|
||||
- **Disable source maps**: 매 prod debug 의 X.
|
||||
- **Profile in prod build only**: 매 dev mode warnings 의 miss.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Chrome DevTools docs 2024-2026, web.dev).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — DevTools 2026 + AI assistance + Profiler |
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
id: wiki-2026-0508-declaration-files
|
||||
title: Declaration Files
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [TypeScript .d.ts, Type Definitions, DefinitelyTyped]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, types, declaration, dts]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: TypeScript 5.x
|
||||
---
|
||||
|
||||
# Declaration Files
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 .d.ts = type-only public surface — runtime code 의 X."**. 매 TypeScript의 매 declaration 파일이 매 JS library의 매 type contract 의 제공 — `@types/node`, `@types/react` 등 매 DefinitelyTyped 가 매 ecosystem 의 backbone. 매 modern (2024-2026) 워크플로우는 `tsc --declaration` 자동 생성 + `tsup` / `rollup-plugin-dts` 의 bundling.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 file types
|
||||
- `lib.d.ts` — TS standard library (DOM, ES2024).
|
||||
- `package.d.ts` — package 자체 type.
|
||||
- `globals.d.ts` — ambient declaration (window 확장).
|
||||
- `module.d.ts` — `declare module 'foo'` shape.
|
||||
|
||||
### 매 publishing
|
||||
- `package.json` `"types"` (or `"typings"`) field → main d.ts entry.
|
||||
- `"exports"` map (Node 16+) — multiple entry points + types.
|
||||
- Dual ESM/CJS = 매 두 d.ts 필요 — `"types"` per condition.
|
||||
|
||||
### 매 응용
|
||||
1. JS library 의 type 추가 — `@types/foo`.
|
||||
2. Module augmentation — `declare module 'react' { ... }`.
|
||||
3. CSS modules / image import — `*.module.css` declaration.
|
||||
4. Env variable typing — `import.meta.env`.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Basic .d.ts
|
||||
```typescript
|
||||
// lib.d.ts
|
||||
export interface User { id: string; name: string }
|
||||
export declare function getUser(id: string): Promise<User>;
|
||||
export declare const VERSION: string;
|
||||
```
|
||||
|
||||
### 2. Module declaration (ambient)
|
||||
```typescript
|
||||
// types/legacy-lib.d.ts
|
||||
declare module 'legacy-lib' {
|
||||
export function init(opts: { key: string }): void;
|
||||
const version: string;
|
||||
export default version;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Module augmentation
|
||||
```typescript
|
||||
// types/react-augment.d.ts
|
||||
import 'react';
|
||||
declare module 'react' {
|
||||
interface CSSProperties {
|
||||
'--brand-hue'?: string;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Global augmentation
|
||||
```typescript
|
||||
// types/globals.d.ts
|
||||
declare global {
|
||||
interface Window {
|
||||
analytics?: { track(event: string, props?: Record<string, unknown>): void };
|
||||
}
|
||||
}
|
||||
export {}; // make this a module
|
||||
```
|
||||
|
||||
### 5. Asset module declarations
|
||||
```typescript
|
||||
// types/assets.d.ts
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
declare module '*.svg?react' {
|
||||
import { FC, SVGProps } from 'react';
|
||||
const Component: FC<SVGProps<SVGSVGElement>>;
|
||||
export default Component;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Vite env types
|
||||
```typescript
|
||||
// src/vite-env.d.ts
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_SENTRY_DSN: string;
|
||||
}
|
||||
interface ImportMeta { readonly env: ImportMetaEnv }
|
||||
```
|
||||
|
||||
### 7. package.json exports + types (dual ESM/CJS)
|
||||
```json
|
||||
{
|
||||
"name": "@acme/lib",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./feature": {
|
||||
"types": "./dist/feature.d.ts",
|
||||
"import": "./dist/feature.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. tsconfig for d.ts emit
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. tsup d.ts bundle
|
||||
```javascript
|
||||
// tsup.config.ts
|
||||
import { defineConfig } from 'tsup';
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true, // bundle d.ts
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
});
|
||||
```
|
||||
|
||||
### 10. Conditional type for string literal API
|
||||
```typescript
|
||||
// route.d.ts — type-safe routing
|
||||
type Route =
|
||||
| { name: 'home'; params?: undefined }
|
||||
| { name: 'user'; params: { id: string } };
|
||||
|
||||
declare function navigate<R extends Route>(...args: R['params'] extends undefined ? [name: R['name']] : [name: R['name'], params: R['params']]): void;
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 JS library 의 type | `@types/foo` (DefinitelyTyped). |
|
||||
| 매 own library | `tsc --declaration` or tsup `dts: true`. |
|
||||
| 매 monorepo internal | `composite: true` + project references. |
|
||||
| 매 CSS / asset import | `*.module.css` ambient declaration. |
|
||||
| Plugin-based extension | Module augmentation. |
|
||||
|
||||
**기본값**: tsup `dts: true` + `exports` map + declarationMap.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type Systems]]
|
||||
- 변형: [[Ambient Declarations]]
|
||||
- Adjacent: [[DefinitelyTyped]] · [[tsup]] · [[Vite]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 d.ts scaffolding, ambient module declaration, exports map.
|
||||
**언제 X**: 매 complex generic inference debug — 매 TS playground 사용.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No `export {}` in global aug**: 매 file 이 script 로 처리됨 → 매 declare global 무효.
|
||||
- **`any` everywhere**: 매 d.ts 의 가치 의 negate.
|
||||
- **`types` 미게시**: 매 user 의 `@types/foo` 의 별도 작성 필요.
|
||||
- **Conditional exports types last**: 매 `"types"` 가 매 conditions 의 first 위치 — 매 spec.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (typescriptlang.org/docs/handbook/declaration-files, TS 5.x release notes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — d.ts authoring + exports map 2026 |
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
category: Other
|
||||
tags: [auto-wikified, technical-documentation, other]
|
||||
title: Decorators
|
||||
description: "데코레이터(Decorators)는 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하여 동작을 정의하거나 의존성을 주입하는 설계 패턴 및 문법적 기능입니다 [1-3]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Decorators
|
||||
|
||||
## 📌 Brief Summary
|
||||
데코레이터(Decorators)는 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하여 동작을 정의하거나 의존성을 주입하는 설계 패턴 및 문법적 기능입니다 [1-3]. 특히 NestJS와 같은 프레임워크에서 모듈, 컨트롤러, 서비스를 구성하는 핵심 아키텍처 요소로 활용되며 [3], 애플리케이션의 횡단 관심사(Cross-Cutting Concerns)를 비즈니스 로직과 분리하여 제어하는 데 유용하게 쓰입니다 [4, 5].
|
||||
|
||||
## 📖 Core Content
|
||||
* **NestJS의 핵심 아키텍처 요소**: NestJS는 타입스크립트 데코레이터(`@Module()`, `@Controller`, `@Get`, `@Injectable` 등)를 적극적으로 사용하여 애플리케이션의 의존성 주입(DI) 컨테이너를 구성합니다 [2, 3, 6, 7]. 모듈은 `@Module()`로 주석이 달린 클래스이며, 프레임워크는 이를 통해 애플리케이션의 구조와 관계를 파악합니다 [2].
|
||||
* **데이터 모델링 및 문서 자동화**: DTO(Data Transfer Object)나 엔티티(Entity) 클래스에 데코레이터를 적용하여 TypeORM, Prisma, Drizzle 등의 ORM과 연동할 수 있습니다 [1]. 또한, 데코레이터와 DTO를 기반으로 Swagger/OpenAPI 문서를 실제 코드와 동기화하여 자동 생성하는 기능도 제공합니다 [8, 9].
|
||||
* **GraphQL 스키마 생성**: NestJS의 `@nestjs/graphql` 모듈에서는 데코레이터를 활용한 코드 우선(Code-first) 접근 방식을 지원합니다 [10]. 클래스에 데코레이터를 추가하는 것만으로 GraphQL 타입과 스키마를 자동으로 생성할 수 있습니다 [11].
|
||||
* **횡단 관심사(Cross-Cutting Concerns)의 분리**: 도메인 로직과 무관한 인프라 측면의 기능(로깅, 인증, 캐싱 등)을 분리할 때 데코레이터가 활용됩니다 [4]. 예를 들어, Django 프로젝트에서는 커스텀 권한(permissions)을 처리하거나 미들웨어와 함께 횡단 관심사의 흐름을 제어하기 위해 데코레이터를 설계 패턴으로 적용합니다 [5, 12].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **학습 곡선의 증가**: Express와 같이 제약이 적고 단순한 미니멀리즘 프레임워크에 익숙한 개발자에게는 데코레이터, 의존성 주입(DI), 모듈 시스템 등의 개념이 낯설 수 있으며 이를 익히기 위한 초기 학습 곡선이 가파릅니다 [6, 9].
|
||||
* **프레임워크 오버헤드**: NestJS의 경우 데코레이터와 의존성 주입(DI) 계층으로 인해 순수한 Express에 비해 약 10~15% 정도의 처리량(Throughput) 감소라는 성능 오버헤드가 발생합니다 [13]. 다만, 프로덕션 환경의 데이터베이스 쿼리나 네트워크 지연 시간에 비하면 이는 대부분 무시할 수 있는 수준입니다 [13].
|
||||
* **과도한 보일러플레이트 작성**: 소규모 팀이나 간단한 프로젝트에서는 모듈을 선언하고, DTO를 만들고, 엔티티에 데코레이터를 부착하는 구조화 작업 자체가 실제 비즈니스 로직 작성보다 더 많은 시간과 비용을 요구하는 기술 부채(Technical Debt)나 과잉 엔지니어링이 될 수 있습니다 [14, 15].
|
||||
* **전역 남용의 위험**: NestJS에서 교차 절단 관심사에 `@Global()` 데코레이터를 사용할 수 있지만, 이를 과도하게 남용할 경우 모듈 시스템이 해결하고자 했던 "모든 것이 어디서나 접근 가능해지는 문제"를 다시 발생시킬 수 있으므로 주의해서 사용해야 합니다 [16].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
id: wiki-2026-0508-definitelytyped
|
||||
title: DefinitelyTyped
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: ["@types", DT, DefinitelyTyped repo]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, types, ecosystem, npm]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: none
|
||||
---
|
||||
|
||||
# DefinitelyTyped
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 untyped JavaScript 의 community-maintained TypeScript declaration 의 monorepo"**. `@types/*` npm scope 의 publish 되며, 8000+ package 의 cover. 2026 의 매 native-TS shift (`exports`/`types` field) 의 점진적 의 X — 매 still backbone 의 legacy ecosystem.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 작동
|
||||
- GitHub `DefinitelyTyped/DefinitelyTyped` repo 의 매 `types/<package>/index.d.ts` 의 hosting.
|
||||
- types-publisher bot 의 매 commit 의 `@types/<package>` 의 npm publish.
|
||||
- TypeScript 의 매 `node_modules/@types/*` 의 auto-include (tsconfig `types` field 의 absence).
|
||||
|
||||
### 매 declaration 의 구조
|
||||
- `index.d.ts` — main declaration.
|
||||
- `package.json` — version, dependencies (other `@types`).
|
||||
- `tests/<pkg>-tests.ts` — type 의 verify 의 sample 의 compile.
|
||||
- `tsconfig.json` — strict mode + lib version.
|
||||
|
||||
### 매 modern shift (2026)
|
||||
- 매 popular library 의 매 inline TS (`exports.types`) 의 ship — `@types/*` 의 unnecessary.
|
||||
- DT 의 매 long-tail / abandoned package 의 still 의 critical.
|
||||
- TypeScript Reference Types 의 매 alternative ecosystem 의 emerging.
|
||||
|
||||
### 매 응용
|
||||
1. Untyped npm package 의 type 의 add.
|
||||
2. Global type (jQuery 의 `$`, Node `process`) 의 declare.
|
||||
3. Module augmentation (express `Request` 의 add field).
|
||||
4. Ambient module (`declare module "*.svg"`).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Install + 사용
|
||||
```bash
|
||||
npm install lodash
|
||||
npm install --save-dev @types/lodash
|
||||
```
|
||||
|
||||
```ts
|
||||
import _ from "lodash";
|
||||
const grouped = _.groupBy([1, 2, 3, 4], (n) => n % 2);
|
||||
// ^? Dictionary<number[]>
|
||||
```
|
||||
|
||||
### Version 의 align
|
||||
```json
|
||||
{
|
||||
"dependencies": { "lodash": "^4.17.21" },
|
||||
"devDependencies": { "@types/lodash": "^4.17.21" }
|
||||
}
|
||||
```
|
||||
매 major.minor 의 같은 의 keep — 매 type drift 의 risk.
|
||||
|
||||
### Type 의 X 의 fallback
|
||||
```ts
|
||||
// types/legacy-lib.d.ts
|
||||
declare module "legacy-lib" {
|
||||
export function doStuff(x: string): number;
|
||||
export const VERSION: string;
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["types/*"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Quick stub (any)
|
||||
```ts
|
||||
declare module "untyped-lib"; // 매 entire module 의 any
|
||||
```
|
||||
매 explicit narrow 보다 의 worst — but 의 match-and-go 의 valid.
|
||||
|
||||
### Module augmentation
|
||||
```ts
|
||||
// types/express-augment.d.ts
|
||||
import "express";
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
user?: { id: string; email: string };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global declaration
|
||||
```ts
|
||||
// types/globals.d.ts
|
||||
declare global {
|
||||
interface Window {
|
||||
__APP_VERSION__: string;
|
||||
}
|
||||
|
||||
var __DEV__: boolean;
|
||||
}
|
||||
|
||||
export {}; // 매 file 의 module 의 의 essential
|
||||
```
|
||||
|
||||
### Asset module declaration
|
||||
```ts
|
||||
// types/assets.d.ts
|
||||
declare module "*.svg" {
|
||||
const content: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
```
|
||||
|
||||
### DT 의 contribute
|
||||
```bash
|
||||
git clone https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
cd DefinitelyTyped/types
|
||||
mkdir my-lib
|
||||
# index.d.ts, tsconfig.json, package.json, my-lib-tests.ts 의 작성
|
||||
npm test
|
||||
# PR 의 submit
|
||||
```
|
||||
|
||||
### 매 inline types (modern alternative)
|
||||
```jsonc
|
||||
// my-lib package.json
|
||||
{
|
||||
"name": "my-lib",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
매 native ship 의 `@types/*` 의 X 의 필요.
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Library 의 native types 의 ship | nothing 의 install — 의 just import |
|
||||
| Library 의 untyped, popular | `npm i -D @types/<pkg>` |
|
||||
| `@types/*` 의 X 의 (long-tail) | 매 local `.d.ts` 의 작성 + `paths` |
|
||||
| Quick prototype | `declare module "x";` (any) |
|
||||
| Library 의 publish | 매 native `types` field 의 ship — DT 의 contribute X |
|
||||
|
||||
**기본값**: 매 `@types/*` 의 first 의 try, fallback 의 local `.d.ts`.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]]
|
||||
- Adjacent: [[tsconfig.json]] · [[Declaration Files]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: untyped library 의 type 의 add, module augmentation, asset module declaration.
|
||||
**언제 X**: library 의 매 already 의 inline types 의 ship — `@types/*` 의 install 의 conflict 의 cause.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Version mismatch**: `@types/lodash@3` + `lodash@4` — 매 wrong type.
|
||||
- **Both inline + DT install**: 매 conflict — 매 module resolution 의 confused.
|
||||
- **`declare module "x";` 의 production 의 ship**: 매 effectively `any` 의 entire library.
|
||||
- **DT PR 의 의 X 의 — local 의만 keep**: 매 update 의 manual.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (DefinitelyTyped repo, TypeScript handbook, npm `@types` scope, types-publisher).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — augmentation + asset module + 2026 inline-types shift 추가 |
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
id: wiki-2026-0508-diffing-algorithm
|
||||
title: Diffing Algorithm
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [VDOM Diff, Reconciliation]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, vue, vdom, reconciliation]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: React/Vue
|
||||
---
|
||||
|
||||
# Diffing Algorithm
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 VDOM diff = O(n) heuristic — 매 same-level node 의 비교 + key-based identity."**. 매 React/Vue/Preact 모두 매 Levenshtein-style O(n³) 의 회피하기 위해 매 두 가정 (1) 다른 type = subtree 교체, (2) key가 child identity 의 hint. 매 React 19 (2024) Fiber + concurrent rendering, Vue 3.4 patchFlag-driven static hoist 의 진화.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 두 가정 (Heuristics)
|
||||
1. 매 different element type ⇒ subtree 의 unmount + 재생성.
|
||||
2. 매 stable `key` ⇒ list reconciliation 의 identity hint.
|
||||
|
||||
### 매 알고리즘
|
||||
- React = Fiber tree, work loop가 매 cooperative scheduling 가능 (concurrent mode).
|
||||
- Vue 3 = compiled patchFlag (static / dynamic 분류) + LIS (Longest Increasing Subsequence) for keyed list.
|
||||
- Svelte/Solid = 매 VDOM 없음 — 매 fine-grained reactivity 의 directly DOM 의 patch.
|
||||
|
||||
### 매 응용
|
||||
1. Keyed list — 매 stable key가 매 reorder cost 의 minimize.
|
||||
2. Conditional render — 매 ternary가 매 type swap 의 발생.
|
||||
3. Concurrent rendering — 매 high-priority update가 매 low-priority interrupt.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Stable key (correct)
|
||||
```jsx
|
||||
// CORRECT — id is stable identity
|
||||
items.map((item) => <Row key={item.id} item={item} />)
|
||||
|
||||
// WRONG — index changes when list reorders
|
||||
items.map((item, i) => <Row key={i} item={item} />)
|
||||
```
|
||||
|
||||
### 2. Type swap forces unmount
|
||||
```jsx
|
||||
{condition ? <div>A</div> : <span>A</span>}
|
||||
// span/div type 다름 → subtree unmount + remount
|
||||
// 같은 component reuse 원하면 같은 type 유지:
|
||||
<div>{condition ? 'A' : 'B'}</div>
|
||||
```
|
||||
|
||||
### 3. React Fiber priority lanes (19)
|
||||
```jsx
|
||||
import { useTransition } from 'react';
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const onChange = (e) => {
|
||||
setInput(e.target.value); // urgent
|
||||
startTransition(() => {
|
||||
setFilter(e.target.value); // can be interrupted
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Vue 3 patchFlag (compiled)
|
||||
```html
|
||||
<!-- Source -->
|
||||
<div>
|
||||
<span>{{ msg }}</span>
|
||||
<span class="static">hi</span>
|
||||
</div>
|
||||
|
||||
<!-- Compiled (simplified) -->
|
||||
<!-- 첫 span has patchFlag = 1 (TEXT) → diff only text -->
|
||||
<!-- 두번째 span hoisted as static — 매 diff 건너뛰기 -->
|
||||
```
|
||||
|
||||
### 5. Vue keyed list w/ LIS
|
||||
```html
|
||||
<TransitionGroup tag="ul">
|
||||
<li v-for="item in items" :key="item.id">{{ item.label }}</li>
|
||||
</TransitionGroup>
|
||||
<!-- Vue calculates LIS to minimize DOM moves -->
|
||||
```
|
||||
|
||||
### 6. React.memo + structural equality
|
||||
```typescript
|
||||
const Row = React.memo(({ item }: { item: Item }) =>
|
||||
<div>{item.label}</div>,
|
||||
(prev, next) => prev.item.id === next.item.id && prev.item.label === next.item.label,
|
||||
);
|
||||
```
|
||||
|
||||
### 7. Solid/Svelte alternative (no diff)
|
||||
```typescript
|
||||
// Solid — fine-grained reactivity, no VDOM
|
||||
import { createSignal } from 'solid-js';
|
||||
const [count, setCount] = createSignal(0);
|
||||
// JSX compiles to direct DOM ops; only `count()` text node updates
|
||||
return <div>count: {count()}</div>;
|
||||
```
|
||||
|
||||
### 8. Avoid layout-impacting reorder
|
||||
```jsx
|
||||
// Stable order with key — only reorder DOM, no remount
|
||||
<>{sorted.map((u) => <UserCard key={u.id} user={u} />)}</>
|
||||
// Each UserCard preserves its instance (state, refs) on reorder.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| List render | 매 stable id key — index X. |
|
||||
| Heavy filter + input | useTransition + deferredValue. |
|
||||
| Static UI | Vue patchFlag / React.memo. |
|
||||
| Maximum perf | Solid / Svelte (skip VDOM). |
|
||||
| Type-switch UI | wrap in same outer type to preserve children. |
|
||||
|
||||
**기본값**: 매 React 19 + Suspense + Transition + 매 stable key.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Virtual DOM과 Reconciliation|Virtual DOM]] · [[Reconciliation]]
|
||||
- 변형: [[React Fiber]] · [[Vue Reactivity]]
|
||||
- 응용: [[Animation]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 keyed-list bug 의 explain, 매 type-swap unmount 의 진단.
|
||||
**언제 X**: 매 specific framework internal — 매 source code 의 read.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Index as key with reorder**: 매 wrong identity → state mix.
|
||||
- **Random key per render**: 매 매 unmount + remount.
|
||||
- **Inline objects/arrays as deps**: 매 referential change → memo bypass.
|
||||
- **Massive un-keyed list**: 매 O(n) DOM ops 매 frame.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (react.dev reconciliation, vuejs.org renderer, Solid docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — VDOM diff + Fiber + Vue patchFlag |
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
---
|
||||
id: wiki-2026-0508-discriminated-unions-for-error-h
|
||||
title: Discriminated Unions for Error Handling
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Tagged Unions, Result Type, Sum Types]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, error-handling, type-system, functional]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: none
|
||||
---
|
||||
|
||||
# Discriminated Unions for Error Handling
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 throw 의 의 X — return value 의 success/failure 의 model"**. ML/Haskell `Either`, Rust `Result<T, E>` 의 TypeScript port. 2026 의 매 typed exception 의 alternative — exhaustive checking + traceable failure path.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 왜 throw 의 X 의
|
||||
- `throw` 의 type-erased — caller 의 매 catch 의 type 의 unknown.
|
||||
- Control flow 의 invisible — 매 implicit goto.
|
||||
- async 의 매 unhandled rejection 의 silent.
|
||||
|
||||
### 매 discriminator tag
|
||||
- 매 literal field (`type`, `tag`, `_tag`, `kind`) 의 매 union 의 narrow.
|
||||
- TypeScript 의 control-flow analysis 의 `if (r.type === "ok")` 의 narrow.
|
||||
- exhaustive check 의 `switch` + `never` 의 가능.
|
||||
|
||||
### 매 응용
|
||||
1. API client (network/parse/validation 의 error 의 분리).
|
||||
2. Form validation (field-level error).
|
||||
3. Parser combinator (success/fail with location).
|
||||
4. State machine (state 의 each 의 tag).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 기본 Result type
|
||||
```ts
|
||||
type Result<T, E = Error> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E };
|
||||
|
||||
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
|
||||
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|
||||
```
|
||||
|
||||
### Domain error 의 tagged
|
||||
```ts
|
||||
type FetchUserError =
|
||||
| { type: "network"; cause: unknown }
|
||||
| { type: "not_found"; userId: string }
|
||||
| { type: "unauthorized" }
|
||||
| { type: "parse"; raw: unknown; issue: string };
|
||||
|
||||
async function fetchUser(id: string): Promise<Result<User, FetchUserError>> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/users/${id}`);
|
||||
} catch (cause) {
|
||||
return err({ type: "network", cause });
|
||||
}
|
||||
if (res.status === 404) return err({ type: "not_found", userId: id });
|
||||
if (res.status === 401) return err({ type: "unauthorized" });
|
||||
|
||||
const raw = await res.json();
|
||||
const parsed = UserSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return err({ type: "parse", raw, issue: parsed.error.message });
|
||||
}
|
||||
return ok(parsed.data);
|
||||
}
|
||||
```
|
||||
|
||||
### Exhaustive handling
|
||||
```ts
|
||||
function renderError(e: FetchUserError): string {
|
||||
switch (e.type) {
|
||||
case "network": return "Network failure. Retry?";
|
||||
case "not_found": return `User ${e.userId} 의 X.`;
|
||||
case "unauthorized": return "Login 의 필요.";
|
||||
case "parse": return `Bad response: ${e.issue}`;
|
||||
default: {
|
||||
const _exhaustive: never = e; // 매 compile error 의 새 case 의 추가 시
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caller 의 narrow 의 사용
|
||||
```ts
|
||||
const r = await fetchUser("abc");
|
||||
if (!r.ok) {
|
||||
// r.error: FetchUserError — 매 fully typed
|
||||
return renderError(r.error);
|
||||
}
|
||||
// r.value: User — 매 narrowed
|
||||
console.log(r.value.email);
|
||||
```
|
||||
|
||||
### Combinator (map / flatMap)
|
||||
```ts
|
||||
function map<T, U, E>(r: Result<T, E>, f: (t: T) => U): Result<U, E> {
|
||||
return r.ok ? ok(f(r.value)) : r;
|
||||
}
|
||||
|
||||
function flatMap<T, U, E>(r: Result<T, E>, f: (t: T) => Result<U, E>): Result<U, E> {
|
||||
return r.ok ? f(r.value) : r;
|
||||
}
|
||||
|
||||
// 매 chained pipeline
|
||||
const result = flatMap(
|
||||
await fetchUser(id),
|
||||
(u) => u.verified ? ok(u) : err({ type: "unauthorized" } as FetchUserError),
|
||||
);
|
||||
```
|
||||
|
||||
### Form validation
|
||||
```ts
|
||||
type FieldResult<T> =
|
||||
| { state: "valid"; value: T }
|
||||
| { state: "invalid"; reasons: string[] }
|
||||
| { state: "pending" };
|
||||
|
||||
function validateEmail(s: string): FieldResult<string> {
|
||||
if (s.length === 0) return { state: "pending" };
|
||||
const reasons: string[] = [];
|
||||
if (!s.includes("@")) reasons.push("@ missing");
|
||||
if (s.length > 254) reasons.push("Too long");
|
||||
return reasons.length ? { state: "invalid", reasons } : { state: "valid", value: s };
|
||||
}
|
||||
```
|
||||
|
||||
### Zod 의 native return
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
|
||||
const UserSchema = z.object({ id: z.string(), email: z.email() });
|
||||
const parsed = UserSchema.safeParse(input);
|
||||
// parsed: { success: true; data: User } | { success: false; error: ZodError }
|
||||
```
|
||||
|
||||
### neverthrow 라이브러리 (2026 standard)
|
||||
```ts
|
||||
import { ok, err, Result, ResultAsync } from "neverthrow";
|
||||
|
||||
const fetchUserSafe = (id: string): ResultAsync<User, FetchUserError> =>
|
||||
ResultAsync.fromPromise(fetch(`/api/users/${id}`), (c) => ({ type: "network", cause: c } as const))
|
||||
.andThen((res) =>
|
||||
res.ok ? ResultAsync.fromSafePromise(res.json()) : err({ type: "not_found", userId: id } as const)
|
||||
);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Domain logic 의 expected failure | Discriminated union Result |
|
||||
| Truly exceptional (OOM, bug) | Throw |
|
||||
| Async I/O | `Promise<Result<T, E>>` 의 neverthrow `ResultAsync` |
|
||||
| Schema parsing | Zod `safeParse` |
|
||||
| Multi-step pipeline | `flatMap` chain 의 Effect TS |
|
||||
|
||||
**기본값**: domain error 의 tagged union, infrastructure error 의 throw.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type System]]
|
||||
- 변형: [[Effect TS 및 ts-brand 라이브러리 활용]]
|
||||
- 응용: [[Error Boundaries]]
|
||||
- Adjacent: [[Zod]] · [[Pattern Matching]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: domain error 의 modeling, exhaustive switch 의 enforce, async error path 의 explicit.
|
||||
**언제 X**: panic-level error (assertion fail) — throw 의 더 적합.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`{ error: string | null; data: T | null }`**: 매 implicit invariant — 둘 모두 null/non-null 의 type 의 allow.
|
||||
- **String-only error**: 매 i18n / programmatic handling 의 X.
|
||||
- **Discriminator 의 `boolean`**: `ok: true/false` 의 OK, but multi-variant 의 X — string literal 의 사용.
|
||||
- **Throw inside Result-returning fn**: 매 hybrid 의 worst.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TypeScript handbook, Effect-TS docs, neverthrow, Rust `Result`, fp-ts `Either`).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — exhaustive check + neverthrow + combinators 추가 |
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
---
|
||||
id: wiki-2026-0508-discriminated-unions-for-state-m
|
||||
title: Discriminated Unions for State Modeling
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Tagged Unions, Sum Types, ADT State]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, state-modeling, types, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React / any
|
||||
---
|
||||
|
||||
# Discriminated Unions for State Modeling
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 impossible state 의 unrepresentable"**. Discriminated union (tagged union, sum type) 으로 매 async/UI state 를 modeling 하면 매 `loading + error + data` simultaneous 같은 illegal combination 매 type system 매 차단.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 motivation
|
||||
- Boolean flag combinatorics: `isLoading`, `isError`, `data` → 8 states, 매 5+ illegal.
|
||||
- DU: 매 mutually-exclusive variants 만 표현.
|
||||
|
||||
### 매 anatomy
|
||||
- Common discriminator field (`type`, `status`, `kind`).
|
||||
- Per-variant payload.
|
||||
- TS narrows by discriminator in `switch` / `if`.
|
||||
|
||||
### 매 응용
|
||||
1. Async data (idle / loading / success / error).
|
||||
2. Form (initial / submitting / submitted / failed).
|
||||
3. Wizard (step1 / step2 / done).
|
||||
4. Auth (anonymous / loading / authenticated / expired).
|
||||
5. Reducer/state machine actions.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Async state DU
|
||||
```ts
|
||||
type AsyncState<T, E = Error> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: E };
|
||||
|
||||
function render<T>(s: AsyncState<T>) {
|
||||
switch (s.status) {
|
||||
case 'idle': return <Idle />;
|
||||
case 'loading': return <Spinner />;
|
||||
case 'success': return <Data data={s.data} />; // 매 data 매 type-safe
|
||||
case 'error': return <Err msg={s.error.message} />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exhaustive check helper
|
||||
```ts
|
||||
function assertNever(x: never): never {
|
||||
throw new Error(`Unhandled variant: ${JSON.stringify(x)}`);
|
||||
}
|
||||
|
||||
function step(s: WizardState) {
|
||||
switch (s.kind) {
|
||||
case 'address': return <AddressForm />;
|
||||
case 'payment': return <PaymentForm />;
|
||||
case 'review': return <Review />;
|
||||
default: return assertNever(s); // 매 compile error 매 새 variant 추가 시
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Form state DU
|
||||
```ts
|
||||
type FormState<V, E = Record<string, string>> =
|
||||
| { phase: 'editing'; values: V }
|
||||
| { phase: 'submitting'; values: V }
|
||||
| { phase: 'failed'; values: V; errors: E }
|
||||
| { phase: 'submitted'; result: { id: string } };
|
||||
```
|
||||
|
||||
### Auth state DU
|
||||
```ts
|
||||
type Auth =
|
||||
| { state: 'anonymous' }
|
||||
| { state: 'authenticating' }
|
||||
| { state: 'authenticated'; user: User; token: string }
|
||||
| { state: 'expired'; lastUser: User };
|
||||
|
||||
function canAccessAdmin(a: Auth): boolean {
|
||||
return a.state === 'authenticated' && a.user.role === 'admin';
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer with action DU
|
||||
```ts
|
||||
type Action =
|
||||
| { type: 'fetch/start' }
|
||||
| { type: 'fetch/success'; payload: User[] }
|
||||
| { type: 'fetch/error'; error: string };
|
||||
|
||||
function reducer(s: AsyncState<User[]>, a: Action): AsyncState<User[]> {
|
||||
switch (a.type) {
|
||||
case 'fetch/start': return { status: 'loading' };
|
||||
case 'fetch/success': return { status: 'success', data: a.payload };
|
||||
case 'fetch/error': return { status: 'error', error: new Error(a.error) };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern matching with `ts-pattern`
|
||||
```ts
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
const view = match(state)
|
||||
.with({ status: 'idle' }, () => <Idle />)
|
||||
.with({ status: 'loading' }, () => <Spinner />)
|
||||
.with({ status: 'success' }, ({ data }) => <Data data={data} />)
|
||||
.with({ status: 'error' }, ({ error }) => <Err msg={error.message} />)
|
||||
.exhaustive();
|
||||
```
|
||||
|
||||
### Nested DU (loading sub-status)
|
||||
```ts
|
||||
type Resource<T> =
|
||||
| { state: 'idle' }
|
||||
| { state: 'loading'; progress?: number }
|
||||
| { state: 'streaming'; partial: T[]; progress: number }
|
||||
| { state: 'success'; data: T }
|
||||
| { state: 'error'; error: Error; retryCount: number };
|
||||
```
|
||||
|
||||
### Builder helpers
|
||||
```ts
|
||||
const idle = <T,>(): AsyncState<T> => ({ status: 'idle' });
|
||||
const loading = <T,>(): AsyncState<T> => ({ status: 'loading' });
|
||||
const success = <T,>(data: T): AsyncState<T> => ({ status: 'success', data });
|
||||
const failure = <T,>(error: Error): AsyncState<T> => ({ status: 'error', error });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 2+ boolean flags 매 mutually exclusive | DU 강제 |
|
||||
| Single boolean | 매 plain bool OK |
|
||||
| Complex transitions | DU + state machine (XState) |
|
||||
| Library API surface | DU 매 caller-friendly |
|
||||
|
||||
**기본값**: async/form/wizard/auth state 매 즉시 DU 모델링.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[Type-Driven-Design]]
|
||||
- 변형: [[Algebraic-Data-Types]] · [[Sum-Types]]
|
||||
- 응용: [[XState]] · [[React-Query]]
|
||||
- Adjacent: [[Exhaustiveness-Checking]] · [[Pattern-Matching]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: async data, multi-step UI, auth flow, anywhere 매 boolean explosion 발생.
|
||||
**언제 X**: 매 single-flag 매 trivial — over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Boolean explosion**: `isLoading && !isError && data` chains.
|
||||
- **Optional fields encoding state**: `{ data?, loading?, error? }` — 매 invalid combos 가능.
|
||||
- **Missing exhaustive check**: 매 new variant 추가 시 매 silently broken.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TS handbook / Richard Feldman "Making Impossible States Impossible").
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — DU patterns for async/form/auth state |
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
id: wiki-2026-0508-draw-call-optimization
|
||||
title: Draw Call Optimization
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Batching, Instancing, GPU Draw Reduction]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [graphics, gpu, performance, webgl, webgpu]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: WebGPU
|
||||
---
|
||||
|
||||
# Draw Call Optimization
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CPU→GPU command submission 의 의 minimize 의 — 의 frame budget 의 dominant cost"**. 의 each draw call 의 의 driver overhead (state validation, command translation) 의 incur. 2026 의 WebGPU 의 매 explicit 의 design 의 의 batching 의 essential 의.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 cost 의 source
|
||||
- 의 driver state validation (shader, buffer, texture binding).
|
||||
- 의 command buffer 의 translation 의 GPU-specific ISA.
|
||||
- 의 GPU 의 의 pipeline switch (cache miss, warp reorganize).
|
||||
|
||||
### 매 reduction 의 strategy
|
||||
- **Batching**: 의 same-state object 의 single draw 의 의 merge.
|
||||
- **Instancing**: 의 same mesh 의 N copy 의 single 의 draw call 의 issue.
|
||||
- **Texture atlas**: 의 multiple texture 의 의 single 의 의 — 의 binding 의 reduce.
|
||||
- **Indirect draw**: 의 GPU 의 의 self-issue 의 의 — CPU 의 의 idle.
|
||||
- **Bindless / large bind group**: 의 binding 의 의 amortize.
|
||||
|
||||
### 매 응용
|
||||
1. UI rendering (의 button 의 thousand 의 single draw).
|
||||
2. Particle system (의 instancing 의 의 millions).
|
||||
3. Tilemap (atlas + instancing).
|
||||
4. Foliage / crowd (의 GPU instancing).
|
||||
5. Game world chunk (의 batching 의 의 static mesh).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Three.js BatchedMesh
|
||||
```ts
|
||||
import * as THREE from "three";
|
||||
|
||||
const batched = new THREE.BatchedMesh(1024, 60_000, 90_000, material);
|
||||
const cubeGeom = new THREE.BoxGeometry();
|
||||
const id = batched.addGeometry(cubeGeom);
|
||||
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
const inst = batched.addInstance(id);
|
||||
const m = new THREE.Matrix4().setPosition(Math.random() * 100, 0, Math.random() * 100);
|
||||
batched.setMatrixAt(inst, m);
|
||||
}
|
||||
scene.add(batched);
|
||||
// 매 single draw call 의 1024 cube
|
||||
```
|
||||
|
||||
### InstancedMesh
|
||||
```ts
|
||||
const geom = new THREE.SphereGeometry(0.5);
|
||||
const mesh = new THREE.InstancedMesh(geom, material, 10_000);
|
||||
const m = new THREE.Matrix4();
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
m.setPosition(Math.random() * 200 - 100, 0, Math.random() * 200 - 100);
|
||||
mesh.setMatrixAt(i, m);
|
||||
}
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
```
|
||||
|
||||
### WebGPU instanced draw
|
||||
```ts
|
||||
const pass = encoder.beginRenderPass(passDesc);
|
||||
pass.setPipeline(pipeline);
|
||||
pass.setBindGroup(0, sceneBindGroup);
|
||||
pass.setVertexBuffer(0, vertexBuffer);
|
||||
pass.setVertexBuffer(1, instanceBuffer); // per-instance data
|
||||
pass.setIndexBuffer(indexBuffer, "uint32");
|
||||
pass.drawIndexed(indexCount, instanceCount);
|
||||
pass.end();
|
||||
```
|
||||
|
||||
### WebGPU indirect draw (GPU 의 self-issue)
|
||||
```ts
|
||||
const indirectBuffer = device.createBuffer({
|
||||
size: 16, // [vertexCount, instanceCount, firstVertex, firstInstance]
|
||||
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE,
|
||||
});
|
||||
|
||||
// 의 compute shader 의 의 indirectBuffer 의 의 fill (e.g. frustum cull)
|
||||
pass.drawIndirect(indirectBuffer, 0);
|
||||
```
|
||||
|
||||
### Texture atlas (UV 의 sub-region)
|
||||
```glsl
|
||||
// fragment shader
|
||||
uniform sampler2D atlas;
|
||||
uniform vec4 uvRect; // x, y, w, h
|
||||
|
||||
void main() {
|
||||
vec2 uv = uvRect.xy + v_uv * uvRect.zw;
|
||||
outColor = texture(atlas, uv);
|
||||
}
|
||||
```
|
||||
|
||||
### Sort 의 의 state-change minimize
|
||||
```ts
|
||||
// 의 draw 의 의 material 의 의 sort
|
||||
drawables.sort((a, b) => {
|
||||
if (a.materialId !== b.materialId) return a.materialId - b.materialId;
|
||||
if (a.meshId !== b.meshId) return a.meshId - b.meshId;
|
||||
return a.depth - b.depth;
|
||||
});
|
||||
```
|
||||
|
||||
### UI batching (single quad mesh)
|
||||
```ts
|
||||
// 의 each UI element 의 의 quad 의 의 single VBO 의 의 append
|
||||
class UIBatcher {
|
||||
vertices = new Float32Array(4096 * 4 * 5); // x, y, u, v, color
|
||||
count = 0;
|
||||
|
||||
pushQuad(x: number, y: number, w: number, h: number, uv: UVRect, color: number) {
|
||||
const v = this.vertices;
|
||||
const o = this.count * 20;
|
||||
v[o+0]=x; v[o+1]=y; v[o+2]=uv.x; v[o+3]=uv.y; v[o+4]=color;
|
||||
v[o+5]=x+w; v[o+6]=y; v[o+7]=uv.x+uv.w; v[o+8]=uv.y; v[o+9]=color;
|
||||
v[o+10]=x+w; v[o+11]=y+h; v[o+12]=uv.x+uv.w; v[o+13]=uv.y+uv.h; v[o+14]=color;
|
||||
v[o+15]=x; v[o+16]=y+h; v[o+17]=uv.x; v[o+18]=uv.y+uv.h; v[o+19]=color;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
flush(pass: GPURenderPassEncoder) {
|
||||
device.queue.writeBuffer(this.vbo, 0, this.vertices, 0, this.count * 20);
|
||||
pass.setVertexBuffer(0, this.vbo);
|
||||
pass.draw(6 * this.count); // 매 single call
|
||||
this.count = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frustum cull (CPU)
|
||||
```ts
|
||||
function cull(objects: Drawable[], camera: Camera): Drawable[] {
|
||||
const frustum = camera.frustum;
|
||||
return objects.filter((o) => frustum.intersects(o.worldBounds));
|
||||
}
|
||||
```
|
||||
|
||||
### GPU-driven cull (compute)
|
||||
```wgsl
|
||||
@compute @workgroup_size(64)
|
||||
fn cullCS(@builtin(global_invocation_id) gid: vec3u) {
|
||||
let i = gid.x;
|
||||
if (i >= arrayLength(&instances)) { return; }
|
||||
let inst = instances[i];
|
||||
if (frustumIntersects(inst.bounds, frustum)) {
|
||||
let slot = atomicAdd(&drawCount, 1u);
|
||||
visibleInstances[slot] = inst;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Same mesh, many copy | InstancedMesh (instancing) |
|
||||
| Different mesh, same material | BatchedMesh (geometry merge) |
|
||||
| UI / 2D | Sprite batcher + atlas |
|
||||
| Static scene | Pre-merge geometry at build time |
|
||||
| Dynamic LOD / cull | GPU indirect draw + compute cull |
|
||||
| Mobile / tile | Reduce binding, atlas, instancing |
|
||||
|
||||
**기본값**: instancing 의 first, batching 의 second, indirect/compute 의 last.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[GPU Pipeline]] · [[Real-Time Rendering]]
|
||||
- 변형: [[Instancing]] · [[Batching]] · [[Indirect Draw]]
|
||||
- Adjacent: [[Texture Atlas]] · [[GPU-driven Rendering]] · [[Frustum Culling]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 의 frame time 의 의 CPU-bound 의 (draw call > 1000), GPU-driven culling, atlas 설계.
|
||||
**언제 X**: 의 매 GPU-bound 의 (fragment-heavy) — 의 다른 의 axis 의 (overdraw, shader complexity) 의 attack.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **One mesh per object**: 의 10,000 entity 의 = 의 10,000 draw — 매 disaster.
|
||||
- **Per-frame buffer recreate**: 의 GC pressure + 의 driver overhead.
|
||||
- **Random material switch**: state thrash — 매 sort 의 의 by material first.
|
||||
- **Premature GPU-driven**: 의 CPU 의 매 not bottleneck 의 시 의 — 매 added complexity.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (WebGPU spec, Three.js BatchedMesh r167+, Unreal/Unity rendering docs, GPU Gems, RenderDoc analysis).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — instancing + indirect + UI batcher 추가 |
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
id: wiki-2026-0508-dynamic-theming
|
||||
title: Dynamic Theming
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Theming, Dark Mode, CSS Variables Theming]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, css, theming, design-system]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React
|
||||
---
|
||||
|
||||
# Dynamic Theming
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 design token 을 runtime swap 할 수 있는 architecture"**. CSS custom properties (variables) 가 매 modern theming 의 backbone 이며, JS bundle 의 무관 하게 instant theme switching 의 가능. 2026 의 light/dark/high-contrast/brand-variant 의 매 standard.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 3-layer token 구조
|
||||
- **Primitive tokens**: raw values (`--blue-500: #3B82F6`).
|
||||
- **Semantic tokens**: intent-based (`--color-primary: var(--blue-500)`).
|
||||
- **Component tokens**: scope-specific (`--button-bg: var(--color-primary)`).
|
||||
|
||||
### 매 swap 메커니즘
|
||||
- `data-theme="dark"` 속성 의 `<html>` element 의 set.
|
||||
- CSS 의 `[data-theme="dark"] { --color-bg: #0a0a0a }` 의 override.
|
||||
- 매 zero JS re-render — 매 paint cycle 만 trigger.
|
||||
|
||||
### 매 응용
|
||||
1. Light/dark mode toggle.
|
||||
2. Brand white-labeling (multi-tenant SaaS).
|
||||
3. Accessibility (high-contrast, reduced-motion variant).
|
||||
4. Per-user customization (saved theme preference).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Token 정의 (CSS)
|
||||
```css
|
||||
:root {
|
||||
/* Primitive */
|
||||
--blue-500: #3B82F6;
|
||||
--gray-900: #111827;
|
||||
--gray-50: #F9FAFB;
|
||||
|
||||
/* Semantic — light default */
|
||||
--color-bg: var(--gray-50);
|
||||
--color-fg: var(--gray-900);
|
||||
--color-primary: var(--blue-500);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-bg: var(--gray-900);
|
||||
--color-fg: var(--gray-50);
|
||||
}
|
||||
|
||||
[data-theme="high-contrast"] {
|
||||
--color-bg: #000;
|
||||
--color-fg: #fff;
|
||||
--color-primary: #ffff00;
|
||||
}
|
||||
```
|
||||
|
||||
### Theme provider (React 19)
|
||||
```tsx
|
||||
import { createContext, use, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
const ThemeCtx = createContext<{ theme: Theme; set: (t: Theme) => void }>(null!);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem("theme") as Theme) ?? "system"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resolved =
|
||||
theme === "system"
|
||||
? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
: theme;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
return <ThemeCtx value={{ theme, set: setTheme }}>{children}</ThemeCtx>;
|
||||
}
|
||||
|
||||
export const useTheme = () => use(ThemeCtx);
|
||||
```
|
||||
|
||||
### FOUC 방지 (inline script)
|
||||
```html
|
||||
<!-- <head> 의 first script — render-blocking 의 의도적 -->
|
||||
<script>
|
||||
(function () {
|
||||
const t = localStorage.getItem("theme") || "system";
|
||||
const resolved = t === "system"
|
||||
? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
: t;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
### Tailwind 4 의 통합
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg: var(--bg);
|
||||
--color-fg: var(--fg);
|
||||
}
|
||||
|
||||
:root { --bg: #fff; --fg: #111; }
|
||||
[data-theme="dark"] { --bg: #0a0a0a; --fg: #f5f5f5; }
|
||||
```
|
||||
|
||||
```tsx
|
||||
<div className="bg-bg text-fg">매 theme-aware</div>
|
||||
```
|
||||
|
||||
### System preference 의 listen
|
||||
```ts
|
||||
const mq = matchMedia("(prefers-color-scheme: dark)");
|
||||
mq.addEventListener("change", (e) => {
|
||||
if (localStorage.getItem("theme") === "system") {
|
||||
document.documentElement.dataset.theme = e.matches ? "dark" : "light";
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### View Transitions API (smooth swap)
|
||||
```ts
|
||||
function toggleTheme() {
|
||||
if (!document.startViewTransition) {
|
||||
flipTheme();
|
||||
return;
|
||||
}
|
||||
document.startViewTransition(() => flipTheme());
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 250ms;
|
||||
}
|
||||
```
|
||||
|
||||
### Brand variant (multi-tenant)
|
||||
```css
|
||||
[data-brand="acme"] { --color-primary: #FF6B35; }
|
||||
[data-brand="globex"] { --color-primary: #2EB872; }
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Static site / blog | CSS variable + `data-theme` |
|
||||
| SaaS multi-tenant | CSS variable + brand attribute layer |
|
||||
| RN / Native | Theme context + StyleSheet (no CSS vars) |
|
||||
| Tailwind 의 사용 | Tailwind 4 `@theme` + CSS variable |
|
||||
| Email template | Inline styles + `prefers-color-scheme` media query |
|
||||
|
||||
**기본값**: CSS custom properties + `data-theme` attribute + inline FOUC script.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS_Architecture_and_Styling|CSS Architecture]] · [[Design Tokens]]
|
||||
- 변형: [[Tailwind CSS 4]] · [[CSS_Architecture_and_Styling|CSS-in-JS]]
|
||||
- 응용: [[Dark Mode]] · [[Accessibility (a11y)]]
|
||||
- Adjacent: [[View Transitions API]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: design token system 의 설계, dark mode 구현, multi-brand theming.
|
||||
**언제 X**: simple 의 single-color brand 의 — 매 over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **JS-only theme**: setState 의 모든 component re-render — 매 slow 의.
|
||||
- **Hard-coded color in component**: token 의 bypass — 매 swap 불가능.
|
||||
- **No FOUC script**: hydration 전 wrong theme flash — 매 jarring UX.
|
||||
- **Theme 의 localStorage 의만 의존**: SSR 의 server-render mismatch.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (MDN, web.dev, Tailwind CSS docs, Adobe Spectrum 의 token system).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 3-layer token + FOUC + View Transitions 추가 |
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
id: wiki-2026-0508-eslint-plugin-development
|
||||
title: ESLint Plugin Development
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [ESLint Custom Rules, ESLint Plugin Authoring]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [eslint, plugin, ast, linting]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/TypeScript
|
||||
framework: ESLint 9
|
||||
---
|
||||
|
||||
# ESLint Plugin Development
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 ESLint plugin은 AST visitor + RuleTester."**. ESLint 9 (2024-) Flat config 시대에는 plugin = `{rules, configs, processors}` object 의 export. 매 rule = `meta` (docs, fixable, schema) + `create(context)` returning visitor map (e.g., `CallExpression(node) { ... }`).
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 AST 기반
|
||||
- ESLint = ESTree spec (espree parser default; @typescript-eslint/parser for TS).
|
||||
- 매 rule visitor가 specific node type 방문 — `Identifier`, `CallExpression`, `JSXElement` 등.
|
||||
- `context.report({node, message, fix})` 으로 violation 보고.
|
||||
|
||||
### 매 Plugin shape (Flat config)
|
||||
- ESLint 9+ deprecated `.eslintrc` 형식 — 매 `eslint.config.js` flat config.
|
||||
- Plugin export = `{meta, rules, configs, processors}` — meta에 `name/version` 명시.
|
||||
|
||||
### 매 응용
|
||||
1. Internal style guide — 매 monorepo 의 component naming, import order.
|
||||
2. Framework rules — `eslint-plugin-react`, `eslint-plugin-vue` 처럼 framework-specific.
|
||||
3. Security lint — 매 dangerous API (eval, innerHTML) 금지.
|
||||
4. Migration codemod — 매 deprecated API 의 자동 fixer.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Minimal rule skeleton
|
||||
```javascript
|
||||
// rules/no-foo.js
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: { description: 'disallow Foo identifier', recommended: true },
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: { unexpected: "'{{name}}' is not allowed." },
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Identifier(node) {
|
||||
if (node.name === 'Foo') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unexpected',
|
||||
data: { name: node.name },
|
||||
fix: (fixer) => fixer.replaceText(node, 'Bar'),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Plugin entry (ESM, ESLint 9 flat)
|
||||
```javascript
|
||||
// index.js
|
||||
import noFoo from './rules/no-foo.js';
|
||||
|
||||
const plugin = {
|
||||
meta: { name: 'eslint-plugin-acme', version: '1.0.0' },
|
||||
rules: { 'no-foo': noFoo },
|
||||
};
|
||||
|
||||
plugin.configs = {
|
||||
recommended: {
|
||||
plugins: { acme: plugin },
|
||||
rules: { 'acme/no-foo': 'error' },
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
```
|
||||
|
||||
### 3. Consumer flat config
|
||||
```javascript
|
||||
// eslint.config.js
|
||||
import acme from 'eslint-plugin-acme';
|
||||
|
||||
export default [
|
||||
acme.configs.recommended,
|
||||
{ rules: { 'acme/no-foo': ['error'] } },
|
||||
];
|
||||
```
|
||||
|
||||
### 4. RuleTester (built-in test)
|
||||
```javascript
|
||||
import { RuleTester } from 'eslint';
|
||||
import rule from '../rules/no-foo.js';
|
||||
|
||||
const tester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: 2024, sourceType: 'module' },
|
||||
});
|
||||
|
||||
tester.run('no-foo', rule, {
|
||||
valid: ['const Bar = 1;'],
|
||||
invalid: [{
|
||||
code: 'const Foo = 1;',
|
||||
errors: [{ messageId: 'unexpected' }],
|
||||
output: 'const Bar = 1;',
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
### 5. TypeScript rule with @typescript-eslint
|
||||
```typescript
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(
|
||||
(name) => `https://acme.dev/rules/${name}`,
|
||||
);
|
||||
|
||||
export default createRule({
|
||||
name: 'no-any',
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: { description: 'disallow any' },
|
||||
schema: [],
|
||||
messages: { noAny: 'Avoid any; use unknown.' },
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
TSAnyKeyword(node) {
|
||||
context.report({ node, messageId: 'noAny' });
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Suggestions (non-auto fix)
|
||||
```javascript
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'considerRename',
|
||||
suggest: [{
|
||||
messageId: 'renameToBar',
|
||||
fix: (fixer) => fixer.replaceText(node, 'Bar'),
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
### 7. AST exploration (astexplorer.net)
|
||||
```javascript
|
||||
// Visitor pattern — leverage selector strings
|
||||
return {
|
||||
'CallExpression[callee.name="eval"]'(node) {
|
||||
context.report({ node, message: 'eval disallowed' });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 8. Scope / variable analysis
|
||||
```javascript
|
||||
create(context) {
|
||||
return {
|
||||
Identifier(node) {
|
||||
const scope = context.sourceCode.getScope(node);
|
||||
const variable = scope.references.find((r) => r.identifier === node);
|
||||
if (variable?.resolved?.defs[0]?.type === 'ImportBinding') {
|
||||
// imported identifier
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Codebase-specific 매 규칙 | Internal plugin (private npm). |
|
||||
| OSS 공유 | Publish `eslint-plugin-foo`. |
|
||||
| TS-only | `@typescript-eslint/utils` `RuleCreator`. |
|
||||
| 매 codemod 용 | jscodeshift / ts-morph (ESLint fix 보다 more powerful). |
|
||||
| 매 단순 ban API | `no-restricted-syntax` config — plugin 불필요. |
|
||||
|
||||
**기본값**: 매 ESLint 9 flat config + `@typescript-eslint/utils` RuleCreator.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[ESLint]] · [[AST]]
|
||||
- 변형: [[Biome]]
|
||||
- Adjacent: [[Prettier]] · [[TypeScript]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: AST visitor 작성, RuleTester case 생성, 매 selector string 의 작성.
|
||||
**언제 X**: 매 cross-file 분석 (ESLint = single-file) — 매 ts-morph / lsif 사용.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Regex on source**: 매 source string regex 의 X — 매 AST 사용.
|
||||
- **No tests**: 매 RuleTester 없는 rule 의 X — false positive 의 즉시 발생.
|
||||
- **Auto-fix without safety**: 매 fix가 매 semantics 변경 시 `suggest` 사용.
|
||||
- **`.eslintrc` in 2026**: 매 flat config 으로 migrate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (ESLint 9 docs, eslint.org/docs/latest/extend).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — ESLint 9 flat config plugin authoring 패턴 |
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
id: wiki-2026-0508-effect-ts-및-ts-brand-라이브러리-활용
|
||||
title: Effect TS 및 ts-brand 라이브러리 활용
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Effect-TS, ts-brand, Branded Types, Effect.gen]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, functional, effect-system, branded-types]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Effect
|
||||
---
|
||||
|
||||
# Effect TS 및 ts-brand 라이브러리 활용
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 typed effect system + nominal type 의 — TypeScript 의 의 ZIO/Cats 의 imported"**. Effect 의 의 async/error/dependency 의 의 single 의 `Effect<A, E, R>` 의 의 unify, ts-brand 의 의 structural type 의 의 nominal flavor 의 의 add. 2026 의 매 enterprise TS codebase 의 의 fast 의 emerging.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Effect 의 type
|
||||
- `Effect<A, E, R>` — 의 success `A`, 의 failure `E`, 의 requirement `R`.
|
||||
- 매 lazy — 매 `Effect.runPromise` / `Effect.runSync` 의 의 actual 의 execute.
|
||||
- 의 composable — `pipe`, `Effect.gen` 의 의 chain.
|
||||
|
||||
### 매 ts-brand 의 nominal type
|
||||
- 의 TypeScript structural — `string === string` 의 의 distinguishable X.
|
||||
- Brand 의 의 `string & { __brand: "UserId" }` 의 의 phantom tag.
|
||||
- 의 runtime cost zero — type-level only.
|
||||
|
||||
### 매 응용
|
||||
1. Domain ID (UserId, OrderId 의 의 mix-up 의 prevent).
|
||||
2. Validated value (Email, NonEmptyString).
|
||||
3. Async pipeline 의 typed error.
|
||||
4. Dependency injection (Effect Layer).
|
||||
5. Retry/timeout/concurrency 의 declarative.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### ts-brand 기본
|
||||
```ts
|
||||
import type { Brand } from "ts-brand";
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type OrderId = Brand<string, "OrderId">;
|
||||
|
||||
const makeUserId = (s: string): UserId => s as UserId;
|
||||
|
||||
function getUser(id: UserId) { /* ... */ }
|
||||
|
||||
const uid = makeUserId("u_123");
|
||||
const oid = "o_456" as OrderId;
|
||||
|
||||
getUser(uid);
|
||||
// getUser(oid); // 매 compile error — UserId 의 X
|
||||
// getUser("u_123"); // 매 compile error — raw string
|
||||
```
|
||||
|
||||
### Validated brand (smart constructor)
|
||||
```ts
|
||||
type Email = Brand<string, "Email">;
|
||||
|
||||
function parseEmail(s: string): Email | null {
|
||||
return /^[^@]+@[^@]+\.[^@]+$/.test(s) ? (s as Email) : null;
|
||||
}
|
||||
|
||||
function sendMail(to: Email, subject: string) { /* ... */ }
|
||||
|
||||
const e = parseEmail(input);
|
||||
if (e) sendMail(e, "hi"); // 매 type-narrowed 의 valid 의 only
|
||||
```
|
||||
|
||||
### Effect 기본
|
||||
```ts
|
||||
import { Effect, pipe } from "effect";
|
||||
|
||||
const fetchUser = (id: UserId): Effect.Effect<User, NetworkError | NotFoundError> =>
|
||||
Effect.tryPromise({
|
||||
try: () => fetch(`/api/users/${id}`).then((r) => {
|
||||
if (r.status === 404) throw new NotFoundError(id);
|
||||
return r.json();
|
||||
}),
|
||||
catch: (e) => e instanceof NotFoundError ? e : new NetworkError(e),
|
||||
});
|
||||
|
||||
const program = pipe(
|
||||
fetchUser(uid),
|
||||
Effect.map((u) => u.email),
|
||||
Effect.tap((email) => Effect.log(`Got: ${email}`)),
|
||||
);
|
||||
|
||||
Effect.runPromise(program).then(console.log);
|
||||
```
|
||||
|
||||
### Effect.gen (do-notation)
|
||||
```ts
|
||||
import { Effect } from "effect";
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const user = yield* fetchUser(uid);
|
||||
const orders = yield* fetchOrders(user.id);
|
||||
const valid = orders.filter((o) => o.status === "paid");
|
||||
return { user, orderCount: valid.length };
|
||||
});
|
||||
```
|
||||
|
||||
### Typed retry / timeout
|
||||
```ts
|
||||
import { Effect, Schedule, Duration } from "effect";
|
||||
|
||||
const robust = pipe(
|
||||
fetchUser(uid),
|
||||
Effect.retry(Schedule.exponential(Duration.millis(100)).pipe(Schedule.compose(Schedule.recurs(3)))),
|
||||
Effect.timeout(Duration.seconds(5)),
|
||||
);
|
||||
```
|
||||
|
||||
### Layer / dependency injection
|
||||
```ts
|
||||
import { Context, Effect, Layer } from "effect";
|
||||
|
||||
class Database extends Context.Tag("Database")<Database, {
|
||||
query: (sql: string) => Effect.Effect<unknown[], DBError>;
|
||||
}>() {}
|
||||
|
||||
const DatabaseLive = Layer.succeed(Database, {
|
||||
query: (sql) => Effect.tryPromise({ try: () => pool.query(sql), catch: (e) => new DBError(e) }),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const db = yield* Database;
|
||||
const rows = yield* db.query("SELECT * FROM users");
|
||||
return rows;
|
||||
});
|
||||
|
||||
Effect.runPromise(program.pipe(Effect.provide(DatabaseLive)));
|
||||
```
|
||||
|
||||
### Schema (Effect's Zod-equivalent)
|
||||
```ts
|
||||
import { Schema } from "effect";
|
||||
|
||||
const User = Schema.Struct({
|
||||
id: Schema.String.pipe(Schema.brand("UserId")),
|
||||
email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+$/), Schema.brand("Email")),
|
||||
age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
|
||||
});
|
||||
|
||||
type User = Schema.Schema.Type<typeof User>;
|
||||
const decoded = Schema.decodeUnknownSync(User)(input);
|
||||
```
|
||||
|
||||
### 의 Concurrency
|
||||
```ts
|
||||
import { Effect } from "effect";
|
||||
|
||||
const fetchAll = Effect.all(
|
||||
[fetchUser(u1), fetchUser(u2), fetchUser(u3)],
|
||||
{ concurrency: 2 },
|
||||
);
|
||||
```
|
||||
|
||||
### 의 Either (sync error)
|
||||
```ts
|
||||
import { Either } from "effect";
|
||||
|
||||
const safeParse = (s: string): Either.Either<number, "not_a_number"> => {
|
||||
const n = Number(s);
|
||||
return Number.isNaN(n) ? Either.left("not_a_number") : Either.right(n);
|
||||
};
|
||||
```
|
||||
|
||||
### Effect 의 React (effect-rx)
|
||||
```tsx
|
||||
import { useRxSuspense } from "@effect-rx/rx-react";
|
||||
|
||||
const userRx = Rx.make((get) => fetchUser(get(userIdRx)));
|
||||
|
||||
function UserProfile() {
|
||||
const user = useRxSuspense(userRx);
|
||||
return <div>{user.email}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Domain ID 의 의 mix-up 의 prevent | ts-brand 의만 |
|
||||
| Validated value (email, URL) | ts-brand 의 + smart constructor |
|
||||
| Complex async pipeline | Effect TS |
|
||||
| Typed error + retry + timeout | Effect TS |
|
||||
| DI 의 의 typed | Effect Layer |
|
||||
| Simple fetch + try/catch | 매 plain async — Effect 의 X |
|
||||
|
||||
**기본값**: ts-brand 의 의 always, Effect 의 의 의 complex pipeline 의만.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[Functional Programming]]
|
||||
- 변형: [[fp-ts]]
|
||||
- 응용: [[Discriminated Unions for Error Handling]] · [[Dependency Injection]]
|
||||
- Adjacent: [[Zod]] · [[Branded Types]] · [[Schema Validation]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: typed pipeline 의 design, domain ID safety, layer-based DI, schema-driven decode.
|
||||
**언제 X**: 의 매 simple CRUD — 매 Effect 의 learning curve 의 의 not worth.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`as UserId` 의 의 raw string 의 의 cast**: 의 brand 의 의 bypass — 의 smart constructor 의 의 always.
|
||||
- **Effect 의 의 entire codebase 의 의 force**: 의 team 의 의 buy-in 의 X 의 시 의 — 매 friction.
|
||||
- **`Effect.runSync` 의 의 async 의**: 의 throw 의 의 — runPromise 의 의 사용.
|
||||
- **Mutable state 의 의 Effect 의**: 의 referential transparency 의 의 violation — Ref/Layer 의 의 사용.
|
||||
- **Brand 의 의 nested 의 reuse**: `Brand<Brand<string, "A">, "B">` 의 의 confusing — 매 single brand 의 keep.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Effect-TS docs effect.website, ts-brand npm, ZIO inspiration, Effect Schema docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Effect.gen + Layer + Schema + ts-brand smart constructor 추가 |
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
---
|
||||
id: wiki-2026-0508-equipment-crafting-and-synthesis
|
||||
title: Equipment Crafting and Synthesis Engine
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Crafting System, Item Synthesis, Forge System]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [game-dev, frontend, ui, crafting, gameplay]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React
|
||||
---
|
||||
|
||||
# Equipment Crafting and Synthesis Engine
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 input item 의 set 의 의 output item 의 deterministic/stochastic 의 transformation 의 system"**. Diablo, Path of Exile, Genshin Impact 의 의 matter 의 core loop. 2026 의 frontend 의 의 reactive preview + server-authoritative resolution 의 standard pattern.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 architecture layer
|
||||
- **Recipe 의 catalog**: input pattern → output spec (data-driven JSON).
|
||||
- **Resolver**: input 의 match 의 recipe 의 find 의 — pattern matching engine.
|
||||
- **Roller**: stochastic 의 modifier roll (affix, stat range).
|
||||
- **Preview UI**: reactive 의 — input 의 change 의 의 result 의 의 live recompute.
|
||||
- **Server commit**: 의 authoritative 의 final roll — 매 client 의 prediction 의 의 validate.
|
||||
|
||||
### 매 stochastic 의 vs 의 deterministic
|
||||
- **Deterministic**: 의 fixed output (e.g. base item + recipe → fixed legendary).
|
||||
- **Stochastic**: 의 random affix (rarity tier, stat range, modifier pool).
|
||||
- **Hybrid**: deterministic base + stochastic affix.
|
||||
|
||||
### 매 응용
|
||||
1. RPG forge (sword + 의 gem → enchanted sword).
|
||||
2. Gacha synthesis (3-star × 3 → 4-star upgrade).
|
||||
3. Crafting MMORPG (pattern + 의 material → gear).
|
||||
4. Auto-battler item merge (3 의 의 same → upgraded).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Recipe 의 schema
|
||||
```ts
|
||||
type Recipe = {
|
||||
id: string;
|
||||
inputs: ItemPattern[]; // ordered or set
|
||||
ordered: boolean;
|
||||
output: OutputSpec;
|
||||
cost?: { gold?: number; energy?: number };
|
||||
unlockLevel?: number;
|
||||
};
|
||||
|
||||
type ItemPattern =
|
||||
| { kind: "exact"; itemId: string; qty: number }
|
||||
| { kind: "tag"; tag: string; rarity?: Rarity; qty: number };
|
||||
|
||||
type OutputSpec =
|
||||
| { kind: "fixed"; itemId: string }
|
||||
| { kind: "rolled"; baseItemId: string; affixPools: AffixPool[] };
|
||||
```
|
||||
|
||||
### Resolver (pattern match)
|
||||
```ts
|
||||
function findRecipe(inputs: Item[], catalog: Recipe[]): Recipe | null {
|
||||
return catalog.find((r) => matches(r, inputs)) ?? null;
|
||||
}
|
||||
|
||||
function matches(recipe: Recipe, inputs: Item[]): boolean {
|
||||
const remaining = [...inputs];
|
||||
for (const pat of recipe.inputs) {
|
||||
const idx = remaining.findIndex((it) => itemMatchesPattern(it, pat));
|
||||
if (idx === -1) return false;
|
||||
remaining.splice(idx, 1);
|
||||
}
|
||||
return remaining.length === 0;
|
||||
}
|
||||
|
||||
function itemMatchesPattern(item: Item, pat: ItemPattern): boolean {
|
||||
if (pat.kind === "exact") return item.itemId === pat.itemId;
|
||||
return item.tags.includes(pat.tag) && (!pat.rarity || item.rarity === pat.rarity);
|
||||
}
|
||||
```
|
||||
|
||||
### Affix roller (seeded)
|
||||
```ts
|
||||
import { xoshiro256ss } from "./prng";
|
||||
|
||||
type AffixPool = { tag: string; weight: number; statRange: [number, number] };
|
||||
|
||||
function rollAffixes(pools: AffixPool[], count: number, seed: bigint): Affix[] {
|
||||
const rng = xoshiro256ss(seed);
|
||||
const result: Affix[] = [];
|
||||
const available = [...pools];
|
||||
for (let i = 0; i < count && available.length > 0; i++) {
|
||||
const total = available.reduce((s, p) => s + p.weight, 0);
|
||||
let r = rng() * total;
|
||||
const idx = available.findIndex((p) => (r -= p.weight) < 0);
|
||||
const pool = available[idx];
|
||||
available.splice(idx, 1);
|
||||
const [lo, hi] = pool.statRange;
|
||||
result.push({ tag: pool.tag, value: lo + rng() * (hi - lo) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### React 의 craft preview UI
|
||||
```tsx
|
||||
function CraftBench({ inventory }: { inventory: Item[] }) {
|
||||
const [slots, setSlots] = useState<Item[]>([]);
|
||||
|
||||
const recipe = useMemo(() => findRecipe(slots, RECIPE_CATALOG), [slots]);
|
||||
const preview = useMemo(() => {
|
||||
if (!recipe) return null;
|
||||
if (recipe.output.kind === "fixed") return { kind: "fixed", item: ITEM_DB[recipe.output.baseItemId] };
|
||||
return { kind: "rolled", base: recipe.output.baseItemId, affixCount: recipe.output.affixPools.length };
|
||||
}, [recipe]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<SlotGrid slots={slots} setSlots={setSlots} inventory={inventory} />
|
||||
<PreviewPanel recipe={recipe} preview={preview} onCraft={() => craft(slots)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server-authoritative commit
|
||||
```ts
|
||||
// Client
|
||||
async function craft(inputs: Item[]) {
|
||||
const r = await fetch("/api/craft", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ inputIds: inputs.map((i) => i.instanceId) }),
|
||||
});
|
||||
if (!r.ok) throw new Error("craft failed");
|
||||
return r.json() as Promise<CraftResult>;
|
||||
}
|
||||
|
||||
// Server (authoritative seed)
|
||||
function handleCraft(req) {
|
||||
const inputs = loadInventory(req.userId, req.inputIds);
|
||||
const recipe = findRecipe(inputs, CATALOG);
|
||||
if (!recipe) return { ok: false, reason: "no_match" };
|
||||
|
||||
const seed = BigInt(`0x${randomBytes(8).toString("hex")}`);
|
||||
const result = applyOutput(recipe.output, seed);
|
||||
consumeInputs(req.userId, inputs);
|
||||
grantItem(req.userId, result);
|
||||
return { ok: true, item: result, seed: seed.toString() };
|
||||
}
|
||||
```
|
||||
|
||||
### Drag-drop 의 slot
|
||||
```tsx
|
||||
function Slot({ item, onDrop }: { item?: Item; onDrop: (it: Item) => void }) {
|
||||
return (
|
||||
<div
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
const id = e.dataTransfer.getData("text/plain");
|
||||
const it = INVENTORY.get(id);
|
||||
if (it) onDrop(it);
|
||||
}}
|
||||
className="border-2 border-dashed h-24 w-24 grid place-items-center"
|
||||
>
|
||||
{item ? <ItemIcon item={item} /> : <span>+</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Probability 의 display
|
||||
```tsx
|
||||
function ProbabilityBar({ pools }: { pools: AffixPool[] }) {
|
||||
const total = pools.reduce((s, p) => s + p.weight, 0);
|
||||
return (
|
||||
<ul>
|
||||
{pools.map((p) => (
|
||||
<li key={p.tag}>
|
||||
{p.tag} — {((p.weight / total) * 100).toFixed(1)}%
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Casual game / mobile | Deterministic recipe — 매 predictability |
|
||||
| ARPG / Diablo-like | Stochastic affix + 의 base 의 deterministic |
|
||||
| Gacha / loot | Stochastic 의 의 server seed 의 authoritative |
|
||||
| Single-player offline | Client RNG 의 OK |
|
||||
| Multiplayer competitive | 매 server 의 의 seed 의 — anti-cheat |
|
||||
|
||||
**기본값**: data-driven recipe + server-rolled affix + reactive preview UI.
|
||||
|
||||
## 🔗 Graph
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: crafting recipe schema 설계, affix pool weighting, preview reactive UI.
|
||||
**언제 X**: 매 simple 의 single-purpose game (puzzle, runner) — 매 over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Client-only roll**: 매 trivially exploitable (replay, save-scum).
|
||||
- **Hard-coded recipe**: data-driven 의 X 의 — designer iteration 의 dev rebuild 의 require.
|
||||
- **Hidden probability**: regulator 의 (China, Korea, Japan) 의 force disclose.
|
||||
- **Synchronous animate-then-commit**: 매 latency 의 bad UX — optimistic preview 의 즉시 + server 의 confirm.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Diablo IV crafting docs, Path of Exile wiki, Genshin gacha rates, GDC talks 의 crafting design).
|
||||
- 신뢰도 B+ — game-specific 의 variance.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — recipe schema + affix roller + server authority 추가 |
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
id: wiki-2026-0508-error-boundaries
|
||||
title: Error Boundaries
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Error Boundary, ErrorBoundary, getDerivedStateFromError]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, error-handling, frontend, resilience]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React
|
||||
---
|
||||
|
||||
# Error Boundaries
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 React subtree 의 의 render-time error 의 catch 의 의 fallback UI 의 의 swap 의 component"**. 의 entire app crash 의 의 prevent 의 — 의 try/catch 의 declarative React equivalent. 2026 의 의 매 React 19 의 still 의 class component 의만 의 의 Error Boundary 의 author 의 가능 (의 hook API 의 X).
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 catch 의 X 의 것
|
||||
- Event handler error (의 try/catch 의 사용).
|
||||
- Async code (setTimeout, promise) — Error Boundary 의 의 reach 의 X.
|
||||
- SSR (의 server-side throw 의 의 catch 의 의 X).
|
||||
- Boundary itself 의 throw.
|
||||
|
||||
### 매 catch 의 것
|
||||
- 의 child render error.
|
||||
- 의 lifecycle method error.
|
||||
- 의 constructor error.
|
||||
|
||||
### 매 hierarchy strategy
|
||||
- **Top-level**: 의 fallback "Something went wrong" 의 의 entire app crash 의 의 prevent.
|
||||
- **Route-level**: 의 broken page 의만 의 의 fail.
|
||||
- **Widget-level**: 의 chart, embed, third-party 의 의 isolate.
|
||||
|
||||
### 매 응용
|
||||
1. Production crash 의 reporting (Sentry, Datadog).
|
||||
2. Third-party widget 의 isolation.
|
||||
3. Per-route error UI (Next.js `error.tsx`).
|
||||
4. Suspense fallback combo (loading + error).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 기본 class boundary
|
||||
```tsx
|
||||
import { Component, type ReactNode, type ErrorInfo } from "react";
|
||||
|
||||
type Props = { fallback: ReactNode; onError?: (e: Error, info: ErrorInfo) => void; children: ReactNode };
|
||||
type State = { hasError: boolean };
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(): State {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
this.props.onError?.(error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.hasError ? this.props.fallback : this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### react-error-boundary 라이브러리
|
||||
```tsx
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
function Fallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:</p>
|
||||
<pre>{error.message}</pre>
|
||||
<button onClick={resetErrorBoundary}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<ErrorBoundary
|
||||
FallbackComponent={Fallback}
|
||||
onError={(error, info) => Sentry.captureException(error, { extra: info })}
|
||||
onReset={() => queryClient.resetQueries()}
|
||||
>
|
||||
<Dashboard />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Suspense + ErrorBoundary 의 combo
|
||||
```tsx
|
||||
<ErrorBoundary FallbackComponent={Fallback}>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<UserProfile id={userId} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### 의 async event 의 의 manual rethrow
|
||||
```tsx
|
||||
function useAsyncError() {
|
||||
const [, setState] = useState();
|
||||
return useCallback((e: unknown) => {
|
||||
setState(() => { throw e; });
|
||||
}, []);
|
||||
}
|
||||
|
||||
function MyComponent() {
|
||||
const throwAsync = useAsyncError();
|
||||
const onClick = async () => {
|
||||
try {
|
||||
await fetchData();
|
||||
} catch (e) {
|
||||
throwAsync(e); // 매 ErrorBoundary 의 의 reach
|
||||
}
|
||||
};
|
||||
return <button onClick={onClick}>Load</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js App Router `error.tsx`
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
useEffect(() => { console.error(error); }, [error]);
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong</h2>
|
||||
<button onClick={reset}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Per-route boundary
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={
|
||||
<ErrorBoundary FallbackComponent={HomeFailed}><Home /></ErrorBoundary>
|
||||
} />
|
||||
<Route path="dashboard" element={
|
||||
<ErrorBoundary FallbackComponent={DashFailed}><Dashboard /></ErrorBoundary>
|
||||
} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
### 의 Sentry 의 integrate
|
||||
```tsx
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
const SentryBoundary = Sentry.withErrorBoundary(MyApp, {
|
||||
fallback: ({ error, resetError }) => <Fallback error={error} resetErrorBoundary={resetError} />,
|
||||
showDialog: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Reset on prop change
|
||||
```tsx
|
||||
<ErrorBoundary
|
||||
FallbackComponent={Fallback}
|
||||
resetKeys={[userId]} // 의 userId 의 의 change 의 시 의 boundary reset
|
||||
>
|
||||
<UserProfile id={userId} />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Top-level app crash 의 prevent | Single root boundary + telemetry |
|
||||
| Route 의 isolation | Per-route boundary (Next.js `error.tsx`) |
|
||||
| Third-party widget | Tight boundary 의 의 widget 의만 |
|
||||
| Async data fetch | TanStack Query 의 `throwOnError` + boundary |
|
||||
| Form validation | Inline error UI — 매 boundary 의 X |
|
||||
|
||||
**기본값**: root + per-route + per-widget — 매 layered boundaries.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[Error Handling]]
|
||||
- 변형: [[Suspense]] · [[React Error Boundary 패턴]]
|
||||
- Adjacent: [[Discriminated Unions for Error Handling]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: production crash protection, third-party widget isolation, Suspense + error 의 combo.
|
||||
**언제 X**: form-level validation — 매 inline UI 의 더 적합.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No top-level boundary**: 의 single child throw 의 의 entire app white-screen.
|
||||
- **Boundary over async without rethrow**: 의 catch 의 의 fail — 의 manual rethrow 의 필요.
|
||||
- **Eat error silently**: 매 telemetry 의 의 X — debug 의 impossible.
|
||||
- **Reset 의 의 X**: user 의 의 retry 의 의 way 의 X — stuck 의 fallback.
|
||||
- **getDerivedStateFromError 의 의 side effect**: pure function 의 의 의 violation.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 19 docs, react-error-boundary, Next.js App Router error handling, Sentry React SDK).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — async rethrow + Next.js error.tsx + reset key 추가 |
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
id: wiki-2026-0508-error-handling-and-stability
|
||||
title: Error Handling and Stability
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Frontend Error Boundaries, Error Recovery]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [errors, stability, observability, sentry]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React/Vue
|
||||
---
|
||||
|
||||
# Error Handling and Stability
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 frontend stability = error 의 isolation + telemetry + graceful degradation."**. 매 single uncaught error가 매 SPA 의 전체 white-screen 의 유발 — 매 ErrorBoundary, global handlers (`window.onerror`, `unhandledrejection`), 매 Sentry-class telemetry 가 필수. 매 2026의 트렌드는 React 19 ErrorBoundary + Sentry + Replay + AI-driven root-cause clustering.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 layers
|
||||
- **Component**: React ErrorBoundary, Vue `errorCaptured`, Svelte `<svelte:boundary>`.
|
||||
- **Async**: try/catch, Promise `.catch`, `unhandledrejection` listener.
|
||||
- **Global**: `window.onerror`, `window.onunhandledrejection`.
|
||||
- **Network**: fetch retry, circuit breaker, AbortController.
|
||||
|
||||
### 매 telemetry 기둥
|
||||
- Capture (stack, breadcrumb, source map).
|
||||
- Group (fingerprint, dedup).
|
||||
- Alert (threshold, regression).
|
||||
- Replay (Sentry / LogRocket — DOM reconstruction).
|
||||
|
||||
### 매 응용
|
||||
1. SPA route-level boundary — 매 chunk load fail 의 reload prompt.
|
||||
2. Form submission retry with exponential backoff.
|
||||
3. Feature flag fallback when remote config unavailable.
|
||||
4. Service Worker offline shell.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. React ErrorBoundary (class)
|
||||
```typescript
|
||||
import { Component, type ReactNode } from 'react';
|
||||
|
||||
interface State { error?: Error }
|
||||
|
||||
export class ErrorBoundary extends Component<{ fallback: ReactNode; children: ReactNode }, State> {
|
||||
state: State = {};
|
||||
static getDerivedStateFromError(error: Error): State { return { error }; }
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
Sentry.captureException(error, { extra: { componentStack: info.componentStack } });
|
||||
}
|
||||
render() {
|
||||
return this.state.error ? this.props.fallback : this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. react-error-boundary (functional)
|
||||
```typescript
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<div role="alert">
|
||||
<p>Failed: {error.message}</p>
|
||||
<button onClick={resetErrorBoundary}>Retry</button>
|
||||
</div>
|
||||
)}
|
||||
onReset={() => location.reload()}
|
||||
>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### 3. Global handlers
|
||||
```typescript
|
||||
window.addEventListener('error', (e) => {
|
||||
Sentry.captureException(e.error ?? new Error(e.message));
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
Sentry.captureException(e.reason);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Chunk load failure recovery
|
||||
```typescript
|
||||
const lazyWithRetry = <T,>(load: () => Promise<{ default: T }>) =>
|
||||
React.lazy(async () => {
|
||||
try {
|
||||
return await load();
|
||||
} catch (err) {
|
||||
if (!sessionStorage.getItem('chunk-retried')) {
|
||||
sessionStorage.setItem('chunk-retried', '1');
|
||||
location.reload();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Fetch retry with backoff
|
||||
```typescript
|
||||
async function fetchRetry(url: string, retries = 3, delay = 500): Promise<Response> {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok && res.status >= 500) throw new Error(`HTTP ${res.status}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
if (retries === 0) throw err;
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
return fetchRetry(url, retries - 1, delay * 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. AbortController on unmount
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const ac = new AbortController();
|
||||
fetch('/api/x', { signal: ac.signal }).catch((e) => {
|
||||
if (e.name !== 'AbortError') report(e);
|
||||
});
|
||||
return () => ac.abort();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 7. Sentry init (2026)
|
||||
```typescript
|
||||
import * as Sentry from '@sentry/react';
|
||||
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({ maskAllText: false }),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
release: import.meta.env.VITE_RELEASE,
|
||||
});
|
||||
```
|
||||
|
||||
### 8. Vue 3 errorHandler
|
||||
```typescript
|
||||
import { createApp } from 'vue';
|
||||
const app = createApp(App);
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
Sentry.captureException(err, { extra: { info } });
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Component-local fail | ErrorBoundary at route level. |
|
||||
| Async fail | try/catch + telemetry. |
|
||||
| Chunk 404 (deploy mid-session) | lazyWithRetry + reload. |
|
||||
| Transient 5xx | Exponential backoff retry. |
|
||||
| Auth expired | Refresh token interceptor. |
|
||||
|
||||
**기본값**: Sentry + Replay + per-route ErrorBoundary + global handlers.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Observability]]
|
||||
- 변형: [[Circuit Breaker]]
|
||||
- 응용: [[Sentry]] · [[Logrocket]]
|
||||
- Adjacent: [[Source Maps]] · [[Feature Flags]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: ErrorBoundary scaffolding, retry helper 작성, Sentry config.
|
||||
**언제 X**: 매 root-cause analysis from minified stack — 매 source map upload 필수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Swallow errors**: 매 `catch {}` 빈 — 매 silent fail.
|
||||
- **No source map**: 매 prod stack 의 `a.b.c` — 매 debug X.
|
||||
- **Boundary at root only**: 매 한 component fail이 매 entire app crash.
|
||||
- **Console.error as monitoring**: 매 user 의 console 의 도달 X — telemetry 필수.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Sentry docs, react.dev error boundary).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Error boundary + Sentry 2026 patterns |
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
id: wiki-2026-0508-es-lint-configuration
|
||||
title: ESLint Configuration
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [ESLint Config, eslint.config.js, Flat Config]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [eslint, linting, javascript, typescript, tooling]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: eslint
|
||||
---
|
||||
|
||||
# ESLint Configuration
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 ESLint v9+ flat config (`eslint.config.js`) 는 array-of-config-objects 의 explicit composition"**. Legacy `.eslintrc.*` 의 deprecated. 매 modern setup 의 typescript-eslint, prettier integration, framework presets (Next, React) 의 mix.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Flat Config (v9+)
|
||||
- Single `eslint.config.js` (or `.mjs`) at project root.
|
||||
- Exports array; each entry: `{ files, ignores, languageOptions, plugins, rules, ... }`.
|
||||
- No `extends` — use `...config` spread.
|
||||
- No `env` — use `globals` from `globals` package.
|
||||
|
||||
### 매 Core Concepts
|
||||
- **languageOptions**: parser, parserOptions, globals.
|
||||
- **plugins**: object map (`{ pluginName: pluginObject }`).
|
||||
- **rules**: 'off' / 'warn' / 'error' (or 0/1/2), with config tuple `['error', opts]`.
|
||||
- **files**: glob array (e.g. `['**/*.ts']`) — scope rules.
|
||||
- **ignores**: replaces `.eslintignore`.
|
||||
|
||||
### 매 응용
|
||||
1. TypeScript project linting (typescript-eslint).
|
||||
2. React/Next.js framework rules.
|
||||
3. Monorepo per-package overrides.
|
||||
4. Prettier integration (eslint-config-prettier).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Minimal Modern Config
|
||||
```js
|
||||
// eslint.config.js (ESM)
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist/**', 'node_modules/**', '.next/**'] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
globals: { ...globals.browser, ...globals.node },
|
||||
},
|
||||
rules: {
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### React + TypeScript
|
||||
```js
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parserOptions: { project: './tsconfig.json' },
|
||||
},
|
||||
plugins: { react, 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y },
|
||||
settings: { react: { version: 'detect' } },
|
||||
rules: {
|
||||
...react.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/react-in-jsx-scope': 'off', // not needed in React 17+
|
||||
'react/prop-types': 'off', // TS handles this
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Next.js (15+)
|
||||
```js
|
||||
import next from '@next/eslint-plugin-next';
|
||||
|
||||
export default [
|
||||
// ... base configs
|
||||
{
|
||||
plugins: { '@next/next': next },
|
||||
rules: {
|
||||
...next.configs.recommended.rules,
|
||||
...next.configs['core-web-vitals'].rules,
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Prettier Integration
|
||||
```bash
|
||||
npm i -D eslint-config-prettier
|
||||
```
|
||||
```js
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
// ... your configs
|
||||
prettier, // MUST be last — disables ESLint formatting rules
|
||||
];
|
||||
```
|
||||
|
||||
### Per-File Overrides
|
||||
```js
|
||||
export default [
|
||||
// base
|
||||
{
|
||||
files: ['**/*.test.{ts,tsx}'],
|
||||
languageOptions: { globals: { ...globals.jest } },
|
||||
rules: { '@typescript-eslint/no-explicit-any': 'off' },
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: { sourceType: 'commonjs' },
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Type-Aware Rules
|
||||
```js
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: true, // auto-find nearest tsconfig
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### package.json Scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:cache": "eslint . --cache --cache-location .cache/eslint"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New project (2026) | Flat config (v9+) |
|
||||
| Legacy `.eslintrc` | Migrate via `@eslint/migrate-config` |
|
||||
| Formatting | Prettier; eslint-config-prettier last |
|
||||
| Type-aware rules | parserOptions.project: true |
|
||||
| Monorepo | Root config + per-package overrides via files glob |
|
||||
|
||||
**기본값**: flat config + typescript-eslint recommended-type-checked + Prettier integration.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[ESLint]] · [[Code Quality]]
|
||||
- 변형: [[Biome]] (alternative) · [[oxlint]] (alternative)
|
||||
- 응용: [[153_pre-commit과_품질_게이트|Pre-commit Hooks]]
|
||||
- Adjacent: [[Prettier]] · [[Husky]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: ESLint setup, rule conflict 의 debug, flat config migration.
|
||||
**언제 X**: extreme performance — Biome / oxlint 의 consider.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`.eslintrc.*` in v9+**: deprecated, no longer auto-loaded.
|
||||
- **Prettier as ESLint plugin (`eslint-plugin-prettier`)**: slow, conflict-prone. Use Prettier separately + eslint-config-prettier.
|
||||
- **`extends` in flat config**: doesn't exist; use spread.
|
||||
- **Global rules without `files` scope in monorepo**: lints unintended files.
|
||||
- **Disabling whole categories**: targeted overrides 의 prefer.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (eslint.org docs — flat config, typescript-eslint v8 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — ESLint flat config full content |
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
id: wiki-2026-0508-events
|
||||
title: Events
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [DOM Events, Event Handling, JavaScript Events]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, dom, events, javascript]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: dom
|
||||
---
|
||||
|
||||
# Events
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 DOM event 는 capture → target → bubble 3-phase 의 propagation"**. Events 는 user/system action (click, input, scroll, ...) 을 JS handler 에 dispatch 하는 mechanism. 매 modern app 의 React SyntheticEvent / addEventListener / passive listener 의 mix 의 사용.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Event Phases
|
||||
- **Capture phase**: root → target (top-down). `{ capture: true }` 의 trigger.
|
||||
- **Target phase**: target element 의 listener.
|
||||
- **Bubble phase**: target → root (default). `e.stopPropagation()` 의 중단.
|
||||
|
||||
### 매 Event Types
|
||||
- **Mouse**: click, dblclick, mousedown/up, mousemove, mouseenter/leave (no bubble), mouseover/out (bubble).
|
||||
- **Keyboard**: keydown, keyup (NOT keypress — deprecated).
|
||||
- **Touch/Pointer**: pointerdown/up/move (unified mouse+touch), touchstart/move/end.
|
||||
- **Form**: input (every keystroke), change (commit), submit, focus/blur (no bubble), focusin/out (bubble).
|
||||
- **Lifecycle**: DOMContentLoaded, load, beforeunload, visibilitychange.
|
||||
- **Custom**: `new CustomEvent('foo', { detail: {...} })`.
|
||||
|
||||
### 매 응용
|
||||
1. UI interaction (button click, form submit).
|
||||
2. Event delegation (single listener for many children).
|
||||
3. Drag-and-drop (pointer events).
|
||||
4. Keyboard shortcuts / accessibility.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic addEventListener
|
||||
```js
|
||||
button.addEventListener('click', (e) => {
|
||||
console.log('clicked', e.target);
|
||||
});
|
||||
|
||||
// removeEventListener requires same fn reference
|
||||
const handler = (e) => console.log(e);
|
||||
el.addEventListener('click', handler);
|
||||
el.removeEventListener('click', handler);
|
||||
```
|
||||
|
||||
### Event Delegation
|
||||
```js
|
||||
// Single listener on parent — handles all child clicks
|
||||
document.querySelector('#list').addEventListener('click', (e) => {
|
||||
const item = e.target.closest('[data-id]');
|
||||
if (!item) return;
|
||||
console.log('item:', item.dataset.id);
|
||||
});
|
||||
```
|
||||
|
||||
### Passive Listener (Scroll Performance)
|
||||
```js
|
||||
// Tells browser handler won't preventDefault → no scroll-blocking
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: true });
|
||||
```
|
||||
|
||||
### AbortController (modern cleanup)
|
||||
```js
|
||||
const ctrl = new AbortController();
|
||||
el.addEventListener('click', handler, { signal: ctrl.signal });
|
||||
el.addEventListener('mouseover', other, { signal: ctrl.signal });
|
||||
// Remove all at once
|
||||
ctrl.abort();
|
||||
```
|
||||
|
||||
### Stop Propagation vs Prevent Default
|
||||
```js
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault(); // cancel default (form GET/POST)
|
||||
e.stopPropagation(); // don't bubble to ancestors
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Events
|
||||
```js
|
||||
const evt = new CustomEvent('user:login', {
|
||||
detail: { userId: 42 },
|
||||
bubbles: true,
|
||||
});
|
||||
element.dispatchEvent(evt);
|
||||
|
||||
document.addEventListener('user:login', (e) => {
|
||||
console.log(e.detail.userId);
|
||||
});
|
||||
```
|
||||
|
||||
### React SyntheticEvent
|
||||
```jsx
|
||||
function Button() {
|
||||
// React pools events; e.persist() no longer needed (React 17+)
|
||||
const onClick = (e) => {
|
||||
console.log(e.nativeEvent); // underlying DOM event
|
||||
console.log(e.currentTarget);
|
||||
};
|
||||
return <button onClick={onClick}>Click</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Pointer Events (drag)
|
||||
```js
|
||||
let dragging = false;
|
||||
el.addEventListener('pointerdown', (e) => {
|
||||
el.setPointerCapture(e.pointerId);
|
||||
dragging = true;
|
||||
});
|
||||
el.addEventListener('pointermove', (e) => {
|
||||
if (!dragging) return;
|
||||
el.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
|
||||
});
|
||||
el.addEventListener('pointerup', (e) => {
|
||||
el.releasePointerCapture(e.pointerId);
|
||||
dragging = false;
|
||||
});
|
||||
```
|
||||
|
||||
### Debounce / Throttle
|
||||
```js
|
||||
function debounce(fn, ms) {
|
||||
let t;
|
||||
return (...args) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
input.addEventListener('input', debounce(search, 300));
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 다수 child 의 listener | Delegation (single parent listener) |
|
||||
| Scroll/touch handler | `{ passive: true }` |
|
||||
| 다수 listener cleanup | `AbortController` |
|
||||
| Mouse + touch unified | Pointer events |
|
||||
| Cross-component coordination | CustomEvent or state library |
|
||||
|
||||
**기본값**: `addEventListener` + delegation + AbortController for cleanup.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[DOM]] · [[JavaScript]]
|
||||
- 응용: [[Drag and Drop]]
|
||||
- Adjacent: [[Accessibility (A11y)|Accessibility]] · [[Performance]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: event listener pattern 의 question, propagation 의 debug, delegation 의 implement.
|
||||
**언제 X**: framework-specific event system 의 deep dive (React/Vue 의 own docs 의 참조).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Inline `onclick=""` attribute**: HTML/JS 의 mix, CSP 의 violation.
|
||||
- **No cleanup in SPA**: memory leak. 매 unmount 의 removeEventListener 의 호출.
|
||||
- **`scroll` without passive**: 60fps scroll 의 block.
|
||||
- **`stopPropagation()` overuse**: delegation pattern 의 break.
|
||||
- **Listener on every list item**: N 의 listener 대신 delegation 의 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (MDN Web Docs — Event reference, WHATWG DOM spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — DOM event handling full content |
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Expo Router
|
||||
description: "**Expo Router**는 **React Native 및 웹 애플리케이션을 위한 파일 기반 라우팅 시스템**입니다 [1, 2]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Expo Router
|
||||
|
||||
## 📌 Brief Summary
|
||||
**Expo Router**는 **React Native 및 웹 애플리케이션을 위한 파일 기반 라우팅 시스템**입니다 [1, 2]. Next.js와 같은 웹 프레임워크에서 깊은 영감을 받아 만들어졌으며, 웹과 모바일 개발 사이의 간극을 효과적으로 메워줍니다 [3]. 개발자가 디렉토리에 파일을 생성하는 것만으로 iOS, Android, 웹을 위한 화면과 내비게이션 스택을 손쉽게 정의할 수 있도록 지원합니다 [3].
|
||||
|
||||
## 📖 Core Content
|
||||
* **자동화된 파일 기반 라우팅:** Expo Router는 디렉토리 내에 새로운 파일을 생성하면 **해당 파일이 앱의 내비게이션 경로(Route)로 자동으로 변환**되는 구조를 제공합니다 [1]. 이러한 접근 방식은 애플리케이션의 내비게이션 아키텍처를 크게 단순화하며, 코드의 조직화 및 라우팅 관리를 훨씬 효율적으로 만들어 줍니다 [1, 2].
|
||||
* **진정한 유니버설 앱 패러다임 구축:** 이 라우팅 시스템은 Android, iOS, 웹 등 **여러 플랫폼에 걸쳐 동일한 컴포넌트의 사용을 지원**합니다 [1]. 이를 통해 단일 코드베이스로 진정한 의미의 '유니버설 앱 패러다임'을 형성할 수 있으며, 특히 기존 웹 개발자가 모바일 개발로 넘어가는 전환 과정을 매우 매끄럽게 만들어 줍니다 [3].
|
||||
* **강력한 Expo 에코시스템과의 통합:** Expo Router는 EAS(Expo Application Services) 빌드 및 배포 파이프라인과 함께 프로덕션 수준의 앱 개발을 위한 핵심 도구로 자리 잡고 있습니다 [3, 4]. 이를 활용하면 개발자는 과거 React Native 설정 시 겪었던 복잡한 네이티브 빌드 구성을 피하고, 단순화된 내비게이션 아키텍처의 이점을 누릴 수 있습니다 [4].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
소스에 관련 정보가 부족합니다. (제공된 소스 데이터 내에는 Expo Router 자체의 구체적인 단점, 제약 사항 또는 기술적 반대 급부(Trade-off)에 대해 명시된 내용이 없습니다.)
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
id: wiki-2026-0508-eye-tracking
|
||||
title: Eye Tracking
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Gaze Tracking, Eye Gaze, Foveated Rendering Input]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [eye-tracking, xr, vr, ar, accessibility, ux]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: webxr
|
||||
---
|
||||
|
||||
# Eye Tracking
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 eye tracking 은 user gaze direction 의 measure — pupil/cornea reflection 또는 ML model 의 사용"**. 2026 의 mainstream: Apple Vision Pro, Meta Quest 3S Pro, PSVR2. 매 foveated rendering, gaze-based UI selection, accessibility (ALS), UX research 의 enables.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Sensing Methods
|
||||
- **PCCR (Pupil Center Corneal Reflection)**: IR LED illumination + IR camera. Sub-degree accuracy, dedicated HW (Tobii, Vision Pro).
|
||||
- **Webcam-based ML**: WebGazer.js, MediaPipe Iris. ~3-5° accuracy, no special HW.
|
||||
- **Electrooculography (EOG)**: skin electrodes; medical/lab use.
|
||||
- **Scleral search coil**: highest precision, invasive (research only).
|
||||
|
||||
### 매 Output Data
|
||||
- **Gaze point**: 2D screen coord or 3D ray.
|
||||
- **Pupil diameter**: cognitive load proxy.
|
||||
- **Fixations**: stable gaze (>100ms) — what user actually reads.
|
||||
- **Saccades**: rapid eye movement between fixations.
|
||||
- **Smooth pursuit**: tracking moving target.
|
||||
|
||||
### 매 응용
|
||||
1. Foveated rendering (high-res only at gaze point — VR perf 4-10x).
|
||||
2. Gaze + pinch UI selection (Apple Vision Pro paradigm).
|
||||
3. Accessibility: gaze-typing for ALS/locked-in patients.
|
||||
4. UX research: heatmaps, attention analysis.
|
||||
5. Driver monitoring (drowsiness detection).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### WebGazer.js (browser, no HW)
|
||||
```js
|
||||
import webgazer from 'webgazer';
|
||||
|
||||
await webgazer.setRegression('ridge')
|
||||
.setGazeListener((data, timestamp) => {
|
||||
if (!data) return;
|
||||
console.log('gaze:', data.x, data.y); // screen px
|
||||
})
|
||||
.begin();
|
||||
|
||||
// User must calibrate by clicking around
|
||||
```
|
||||
|
||||
### MediaPipe Iris (TF.js)
|
||||
```js
|
||||
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection';
|
||||
|
||||
const detector = await faceLandmarksDetection.createDetector(
|
||||
faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh,
|
||||
{ runtime: 'tfjs', refineLandmarks: true } // refine = iris landmarks
|
||||
);
|
||||
|
||||
const faces = await detector.estimateFaces(video);
|
||||
const irisLeft = faces[0].keypoints.filter(k => k.name?.includes('leftIris'));
|
||||
const irisRight = faces[0].keypoints.filter(k => k.name?.includes('rightIris'));
|
||||
```
|
||||
|
||||
### WebXR Eye Tracking (Vision Pro Safari, Quest)
|
||||
```ts
|
||||
const session = await navigator.xr.requestSession('immersive-vr', {
|
||||
requiredFeatures: ['eye-tracking'],
|
||||
});
|
||||
|
||||
session.requestAnimationFrame(function onFrame(t, frame) {
|
||||
const eyeSpace = session.requestReferenceSpace('viewer');
|
||||
const gaze = frame.getViewerPose(eyeSpace);
|
||||
// gaze.transform.position / orientation = gaze ray
|
||||
session.requestAnimationFrame(onFrame);
|
||||
});
|
||||
```
|
||||
|
||||
### Fixation Detection (I-DT algorithm)
|
||||
```ts
|
||||
// Identification by Dispersion Threshold
|
||||
function detectFixations(gazePoints: {x:number, y:number, t:number}[],
|
||||
dispersionPx = 50, minDurationMs = 100) {
|
||||
const fixations = [];
|
||||
let window: typeof gazePoints = [];
|
||||
for (const p of gazePoints) {
|
||||
window.push(p);
|
||||
const xs = window.map(g => g.x), ys = window.map(g => g.y);
|
||||
const disp = (Math.max(...xs) - Math.min(...xs))
|
||||
+ (Math.max(...ys) - Math.min(...ys));
|
||||
if (disp > dispersionPx) {
|
||||
const dur = window[window.length-2].t - window[0].t;
|
||||
if (dur >= minDurationMs) fixations.push(window.slice(0, -1));
|
||||
window = [p];
|
||||
}
|
||||
}
|
||||
return fixations;
|
||||
}
|
||||
```
|
||||
|
||||
### Foveated Rendering Hint (WebGPU)
|
||||
```ts
|
||||
// VRS / foveated rendering via GPU extension (browser-dependent)
|
||||
const passEncoder = encoder.beginRenderPass({
|
||||
colorAttachments: [...],
|
||||
// implementation-specific: shading rate texture pointing at gaze
|
||||
});
|
||||
```
|
||||
|
||||
### Heatmap from Fixations
|
||||
```ts
|
||||
function buildHeatmap(fixations, w, h, sigma = 30) {
|
||||
const map = new Float32Array(w * h);
|
||||
for (const f of fixations) {
|
||||
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
|
||||
const d2 = (x-f.x)**2 + (y-f.y)**2;
|
||||
map[y*w+x] += Math.exp(-d2 / (2*sigma*sigma)) * f.duration;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| VR/AR headset (2026) | Native eye-tracking API (visionOS, OpenXR ext) |
|
||||
| Browser, accuracy critical | Tobii / dedicated HW + SDK |
|
||||
| Browser, no HW | WebGazer.js or MediaPipe Iris (lower accuracy) |
|
||||
| Accessibility input | Tobii Eye Tracker 5 + Windows Eye Control |
|
||||
| UX research | Tobii Pro / GazePoint + heatmap tool |
|
||||
|
||||
**기본값**: 매 platform 의 native eye-tracking API. Browser fallback: MediaPipe Iris.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Human Computer Interaction]] · [[XR]]
|
||||
- 응용: [[Accessibility (A11y)|Accessibility]]
|
||||
- Adjacent: [[WebXR]] · [[MediaPipe]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: gaze-based UI 의 design, foveated rendering question, accessibility input.
|
||||
**언제 X**: physiological eye-disease diagnosis (clinical 의 ophthalmologist).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No calibration**: webcam-based 의 every-user calibration 의 require.
|
||||
- **Click-on-gaze without dwell time**: Midas touch problem — every glance triggers.
|
||||
- **Storing raw gaze without consent**: PII / biometric (GDPR Art. 9).
|
||||
- **Webcam ML for safety-critical**: insufficient accuracy (driver assist 의 dedicated HW).
|
||||
- **Ignoring head movement**: gaze ≠ pupil position; head pose 의 compensate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Apple visionOS docs, OpenXR EXT_eye_gaze_interaction, Tobii Developer Zone).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — eye tracking full content |
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
id: wiki-2026-0508-fabric
|
||||
title: Fabric
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Native Fabric, Fabric Renderer, RN New Architecture]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react-native, fabric, renderer, mobile, jsi]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: cpp
|
||||
framework: react-native
|
||||
---
|
||||
|
||||
# Fabric
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Fabric 는 React Native 의 new renderer — JSI 기반 synchronous JS↔native 의 enables"**. 2018-2024 에 incrementally rolled out, RN 0.74+ 에서 default. 매 legacy bridge (async JSON serialization) 의 replace, 매 concurrent React features (Suspense, Transitions) 의 mobile 의 enable.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Architecture (vs Old Bridge)
|
||||
- **Old bridge**: JS thread ↔ Native thread async JSON messages. Serialize cost, no sync calls, list jank.
|
||||
- **Fabric**: JSI (JavaScript Interface) — JS engine (Hermes) 의 C++ HostObjects 의 direct access. Sync calls, shared memory, type-safe via codegen.
|
||||
|
||||
### 매 Components
|
||||
- **JSI**: lightweight C++ API for JS engines (Hermes/JSC). 매 binding의 base.
|
||||
- **Fabric Renderer (C++)**: shadow tree, layout (Yoga), commit phase. 매 cross-platform.
|
||||
- **TurboModules**: lazy-loaded native modules with codegen-typed interface.
|
||||
- **Codegen**: TS/Flow types → C++/Java/ObjC native code.
|
||||
- **Hermes**: default JS engine (faster startup, lower memory, bytecode).
|
||||
|
||||
### 매 응용
|
||||
1. React 18 concurrent features (Suspense, useTransition) on mobile.
|
||||
2. Synchronous measure/layout queries.
|
||||
3. Type-safe native module bindings.
|
||||
4. New Architecture only libs (Reanimated 3, Skia).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Enable New Architecture (RN 0.76+)
|
||||
```bash
|
||||
# Default ON in 0.76+; explicit:
|
||||
# ios/Podfile
|
||||
# RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
|
||||
|
||||
# android/gradle.properties
|
||||
newArchEnabled=true
|
||||
```
|
||||
|
||||
### Spec-driven Native Component (Codegen)
|
||||
```ts
|
||||
// MyViewNativeComponent.ts
|
||||
import type { ViewProps } from 'react-native';
|
||||
import type { Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
|
||||
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
||||
|
||||
export interface NativeProps extends ViewProps {
|
||||
color?: string;
|
||||
count?: WithDefault<Int32, 0>;
|
||||
}
|
||||
|
||||
export default codegenNativeComponent<NativeProps>('MyView');
|
||||
```
|
||||
|
||||
### TurboModule Spec
|
||||
```ts
|
||||
// NativeCalculator.ts
|
||||
import type { TurboModule } from 'react-native';
|
||||
import { TurboModuleRegistry } from 'react-native';
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
add(a: number, b: number): number; // SYNC call (impossible on old bridge)
|
||||
greet(name: string): Promise<string>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');
|
||||
```
|
||||
|
||||
### iOS TurboModule Implementation
|
||||
```objc
|
||||
// Calculator.mm
|
||||
#import "Calculator.h"
|
||||
|
||||
@implementation Calculator
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (NSNumber *)add:(double)a b:(double)b {
|
||||
return @(a + b);
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)
|
||||
getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<facebook::react::NativeCalculatorSpecJSI>(params);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
### JSI Direct Binding (advanced)
|
||||
```cpp
|
||||
// C++ side
|
||||
runtime.global().setProperty(
|
||||
runtime, "nativeAdd",
|
||||
Function::createFromHostFunction(runtime,
|
||||
PropNameID::forAscii(runtime, "nativeAdd"), 2,
|
||||
[](Runtime& rt, const Value&, const Value* args, size_t) {
|
||||
return Value(args[0].asNumber() + args[1].asNumber());
|
||||
}));
|
||||
```
|
||||
|
||||
### Concurrent Features on RN
|
||||
```jsx
|
||||
import { useTransition, Suspense } from 'react';
|
||||
|
||||
function App() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [query, setQuery] = useState('');
|
||||
return (
|
||||
<>
|
||||
<TextInput onChangeText={(t) => startTransition(() => setQuery(t))} />
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Results query={query} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New project (2026) | Fabric default (RN 0.76+) |
|
||||
| Legacy app | Migrate incrementally; interop layer |
|
||||
| Custom native view | Fabric component + codegen |
|
||||
| Sync native call | TurboModule (impossible old) |
|
||||
| Heavy animation | Reanimated 3 (Fabric-only) |
|
||||
|
||||
**기본값**: New Architecture ON, Hermes ON, codegen-driven specs.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React Native]] · [[React]]
|
||||
- 변형: [[TurboModules]] · [[Hermes]] · [[JSI]]
|
||||
- 응용: [[React Native Skia]]
|
||||
- Adjacent: [[Concurrent React]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: RN new architecture migration, TurboModule/Fabric component authoring, JSI binding.
|
||||
**언제 X**: pure JS-only RN questions (state management, navigation library).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Mixing old bridge modules with Fabric without interop**: runtime crash.
|
||||
- **Skipping codegen**: hand-written specs drift from native; codegen 의 source of truth.
|
||||
- **JSI HostObject 의 long-running work**: blocks JS thread; offload to native thread.
|
||||
- **Old `NativeModules.X` API in new code**: TurboModuleRegistry 의 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React Native official docs — New Architecture, RFC 0588 Fabric).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Fabric renderer / new arch full content |
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Fabric Renderer
|
||||
description: "Fabric Renderer는 React Native의 새로운 아키텍처(New Architecture)에 도입된 혁신적인 UI 렌더링 시스템으로, 기존의 UI 관리 레이어를 밑바닥부터 다시 작성한 결과물입니다 [1]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Fabric Renderer
|
||||
|
||||
## 📌 Brief Summary
|
||||
Fabric Renderer는 React Native의 새로운 아키텍처(New Architecture)에 도입된 혁신적인 UI 렌더링 시스템으로, 기존의 UI 관리 레이어를 밑바닥부터 다시 작성한 결과물입니다 [1]. 과거 자바스크립트 스레드와 네이티브 스레드 간의 비동기적 브릿지(Bridge) 통신이 유발하던 성능 병목 현상을 해결하기 위해 설계되었습니다 [2, 3]. 네이티브 UI 레이어에 대한 동기적(synchronous) 접근을 가능하게 하여, React 18의 동시성 렌더링(Concurrent rendering)을 지원하고 네이티브에 가까운 부드러운 앱 성능을 제공합니다 [1, 3].
|
||||
|
||||
## 📖 Core Content
|
||||
* **C++ 기반의 섀도우 트리(Shadow Tree) 생성:** 구형 아키텍처에서는 UI가 별도의 네이티브 스레드에서 관리되었으며 자바스크립트 스레드와의 통신이 비동기적으로 이루어졌습니다 [1]. 반면, Fabric은 UI의 가상 표현인 '섀도우 트리'를 C++에서 직접 생성하게 함으로써 스레드 간의 직접적이고 효율적인 데이터 공유를 가능하게 합니다 [1].
|
||||
* **동기적 레이아웃(Synchronous Layout) 연산:** Fabric 시스템은 비동기 왕복(round-trips) 없이 네이티브 뷰를 측정하고 렌더링할 수 있습니다 [3]. 네이티브 측에서 레이아웃을 계산한 뒤 동일한 렌더 사이클 내에 자바스크립트 스레드에 제공할 수 있게 되어, UI 요소가 한 프레임에 나타났다가 다음 프레임에 재배치되는 'UI 점프' 현상을 본질적으로 해결합니다 [1].
|
||||
* **동시성 렌더링(Concurrent Rendering) 완벽 지원:** Fabric은 React 18 및 그 이후 버전과 호환되며, 데이터 페칭을 위한 Suspense나 Transitions와 같은 동시성 기능을 지원합니다 [1]. 이를 통해 React는 렌더링의 우선순위를 조정하거나 사용자 입력에 즉각적으로 반응하기 위해 기존 렌더링 작업을 중단할 수 있어, 훨씬 더 유동적이고 반응성 높은 UI를 제공합니다 [1].
|
||||
* **성능 및 반응성 향상:** 렌더링 로직을 C++로 이동시키고 JSI를 통한 직접적인 통신을 활성화함으로써, UI 구성 요소를 렌더링하고 업데이트하는 데 걸리는 시간이 대폭 감소했습니다 [1]. 이는 복잡한 리스트 스크롤링, 네비게이션 전환 등에서 프레임 드랍 현상을 없애주며, 네이티브 컴포넌트들을 네이티브와 다름없는 성능으로 제어하고 애니메이션화할 수 있게 합니다 [3, 4].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **서드파티 라이브러리 호환성 및 과도기적 한계:** Fabric을 포함한 새로운 아키텍처는 아직 모든 신규 프로젝트에 기본값(default)으로 적용된 상태는 아니며, 선택적(opt-in)으로 사용할 수 있는 과도기적 단계에 있습니다 [5]. 따라서 초기 도입자(early adopters)에게는 훌륭한 성능을 제공하지만, 기존의 거대한 에코시스템 내 서드파티 라이브러리들이 새로운 아키텍처(Fabric 및 TurboModules)를 완벽히 지원하도록 채택(adoption) 및 업데이트될 때까지는 호환성 검토에 추가적인 노력이 필요할 수 있습니다 [3].
|
||||
* **플랫폼 종속적 애니메이션 한계:** Fabric이 네이티브 UI 레이어에 대한 렌더링 성능을 극대화했음에도 불구하고, React Native의 근본 특성상 실제 네이티브 플랫폼 컴포넌트(iOS의 UIKit, Android의 Views)를 그대로 사용합니다 [6]. 이는 진정한 네이티브 느낌을 주는 장점이 되지만, 극도로 복잡한 커스텀 애니메이션이나 비표준 파티클 효과 등을 구현할 때는 전체 렌더링 파이프라인을 독자적으로 통제하는 프레임워크(예: Flutter)에 비해 제약이 따르거나 플랫폼 간 미세한 편차가 발생할 수 있습니다 [6].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
id: wiki-2026-0508-figma-tokens-studio
|
||||
title: Figma Tokens Studio
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Tokens Studio, Figma Design Tokens Plugin]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [figma, design-tokens, design-system]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JSON
|
||||
framework: Figma Plugin
|
||||
---
|
||||
|
||||
# Figma Tokens Studio
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Tokens Studio = Figma 안의 design token source-of-truth + Style Dictionary 연동."**. 매 W3C Design Tokens Format Spec (DTCG, 2024-)에 매 align — 매 plugin이 매 token 의 JSON 의 Figma styles + variables 의 sync. 매 modern flow는 Tokens Studio → GitHub PR → Style Dictionary build → CSS/Tailwind/iOS/Android 의 multi-platform 출력.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 token types
|
||||
- color, dimension, fontFamily, fontWeight, lineHeight, letterSpacing, opacity, borderRadius, boxShadow, typography (composite).
|
||||
- 매 alias `{global.color.primary.500}` — 매 reference 의 통한 indirection.
|
||||
|
||||
### 매 sets / themes
|
||||
- **Set**: 매 token 의 그룹 (global, brand-A, light, dark).
|
||||
- **Theme**: 매 set 의 enabled combination (e.g., brand-A + light).
|
||||
- 매 multi-brand × multi-mode 의 매 표현.
|
||||
|
||||
### 매 응용
|
||||
1. Figma Variables sync — 매 plugin이 매 native variables 의 push.
|
||||
2. Code export — Style Dictionary → CSS custom props / Tailwind theme / iOS UIColor.
|
||||
3. Git sync — Tokens Studio Pro → GitHub repo, PR-based review.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. DTCG token JSON shape
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"primary": {
|
||||
"500": { "$value": "#3366FF", "$type": "color" }
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"md": { "$value": "16px", "$type": "dimension" }
|
||||
},
|
||||
"typography": {
|
||||
"heading-lg": {
|
||||
"$type": "typography",
|
||||
"$value": {
|
||||
"fontFamily": "{font.family.sans}",
|
||||
"fontSize": "{font.size.xl}",
|
||||
"fontWeight": 700,
|
||||
"lineHeight": 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Alias / reference
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"brand": { "$value": "#FF5500", "$type": "color" },
|
||||
"button": {
|
||||
"primary": { "$value": "{color.brand}", "$type": "color" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Style Dictionary config
|
||||
```javascript
|
||||
// sd.config.js
|
||||
import StyleDictionary from 'style-dictionary';
|
||||
|
||||
export default {
|
||||
source: ['tokens/**/*.json'],
|
||||
platforms: {
|
||||
css: {
|
||||
transformGroup: 'css',
|
||||
buildPath: 'build/css/',
|
||||
files: [{ destination: 'tokens.css', format: 'css/variables' }],
|
||||
},
|
||||
tailwind: {
|
||||
transformGroup: 'js',
|
||||
buildPath: 'build/tw/',
|
||||
files: [{ destination: 'theme.js', format: 'javascript/module' }],
|
||||
},
|
||||
ios: {
|
||||
transformGroup: 'ios-swift',
|
||||
buildPath: 'build/ios/',
|
||||
files: [{ destination: 'Tokens.swift', format: 'ios-swift/class.swift' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 4. CSS output
|
||||
```css
|
||||
:root {
|
||||
--color-primary-500: #3366ff;
|
||||
--spacing-md: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Tailwind preset from tokens
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
import tokens from './build/tw/theme.js';
|
||||
|
||||
export default {
|
||||
theme: {
|
||||
colors: tokens.color,
|
||||
spacing: tokens.spacing,
|
||||
fontFamily: { sans: tokens.font.family.sans },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Themes via attribute
|
||||
```css
|
||||
[data-theme='light'] { --color-bg: #ffffff; }
|
||||
[data-theme='dark'] { --color-bg: #0b0b10; }
|
||||
|
||||
body { background: var(--color-bg); }
|
||||
```
|
||||
|
||||
### 7. GitHub Action (build on PR merge)
|
||||
```yaml
|
||||
name: tokens-build
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['tokens/**']
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: npm ci && npm run tokens:build
|
||||
- run: |
|
||||
git config user.email bot@acme.dev
|
||||
git config user.name token-bot
|
||||
git add build && git commit -m "chore(tokens): build" || true
|
||||
git push
|
||||
```
|
||||
|
||||
### 8. Figma Variables JSON (native, post-2024)
|
||||
```javascript
|
||||
// figma plugin code (post-2024 Variables API)
|
||||
const collection = figma.variables.createVariableCollection('Brand');
|
||||
const v = figma.variables.createVariable('color.primary.500', collection.id, 'COLOR');
|
||||
v.setValueForMode(collection.modes[0].modeId, { r: 0.2, g: 0.4, b: 1, a: 1 });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 단일 brand | Tokens Studio Free + JSON export. |
|
||||
| Multi-brand / Pro | Tokens Studio Pro + GitHub sync. |
|
||||
| Native Figma only | Figma Variables direct. |
|
||||
| Multi-platform code | Style Dictionary build pipeline. |
|
||||
| 매 component spec | Tokens + auto-generated docs. |
|
||||
|
||||
**기본값**: Tokens Studio + DTCG JSON + Style Dictionary + Tailwind.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Design Tokens]] · [[Design System]]
|
||||
- 변형: [[Figma Variables]]
|
||||
- 응용: [[Style Dictionary Pipelines|Style Dictionary]] · [[Component Library]]
|
||||
- Adjacent: [[CSS_Architecture_and_Styling|CSS Variables]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 token JSON 의 scaffolding, Style Dictionary config, 매 alias 검증.
|
||||
**언제 X**: 매 visual review — Figma 안에서 매 designer 의 confirm.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Hard-coded hex in code**: 매 token bypass — drift 발생.
|
||||
- **Token name with semantic in global**: 매 `color.button` 의 global 안에 — 매 brand layer 분리.
|
||||
- **No alias**: 매 매 component-level token 의 raw value — refactor 의 어려움.
|
||||
- **Manual sync**: 매 designer가 매 figma update + dev가 매 code update — 매 GitHub action 자동화.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (tokens.studio docs 2024-2026, DTCG spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Tokens Studio + DTCG + Style Dictionary |
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Flutter
|
||||
description: "Flutter는 구글이 2017년에 출시한 오픈 소스 UI 소프트웨어 개발 키트로, 단일 코드베이스를 통해 모바일, 웹, 데스크톱 등 다양한 플랫폼의 애플리케이션을 구축할 수 있게 해주는 프레임워크이다 [1, 2]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Flutter
|
||||
|
||||
## 📌 Brief Summary
|
||||
Flutter는 구글이 2017년에 출시한 오픈 소스 UI 소프트웨어 개발 키트로, 단일 코드베이스를 통해 모바일, 웹, 데스크톱 등 다양한 플랫폼의 애플리케이션을 구축할 수 있게 해주는 프레임워크이다 [1, 2]. Dart 프로그래밍 언어를 기반으로 하며, 호스트 운영체제의 네이티브 UI 컴포넌트를 호출하는 대신 프레임워크 자체 렌더링 엔진(Skia 또는 Impeller)을 사용하여 화면의 모든 픽셀을 직접 그리는 방식을 취한다 [3-6]. 이를 통해 개발자는 플랫폼에 구애받지 않고 '픽셀 퍼펙트(Pixel-perfect)'한 일관된 사용자 인터페이스와 높은 성능을 제공할 수 있다 [4, 7].
|
||||
|
||||
## 📖 Core Content
|
||||
* **자체 렌더링 엔진과 아키텍처 혁신**: Flutter는 모든 UI를 위젯(Widget)으로 구성하며, 네이티브 구성 요소를 사용하지 않고 자체 그래픽 엔진을 통해 UI를 렌더링한다 [4, 5, 8]. 기존 Skia 엔진의 한계였던 셰이더 컴파일 지연(Jank) 문제를 해결하기 위해 **Impeller 렌더링 엔진**을 도입하였으며, 빌드 시점에 셰이더를 미리 컴파일함으로써 일관된 60fps 이상의 부드러운 성능을 보장한다 [9-11].
|
||||
* **Dart 언어와 컴파일 최적화**: Flutter는 Dart 언어를 사용하여 JIT(Just-in-Time) 및 AOT(Ahead-of-Time) 컴파일을 모두 지원한다 [5]. 개발 중에는 JIT를 통한 **'핫 리로드(Hot Reload)'**로 상태를 유지하면서 1초 이내에 빠른 피드백을 제공하고, 프로덕션에서는 AOT를 통해 네이티브 ARM 코드로 컴파일되어 빠른 시작 시간과 고성능을 자랑한다 [5, 12-14]. Dart 3의 도입으로 사운드 널 안정성(Sound null safety), 패턴 매칭, 레코드 등의 기능이 추가되어 코드베이스가 더욱 견고해졌다 [11].
|
||||
* **실전 상태 관리 패턴**: 규모에 따라 다양한 아키텍처 패턴이 적용된다. 대규모 엔터프라이즈 프로젝트에서는 엄격한 관심사 분리를 요구하는 스트림(Stream) 기반의 이벤트 중심 상태 관리 방식인 **'BLoC (Business Logic Component)'** 패턴이 테스트 용이성 덕분에 선호된다 [15]. 중소규모 프로젝트에서는 유연한 의존성 주입을 돕는 **'Provider'**나, 이를 개선하여 MVVM 아키텍처와의 결합도가 높은 현대적 반응형 패턴인 **'Riverpod'**이 폭넓게 활용된다 [15].
|
||||
* **AI 및 머신러닝 생태계 통합**: 구글의 에코시스템과 긴밀하게 통합되어 있어, Firebase AI Logic을 통한 Gemini 모델 접근이나 기기 내 직접 추론을 위한 TensorFlow Lite를 Flutter 애플리케이션에 매끄럽게 적용할 수 있다 [16, 17].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **플랫폼 네이티브 컴포넌트의 부재**: Flutter는 실제 플랫폼 네이티브 UI 요소(예: iOS의 UIKit)를 사용하지 않고 이를 시각적으로 모방한다 [4, 18]. 이로 인해 새로운 OS 버전에서 UI 패러다임이 바뀔 경우 커뮤니티가 이를 복제할 때까지 기다려야 하며, 스크롤 물리 효과나 접근성(Accessibility) 의미 체계 등에서 네이티브 표준과 미묘한 차이가 발생할 수 있다 [4, 18-20].
|
||||
* **앱 용량 증가**: Dart 런타임과 그래픽 렌더링 엔진(Impeller 등)을 애플리케이션 내에 자체적으로 포함하여 배포해야 하므로, React Native에 비해 기본적인 앱의 파일 크기(최소 8~12MB 수준)가 상대적으로 더 크며 기기 메모리 점유율도 약간 더 높다 [13, 21].
|
||||
* **학습 곡선과 인재 확보의 한계**: 세계적으로 널리 쓰이는 JavaScript/TypeScript를 사용하는 React Native와 달리, 상대적으로 생태계가 작은 Dart 언어를 별도로 학습해야 한다 [22-24]. 이로 인해 전문 개발자 풀이 좁아 팀을 확장하거나 인재를 채용하는 데 더 많은 시간과 비용(프리미엄 인건비, 교육 비용 등)이 소요될 수 있다 [22, 24, 25].
|
||||
* **서드파티 라이브러리 및 3D 제약**: 모바일 생태계에서 특정 네이티브 모듈이나 서드파티 라이브러리 지원이 JavaScript 생태계보다 성숙하지 않을 수 있어 일부 기능을 처음부터 직접 개발해야 할 수 있다 [26]. 또한 고도의 3D 애니메이션이나 컴포넌트 렌더링에 있어서는 React Native나 완전한 네이티브 환경에 비해 지원이 부족하다 [27].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Flutter AOT Compilation (Dart)
|
||||
description: "Flutter의 AOT(Ahead-of-Time) 컴파일은 Dart 프로그래밍 언어로 작성된 애플리케이션 코드를 실행 이전에 네이티브 ARM 코드(기계어)로 사전 컴파일하는 기술이다 [1-3]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Flutter AOT Compilation (Dart)
|
||||
|
||||
## 📌 Brief Summary
|
||||
Flutter의 AOT(Ahead-of-Time) 컴파일은 Dart 프로그래밍 언어로 작성된 애플리케이션 코드를 실행 이전에 네이티브 ARM 코드(기계어)로 사전 컴파일하는 기술이다 [1-3]. 이는 실행 시점의 오버헤드를 줄여 애플리케이션의 빠른 초기 시작(Cold start) 속도를 달성하고 높은 런타임 성능을 보장하는 Flutter 아키텍처의 핵심 메커니즘이다 [2, 4].
|
||||
|
||||
## 📖 Core Content
|
||||
* **네이티브 기계어로의 직접 컴파일**: Flutter는 프로덕션 배포 시 Dart AOT 컴파일러를 활용하여 코드를 Android 및 iOS 모바일 기기를 위한 네이티브 기계어(ARM 코드)로 직접 변환한다 [1-3, 5]. 실행 시점에 파싱되거나 해석되어야 하는 자바스크립트 기반 프레임워크와 달리, 기계어 형태로 컴파일되므로 향상된 퍼포먼스를 제공한다 [4].
|
||||
* **빠른 콜드 스타트(Cold Start) 속도**: 코드가 미리 컴파일되어 있기 때문에 애플리케이션 구동 시 자바스크립트 엔진(예: Hermes) 초기화 및 번들 파싱 과정을 거칠 필요가 없다 [2]. 이러한 이점 덕분에 Flutter 애플리케이션은 일반적으로 더 빠른 초기 시작 속도를 자랑한다 [2].
|
||||
* **실행 효율 극대화**: Flutter는 플랫폼 네이티브 컴포넌트에 의존하는 대신 자체 그래픽 렌더링 엔진(Skia 또는 Impeller)을 사용하며, Dart 언어의 AOT 컴파일 기능을 결합하여 애니메이션이나 복잡한 렌더링 작업 시 프레임워크의 실행 효율성을 극대화한다 [5, 6].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **애플리케이션 용량(App Size) 증가**: AOT 컴파일된 코드와 함께 Dart 런타임 및 자체 렌더링 엔진(Impeller 등)이 앱 번들에 포함되어 배포되어야 한다 [2]. 이로 인해 운영체제의 네이티브 UI 컴포넌트를 렌더링하는 React Native(최소 5~8MB) 대비 Flutter 앱의 최소 초기 파일 크기(최소 8~12MB)가 상대적으로 더 크며, 이는 기기의 메모리 성능에 부담을 줄 수 있다 [2, 7].
|
||||
* **Dart 언어 종속성과 인력 확보 한계**: AOT 컴파일의 이점을 누리기 위해선 반드시 Dart 언어를 학습해야 한다 [7, 8]. 범용성이 높은 JavaScript에 비해 개발자 풀과 생태계가 좁아 프로젝트 초기 인력 확보와 학습 곡선(Learning Curve) 극복에 비용과 시간이 더 들 수 있다 [7-9].
|
||||
* **아키텍처 지원 제약**: Flutter는 64비트 ARM 아키텍처 지원에 일부 한계가 있어(가장 근접하게 지원되는 구조가 ARM64-v8A) 일부 특정 모바일 디바이스 환경에서의 채택을 제한할 수 있다는 단점이 있다 [10].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Flutter Impeller
|
||||
description: "Flutter Impeller는 기존 Skia 엔진을 대체하기 위해 개발된 Flutter의 최신 그래픽 렌더링 엔진입니다."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Flutter Impeller
|
||||
|
||||
## 📌 Brief Summary
|
||||
Flutter Impeller는 기존 Skia 엔진을 대체하기 위해 개발된 Flutter의 최신 그래픽 렌더링 엔진입니다. 애플리케이션 빌드 시점에 셰이더(Shader)를 미리 컴파일하여 런타임에 발생하는 프레임 드랍(Jank) 현상을 근본적으로 해결할 목적으로 설계되었습니다. 2023년(Flutter 3.10)부터 iOS에서 기본 렌더러로 안정화되었으며, Android를 비롯한 다른 플랫폼으로도 지원이 확대되고 있습니다 [1, 2].
|
||||
|
||||
## 📖 Core Content
|
||||
* **셰이더 컴파일 쟁크(Jank) 해결**: 기존 구조의 가장 큰 성능 문제였던 '셰이더 컴파일 쟁크'는 사용자가 새로운 시각 효과를 처음 마주할 때 셰이더를 실시간으로 컴파일하면서 발생하던 프레임 드랍 현상이었습니다 [1, 2]. Impeller는 앱 빌드 과정에서 더 작고 최적화된 셰이더 세트를 미리 컴파일(Pre-compile)해두는 방식으로 이 문제를 해결하여 첫 프레임부터 일관되고 부드러운 렌더링을 제공합니다 [3, 4].
|
||||
* **현대적 모바일 GPU 최적화**: Impeller는 테셀레이션(Tessellation) 기반의 렌더링 방식을 채택하여 최신 모바일 GPU 환경에 고도로 최적화되어 있습니다 [2]. 이를 통해 화면의 모든 픽셀을 직접 그리는(Custom Rendering) Flutter의 철학을 유지하면서도 복잡한 그래픽을 빠르고 예측 가능하게 처리합니다 [2, 5].
|
||||
* **성능 일관성**: Impeller의 도입으로 복잡한 애니메이션과 전환 효과가 사용되는 환경에서도 고주사율 기기에서 120fps 또는 일관된 60fps의 렌더링 성능을 보장받을 수 있게 되었습니다 [3, 6, 7].
|
||||
* **플랫폼 적용 현황**: iOS 환경에서는 완전히 안정화되어 프로덕션 레벨의 기본 그래픽 백엔드로 자리 잡았으며, Android에서는 프리뷰(Preview) 형태로 제공되며 버전이 업데이트될 때마다 빠르게 발전하고 있습니다 [2, 8].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **앱 용량(App Size) 증가**: 앱 패키징 시 Dart 런타임과 함께 Impeller 렌더링 엔진 자체가 포함되어야 합니다. 이로 인해 앱의 최소 크기(APK 기준)가 8~12MB 수준으로 구성되며, 시스템의 네이티브 렌더러를 활용하는 React Native(약 5~8MB)에 비해 베이스라인 용량이 상대적으로 커진다는 제약이 있습니다 [7].
|
||||
* **메모리 오버헤드**: 운영체제의 네이티브 UI 컴포넌트를 차용하지 않고, Impeller/Skia 엔진을 이용해 자체적인 위젯 트리와 렌더링 파이프라인을 독자적으로 유지하며 화면을 그립니다 [9, 10]. 이로 인해 네이티브 UI를 사용하는 방식 대비 애플리케이션의 기본 메모리 사용량이 다소 높게 발생합니다 [9].
|
||||
* **플랫폼별 성숙도 편차**: iOS에서는 Impeller가 기본값으로 완전히 성숙하여 셰이더 쟁크 문제를 해결했지만, Android 생태계에서는 아직 성숙해 가는 단계(Preview)이므로 플랫폼 간 그래픽 백엔드 안정성 및 최적화 수준에 일시적인 편차가 존재합니다 [2, 11].
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Flutter Web / Desktop
|
||||
description: "Flutter Web / Desktop은 단일 코드베이스를 사용하여 모바일뿐만 아니라 웹과 데스크톱(Windows, macOS, Linux) 환경을 위한 애플리케이션을 구축할 수 있게 해주는 크로스 플랫폼 프레임워크 기능이다 [1, 2]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Flutter Web / Desktop
|
||||
|
||||
## 📌 Brief Summary
|
||||
Flutter Web / Desktop은 단일 코드베이스를 사용하여 모바일뿐만 아니라 웹과 데스크톱(Windows, macOS, Linux) 환경을 위한 애플리케이션을 구축할 수 있게 해주는 크로스 플랫폼 프레임워크 기능이다 [1, 2]. 최근 WebAssembly(WasmGC) 기술 채택과 데스크톱 지원의 안정화를 거치며 성능과 범용성이 크게 향상되었다 [1]. 모바일 단말기를 넘어 모든 사용자 접점(Touchpoints)에서 일관된 사용자 경험을 제공해야 하는 비즈니스에 특히 성숙한 솔루션으로 평가받고 있다 [1, 3].
|
||||
|
||||
## 📖 Core Content
|
||||
* **Flutter Web의 성능 비약적 발전**: WebAssembly(WasmGC)가 주류 기술로 채택됨에 따라 자바스크립트 대신 Wasm으로 코드를 컴파일할 수 있게 되었다 [1]. 이를 통해 로딩 시간이 눈에 띄게 단축되었고 번들 크기가 감소했으며, 복잡한 UI와 애니메이션에서도 네이티브에 가까운 성능을 제공하여 정교한 웹 애플리케이션 개발에 강력한 경쟁력을 갖추게 되었다 [1].
|
||||
* **안정화된 데스크톱 아키텍처 지원**: Windows, macOS, Linux 운영체제에 대한 데스크톱 지원이 매우 안정적이고 견고해졌다 [1, 2]. 개발자는 데스크톱 환경을 위한 사전 제작된 플러그인을 설치하거나 직접 생성할 수 있으며, 기존 소스 코드를 컴파일하여 기본 데스크톱 앱을 만들어 낼 수 있다 [2].
|
||||
* **프로덕션 수준의 다중 플랫폼 타겟팅**: Flutter는 모바일 플랫폼 이외에도 웹과 데스크톱 타겟을 단일 코드베이스로 동시에 지원할 수 있어 뛰어난 다중 플랫폼(Multi-platform) 커버리지를 제공한다 [3, 4]. 이러한 웹과 데스크톱 타겟 기능은 많은 애플리케이션 유형에서 즉시 상용화가 가능한(production-ready) 수준이며, React Native의 데스크톱/웹 지원에 비해 한층 더 성숙한 것으로 평가된다 [3-5].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **플랫폼 표준 및 관례 불일치 위험**: Flutter는 호스트 플랫폼의 네이티브 UI 구성요소를 사용하지 않고 자체 엔진을 사용해 모든 픽셀을 렌더링하므로, 모든 환경에서 동일한 시각적 결과를 보장하지만 각 웹 및 데스크톱 플랫폼의 기본 동작 관례를 개발자가 직접 맞춰야 하는 부담이 있다 [6]. 특히 스크롤 물리 엔진이나 텍스트 선택 동작, 접근성 의미론(Accessibility semantics)이 해당 플랫폼의 고유 표준과 미세하게 빗나갈 가능성이 존재한다 [6].
|
||||
* **'모방된(Simulated)' 네이티브 경험**: 각 플랫폼(웹, Windows, macOS 등) 고유의 UI 동작을 그대로 상속받지 못하고 Material 또는 Cupertino 위젯을 통해 이를 흉내(Simulated)내야 하므로, 해당 운영체제의 완벽한 네이티브 룩앤필(Native feel)이 필수적인 프로젝트에서는 적합하지 않을 수 있다 [5, 6].
|
||||
* **메모리 및 용량 오버헤드**: Flutter 앱은 네이티브 컴포넌트를 호출하는 방식이 아니라 자체적인 위젯 트리와 렌더링 엔진, Dart 런타임을 포함한 채 배포되어야 하므로 근본적으로 앱 크기가 더 무겁고 메모리 오버헤드가 더 높게 발생할 수 있다 [7, 8].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
id: wiki-2026-0508-folder-structure-best-practices
|
||||
title: Folder Structure Best Practices
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Project Structure, Code Organization, Frontend Folder Layout]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [architecture, folder-structure, frontend, project-organization]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-next
|
||||
---
|
||||
|
||||
# Folder Structure Best Practices
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 folder structure 의 골 — colocation by feature, not by file-type"**. 매 modern frontend (2026) 의 favored: feature-sliced design, screaming architecture, Next.js app router colocation. 매 type-based folders (`components/`, `utils/`) 의 rejection — feature 의 boundary 의 unclear.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Type-based (anti) vs Feature-based (good)
|
||||
- **Type-based**: `/components`, `/hooks`, `/utils`, `/services`, `/types` — every change touches 5 folders.
|
||||
- **Feature-based**: `/features/checkout/{components,hooks,api,types}` — change scope = single folder.
|
||||
|
||||
### 매 Levels of Hierarchy
|
||||
1. **Routes / pages** (Next.js `app/`, Remix `routes/`).
|
||||
2. **Features** (`features/<feature>/...`) — domain boundaries.
|
||||
3. **Shared** (`lib/`, `shared/`, `ui/`) — cross-feature reusables.
|
||||
4. **Entities/domain** (Feature-Sliced: `entities/user`, `entities/cart`).
|
||||
|
||||
### 매 Boundaries
|
||||
- Features 의 each other 의 import X (cross-feature 의 shared layer 의 통과).
|
||||
- Lower layers 의 higher 의 import X (UI 의 feature 의 import X).
|
||||
- 매 ESLint `no-restricted-imports` 또는 `eslint-plugin-boundaries` 의 enforce.
|
||||
|
||||
### 매 응용
|
||||
1. Next.js 15 app router project.
|
||||
2. Vite + React SPA.
|
||||
3. Monorepo (Turborepo, Nx).
|
||||
4. React Native app.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Feature-Sliced (recommended 2026)
|
||||
```
|
||||
src/
|
||||
├── app/ # app entry, providers, global styles
|
||||
├── pages/ # (or routes/) page-level composition
|
||||
├── features/ # business actions (toggle-cart, send-comment)
|
||||
│ └── send-comment/
|
||||
│ ├── ui/
|
||||
│ ├── model/ # state, hooks
|
||||
│ ├── api/
|
||||
│ └── index.ts # public API
|
||||
├── entities/ # business entities (user, post, cart)
|
||||
│ └── user/
|
||||
│ ├── ui/
|
||||
│ ├── model/
|
||||
│ └── api/
|
||||
├── shared/ # reusable, domain-agnostic
|
||||
│ ├── ui/ # buttons, inputs (design system)
|
||||
│ ├── lib/ # utilities
|
||||
│ ├── api/ # base http client
|
||||
│ └── config/
|
||||
└── widgets/ # composite UI blocks (Header, Sidebar)
|
||||
```
|
||||
|
||||
### Next.js 15 App Router (colocation)
|
||||
```
|
||||
app/
|
||||
├── layout.tsx
|
||||
├── page.tsx
|
||||
├── (marketing)/ # route group, no URL segment
|
||||
│ └── about/page.tsx
|
||||
├── dashboard/
|
||||
│ ├── layout.tsx
|
||||
│ ├── page.tsx
|
||||
│ ├── _components/ # private (underscore = not routable)
|
||||
│ │ └── Sidebar.tsx
|
||||
│ ├── settings/
|
||||
│ │ ├── page.tsx
|
||||
│ │ └── actions.ts # server actions
|
||||
│ └── api/
|
||||
│ └── route.ts
|
||||
src/
|
||||
├── lib/ # shared utilities, db, auth
|
||||
├── components/ui/ # design system
|
||||
└── features/ # cross-page features
|
||||
```
|
||||
|
||||
### Vite + React SPA (mid-size)
|
||||
```
|
||||
src/
|
||||
├── main.tsx
|
||||
├── App.tsx
|
||||
├── routes/ # react-router routes
|
||||
├── features/
|
||||
│ └── auth/
|
||||
│ ├── components/
|
||||
│ ├── hooks/
|
||||
│ ├── api.ts
|
||||
│ ├── types.ts
|
||||
│ └── index.ts
|
||||
├── components/ui/ # design system
|
||||
├── hooks/ # generic hooks
|
||||
├── lib/
|
||||
└── types/
|
||||
```
|
||||
|
||||
### Monorepo (Turborepo)
|
||||
```
|
||||
apps/
|
||||
├── web/ # Next.js app
|
||||
├── mobile/ # Expo
|
||||
└── docs/
|
||||
packages/
|
||||
├── ui/ # shared design system
|
||||
├── config/ # eslint, ts, tailwind presets
|
||||
├── api/ # tRPC / generated client
|
||||
└── db/ # Prisma schema
|
||||
```
|
||||
|
||||
### Feature Public API (barrel)
|
||||
```ts
|
||||
// features/checkout/index.ts — explicit exports only
|
||||
export { CheckoutPage } from './ui/CheckoutPage';
|
||||
export { useCheckout } from './model/useCheckout';
|
||||
export type { CheckoutState } from './model/types';
|
||||
// internals (api/, model/internal) NOT re-exported
|
||||
```
|
||||
|
||||
### Boundary Enforcement (eslint-plugin-boundaries)
|
||||
```js
|
||||
// eslint.config.js
|
||||
import boundaries from 'eslint-plugin-boundaries';
|
||||
|
||||
export default [{
|
||||
plugins: { boundaries },
|
||||
settings: {
|
||||
'boundaries/elements': [
|
||||
{ type: 'shared', pattern: 'src/shared/*' },
|
||||
{ type: 'entity', pattern: 'src/entities/*' },
|
||||
{ type: 'feature', pattern: 'src/features/*' },
|
||||
{ type: 'widget', pattern: 'src/widgets/*' },
|
||||
{ type: 'page', pattern: 'src/pages/*' },
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
'boundaries/element-types': ['error', {
|
||||
default: 'disallow',
|
||||
rules: [
|
||||
{ from: 'page', allow: ['widget', 'feature', 'entity', 'shared'] },
|
||||
{ from: 'widget', allow: ['feature', 'entity', 'shared'] },
|
||||
{ from: 'feature', allow: ['entity', 'shared'] },
|
||||
{ from: 'entity', allow: ['shared'] },
|
||||
{ from: 'shared', allow: ['shared'] },
|
||||
],
|
||||
}],
|
||||
},
|
||||
}];
|
||||
```
|
||||
|
||||
### TS Path Aliases
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@shared/*": ["src/shared/*"],
|
||||
"@features/*": ["src/features/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Small SPA (<20 components) | Flat or simple `components/` + `pages/` |
|
||||
| Mid-size (20-200) | Feature-based (`features/` + `shared/`) |
|
||||
| Large / team scale | Feature-Sliced Design + boundary lint |
|
||||
| Next.js project | App router colocation + `src/features/` |
|
||||
| Monorepo | Turborepo `apps/` + `packages/` |
|
||||
|
||||
**기본값**: feature-based + shared layer + ESLint boundary enforcement.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Software Architecture]] · [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- 변형: [[Feature-Sliced Design]] · [[Atomic Design]]
|
||||
- 응용: [[Monorepo]] · [[Design System]]
|
||||
- Adjacent: [[Next.js]] · [[Turborepo]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: project structure 의 design, refactor folder layout, boundary rule 의 setup.
|
||||
**언제 X**: tiny prototype — overhead 의 not worth.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Type-based root** (`/components`, `/hooks`, `/utils`): scales poorly.
|
||||
- **Deeply nested barrels**: circular import / slow build.
|
||||
- **God `utils/`**: dump folder. 매 specific domain 의 split.
|
||||
- **Cross-feature imports**: feature isolation 의 break — shared 의 use.
|
||||
- **`index.ts` 의 every file의 leak**: tree-shaking break, public API 의 unclear.
|
||||
- **`pages/` 안에 business logic**: route 는 thin, feature 의 delegate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Feature-Sliced Design docs, Next.js project structure docs, Bulletproof React).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — folder structure best practices full content |
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
---
|
||||
id: wiki-2026-0508-frontend-architecture-and-folder
|
||||
title: Frontend Architecture and Folder Structure
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Project Structure, Folder Convention, Frontend Layout]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, architecture, project-structure, conventions]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React
|
||||
---
|
||||
|
||||
# Frontend Architecture and Folder Structure
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 file layout 의 의 architecture 의 — folder 의 dependency direction 의 enforce"**. 2026 의 의 매 dominant convention 의 **Feature-Sliced Design (FSD)** + **colocation by route** (Next.js App Router). 매 monorepo 의 의 매 cross-package boundary 의 의 nx/turbo 의 의 enforce.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 layered approach (FSD)
|
||||
- **app**: 의 entrypoint, provider, router setup.
|
||||
- **pages / routes**: 의 page-level composition.
|
||||
- **widgets**: 의 self-contained UI block (Header, Sidebar).
|
||||
- **features**: 의 user action (LoginForm, AddToCart).
|
||||
- **entities**: 의 domain object (User, Product).
|
||||
- **shared**: 의 reusable utility (UI kit, lib, config).
|
||||
|
||||
매 dependency 의 down-only — 의 widget 의 entity/shared 의 import 의 OK, 의 reverse 의 X.
|
||||
|
||||
### 매 colocation 의 rule
|
||||
- 의 component 의 의 close 의 의 use site.
|
||||
- 의 test, story, type 의 의 same folder.
|
||||
- Route folder 의 의 page-only 의 keep — 의 reusable 의 의 lift up.
|
||||
|
||||
### 매 응용
|
||||
1. Solo project 의 의 minimal (`src/components`, `src/pages`).
|
||||
2. Team project 의 의 FSD layered.
|
||||
3. Monorepo 의 의 nx/turbo 의 의 boundary enforce.
|
||||
4. Design system 의 의 separate package.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### FSD layout
|
||||
```
|
||||
src/
|
||||
app/
|
||||
providers/
|
||||
routes/
|
||||
main.tsx
|
||||
pages/
|
||||
home/
|
||||
profile/
|
||||
widgets/
|
||||
header/
|
||||
sidebar/
|
||||
features/
|
||||
auth-login/
|
||||
ui/
|
||||
model/
|
||||
api/
|
||||
index.ts
|
||||
entities/
|
||||
user/
|
||||
ui/
|
||||
model/
|
||||
api/
|
||||
shared/
|
||||
ui/
|
||||
lib/
|
||||
config/
|
||||
api/
|
||||
```
|
||||
|
||||
### Next.js App Router 의 colocation
|
||||
```
|
||||
app/
|
||||
layout.tsx
|
||||
page.tsx
|
||||
(marketing)/ # route group
|
||||
pricing/
|
||||
page.tsx
|
||||
_components/
|
||||
PricingCard.tsx
|
||||
dashboard/
|
||||
layout.tsx
|
||||
page.tsx
|
||||
[id]/
|
||||
page.tsx
|
||||
loading.tsx
|
||||
error.tsx
|
||||
_lib/
|
||||
fetch-data.ts
|
||||
components/ # shared, lifted
|
||||
ui/
|
||||
lib/
|
||||
utils.ts
|
||||
```
|
||||
|
||||
### Public API per slice (`index.ts`)
|
||||
```ts
|
||||
// features/auth-login/index.ts
|
||||
export { LoginForm } from "./ui/LoginForm";
|
||||
export { useLogin } from "./model/use-login";
|
||||
// 매 internal helper 의 의 of export 의 X — barrier.
|
||||
```
|
||||
|
||||
```ts
|
||||
// widgets/header/Header.tsx
|
||||
import { LoginForm } from "@/features/auth-login"; // OK — public API
|
||||
// import { internalHelper } from "@/features/auth-login/lib/internal"; // X
|
||||
```
|
||||
|
||||
### TS path alias
|
||||
```jsonc
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@app/*": ["src/app/*"],
|
||||
"@pages/*": ["src/pages/*"],
|
||||
"@widgets/*": ["src/widgets/*"],
|
||||
"@features/*": ["src/features/*"],
|
||||
"@entities/*": ["src/entities/*"],
|
||||
"@shared/*": ["src/shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint boundaries
|
||||
```js
|
||||
// eslint.config.js
|
||||
import boundaries from "eslint-plugin-boundaries";
|
||||
|
||||
export default [{
|
||||
plugins: { boundaries },
|
||||
settings: {
|
||||
"boundaries/elements": [
|
||||
{ type: "app", pattern: "src/app/*" },
|
||||
{ type: "pages", pattern: "src/pages/*" },
|
||||
{ type: "widgets", pattern: "src/widgets/*" },
|
||||
{ type: "features", pattern: "src/features/*" },
|
||||
{ type: "entities", pattern: "src/entities/*" },
|
||||
{ type: "shared", pattern: "src/shared/*" },
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
"boundaries/element-types": ["error", {
|
||||
default: "disallow",
|
||||
rules: [
|
||||
{ from: "app", allow: ["pages", "widgets", "features", "entities", "shared"] },
|
||||
{ from: "pages", allow: ["widgets", "features", "entities", "shared"] },
|
||||
{ from: "widgets", allow: ["features", "entities", "shared"] },
|
||||
{ from: "features", allow: ["entities", "shared"] },
|
||||
{ from: "entities", allow: ["shared"] },
|
||||
{ from: "shared", allow: ["shared"] },
|
||||
],
|
||||
}],
|
||||
},
|
||||
}];
|
||||
```
|
||||
|
||||
### Slice 의 internal layer
|
||||
```
|
||||
features/auth-login/
|
||||
ui/ # React component
|
||||
LoginForm.tsx
|
||||
LoginForm.test.tsx
|
||||
LoginForm.stories.tsx
|
||||
model/ # state, hooks, business logic
|
||||
use-login.ts
|
||||
login-store.ts
|
||||
api/ # network call
|
||||
login-request.ts
|
||||
lib/ # helpers (private)
|
||||
validate-credentials.ts
|
||||
index.ts # public API
|
||||
```
|
||||
|
||||
### Monorepo (turbo)
|
||||
```
|
||||
apps/
|
||||
web/ # Next.js app
|
||||
mobile/ # React Native
|
||||
packages/
|
||||
ui/ # design system
|
||||
api-client/ # tRPC/REST client
|
||||
config-eslint/
|
||||
config-tsconfig/
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] },
|
||||
"lint": {},
|
||||
"test": { "dependsOn": ["^build"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Barrel file 의 caution
|
||||
```ts
|
||||
// 매 export * 의 의 tree-shaking 의 hurt 의 가능
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
|
||||
// 매 explicit re-export 의 더 의 safe
|
||||
export { Button } from "./Button";
|
||||
export { Card } from "./Card";
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Solo / prototype | `src/{components,pages,lib}` flat |
|
||||
| Team < 5, single app | FSD-lite (skip `entities`, merge `widgets`) |
|
||||
| Team > 5 | Full FSD + ESLint boundaries |
|
||||
| Multi-app | Monorepo (turbo / nx) + shared `packages/ui` |
|
||||
| Next.js App Router | Route colocation + `_private` folders |
|
||||
|
||||
**기본값**: FSD-lite (app/pages/features/shared) + path alias + ESLint boundaries.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Software Architecture]] · [[Project Structure]]
|
||||
- 변형: [[Feature-Sliced Design]] · [[Atomic Design]] · [[Clean Architecture]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: new project 의 scaffold, legacy reorg, monorepo migration, dependency direction 의 enforce.
|
||||
**언제 X**: 의 매 < 50 file project — flat structure 의 충분.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`src/components/` god folder**: 매 1000+ file 의 의 unscalable.
|
||||
- **Cross-feature import**: feature-A 의 feature-B 의 internal 의 reach — 의 shared/entities 의 의 lift up.
|
||||
- **Deep nesting** (`a/b/c/d/e/f/`): 매 navigate 의 hard — 매 max 4-level 의 keep.
|
||||
- **Barrel `export *`**: 매 tree-shake 의 의 break, 매 circular 의 의 hide.
|
||||
- **No boundary enforcement**: convention 의 의 의 only — 의 violation 의 의 silent 의 accumulate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Feature-Sliced Design docs, Next.js App Router, Vercel monorepo guide, nx docs, eslint-plugin-boundaries).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — FSD layout + ESLint boundaries + monorepo 추가 |
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
id: wiki-2026-0508-frontend
|
||||
title: Frontend
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Web Frontend, Client-side Engineering, UI Engineering]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, web, react, performance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# Frontend
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 user 의 매 device 에서 매 실행되는 매 모든 것 — 매 markup, 매 style, 매 behavior, 매 network."**. 2026 의 매 frontend 는 매 React 19 + 매 RSC + 매 Vite/Turbopack + 매 Bun/Node + 매 Edge runtime 의 매 stack 에서 매 진화 중. Core Web Vitals + 매 a11y + 매 i18n 이 매 hygiene.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Layer
|
||||
- **Markup**: HTML, semantic, a11y.
|
||||
- **Style**: CSS — Cascade Layers, Container Queries, `:has()`, OKLCH.
|
||||
- **Behavior**: JS/TS — framework (React/Vue/Svelte/Solid).
|
||||
- **Build**: Vite, Turbopack, Bun, esbuild, swc.
|
||||
- **Runtime**: Browser, Edge (Cloudflare Workers, Vercel), Node, Bun.
|
||||
|
||||
### 매 Framework landscape (2026)
|
||||
- **React 19**: RSC + Actions + use() hook + Compiler.
|
||||
- **Next.js 15**: App Router default, Turbopack stable.
|
||||
- **Astro 5**: content site / island.
|
||||
- **SvelteKit 2** / **Vue 3.5 + Nuxt 4** / **Solid 2**.
|
||||
- **Remix → React Router 7**: data loading first.
|
||||
|
||||
### 매 Performance pillars
|
||||
- **LCP < 2.5s**: image optimize, preconnect, fetchpriority.
|
||||
- **INP < 200ms**: long task chunking, scheduler.yield.
|
||||
- **CLS < 0.1**: dimension reserved, font-display swap.
|
||||
- **Bundle**: route split, tree-shake, dynamic import.
|
||||
|
||||
### 매 Modern primitives
|
||||
- **CSS**: container queries, `:has()`, view transitions API, anchor positioning.
|
||||
- **JS**: scheduler.yield, AbortSignal.timeout, Temporal (stage 4 2026).
|
||||
- **HTML**: popover, dialog, search.
|
||||
- **Network**: HTTP/3, Early Hints, Speculation Rules.
|
||||
|
||||
### 매 응용
|
||||
1. SSR + RSC 로 매 SEO 기반 commerce.
|
||||
2. Edge personalization — 매 cookie / geo 기반.
|
||||
3. PWA + offline-first.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### View Transitions API (cross-document)
|
||||
```html
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
<style>
|
||||
::view-transition-old(hero) { animation: fade-out 0.3s; }
|
||||
::view-transition-new(hero) { animation: fade-in 0.3s; }
|
||||
.hero { view-transition-name: hero; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Container query
|
||||
```css
|
||||
.card { container-type: inline-size; }
|
||||
@container (min-width: 400px) {
|
||||
.card-body { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
```
|
||||
|
||||
### React 19 Actions (form)
|
||||
```tsx
|
||||
'use client';
|
||||
async function submit(formData: FormData) {
|
||||
'use server';
|
||||
await db.insert(formData.get('msg') as string);
|
||||
}
|
||||
export default function Form() {
|
||||
return <form action={submit}><input name="msg" /><button>Send</button></form>;
|
||||
}
|
||||
```
|
||||
|
||||
### use() hook (React 19)
|
||||
```tsx
|
||||
function Comments({ promise }: { promise: Promise<Comment[]> }) {
|
||||
const comments = use(promise); // suspends
|
||||
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
### Image optimization (Next 15)
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
<Image src="/hero.avif" width={1280} height={720} priority alt="" sizes="100vw" />
|
||||
```
|
||||
|
||||
### Speculation Rules (prerender)
|
||||
```html
|
||||
<script type="speculationrules">
|
||||
{
|
||||
"prerender": [{ "where": { "selector_matches": "a.product" }, "eagerness": "moderate" }]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Scheduler.yield long task
|
||||
```javascript
|
||||
async function process(items) {
|
||||
for (const it of items) {
|
||||
work(it);
|
||||
if ('scheduler' in self) await scheduler.yield();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS cascade layers
|
||||
```css
|
||||
@layer reset, base, components, utilities;
|
||||
@layer reset { * { margin: 0; box-sizing: border-box; } }
|
||||
@layer components { .btn { padding: 0.5rem 1rem; } }
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Stack |
|
||||
|---|---|
|
||||
| 매 SEO content site | Astro 5 (zero JS default) |
|
||||
| 매 SaaS dashboard | Vite + React 19 (CSR) |
|
||||
| 매 commerce | Next 15 App Router (RSC + ISR) |
|
||||
| 매 docs | Astro Starlight / Docusaurus |
|
||||
| 매 marketing | Astro / 11ty |
|
||||
| 매 realtime | React + WebSocket / Liveblocks |
|
||||
|
||||
**기본값**: Vite + React 19 + TS strict + ESLint + Vitest. 매 SSR 필요 시 매 Next 15.
|
||||
|
||||
## 🔗 Graph
|
||||
- 응용: [[E-commerce]]
|
||||
- Adjacent: [[React]] · [[Performance]] · [[Accessibility (A11y)|Accessibility]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 stack 선택, 매 perf 진단, 매 pattern (RSC, Suspense, View Transitions) 적용.
|
||||
**언제 X**: 매 backend-only — 매 다른 도메인.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **CSS-in-JS runtime**: 매 INP 악화 (2026 모범: zero-runtime — Tailwind / vanilla-extract / Panda).
|
||||
- **Mega global state**: 매 server vs client state 미분리.
|
||||
- **Polyfill 남발**: 매 modern browser default — 매 baseline 활용.
|
||||
- **No a11y test**: 매 axe-core CI 없음.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev, MDN baseline 2026, React 19 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 2026 stack + modern primitives |
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
id: wiki-2026-0508-functional-programming-in-typesc
|
||||
title: Functional Programming in TypeScript
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [FP TS, fp-ts, Effect-TS, Functional TypeScript]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, functional-programming, effect-ts, fp-ts]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: effect-ts
|
||||
---
|
||||
|
||||
# Functional Programming in TypeScript
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 type system 이 매 effect 와 매 error 를 매 first-class 로 추적한다."**. Pure function + immutable + typed effect. 매 2026 의 FP-TS landscape 는 매 Effect-TS 가 매 dominant — fp-ts/io-ts 의 매 후계자.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 원칙
|
||||
- **Purity**: 매 같은 input → 매 같은 output. 매 side effect X.
|
||||
- **Immutability**: 매 mutation X. 매 readonly + structural copy.
|
||||
- **Composition**: 매 small function 의 매 pipe / flow.
|
||||
- **Total functions**: 매 모든 input 에 매 정의된 output. 매 throw X — 매 Either / Effect.
|
||||
- **Typed errors**: 매 error type 을 매 signature 에 매 표현.
|
||||
|
||||
### 매 핵심 구조
|
||||
- **Option**: nullable 의 매 type-safe 표현.
|
||||
- **Either**: success / failure union.
|
||||
- **Effect**: 매 lazy computation + dependency + error type — `Effect<A, E, R>`.
|
||||
- **Stream**: 매 lazy async iterator.
|
||||
- **Schema**: 매 runtime + static type.
|
||||
|
||||
### 매 응용
|
||||
1. API client — 매 typed error / retry / timeout 의 매 declarative.
|
||||
2. Form validation — 매 Schema parse + Either.
|
||||
3. Concurrent workflow — 매 Effect.all + structured concurrency.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Option / Either basics (Effect-TS)
|
||||
```typescript
|
||||
import { Option, Either } from 'effect';
|
||||
|
||||
const safeDiv = (a: number, b: number): Option.Option<number> =>
|
||||
b === 0 ? Option.none() : Option.some(a / b);
|
||||
|
||||
const parsed: Either.Either<number, string> =
|
||||
Either.try({ try: () => JSON.parse('{"x":1}').x, catch: e => String(e) });
|
||||
```
|
||||
|
||||
### Pipe + flow
|
||||
```typescript
|
||||
import { pipe, flow } from 'effect';
|
||||
|
||||
const upper = (s: string) => s.toUpperCase();
|
||||
const exclaim = (s: string) => `${s}!`;
|
||||
|
||||
const shout = flow(upper, exclaim);
|
||||
console.log(pipe('hello', shout)); // "HELLO!"
|
||||
```
|
||||
|
||||
### Effect with typed errors
|
||||
```typescript
|
||||
import { Effect } from 'effect';
|
||||
|
||||
class NotFoundError { readonly _tag = 'NotFoundError' }
|
||||
class NetworkError { readonly _tag = 'NetworkError' }
|
||||
|
||||
const fetchUser = (id: string): Effect.Effect<User, NotFoundError | NetworkError> =>
|
||||
Effect.tryPromise({
|
||||
try: () => fetch(`/u/${id}`).then(r => {
|
||||
if (r.status === 404) throw new NotFoundError();
|
||||
return r.json();
|
||||
}),
|
||||
catch: e => e instanceof NotFoundError ? e : new NetworkError(),
|
||||
});
|
||||
```
|
||||
|
||||
### Service / dependency injection
|
||||
```typescript
|
||||
import { Context, Effect, Layer } from 'effect';
|
||||
|
||||
class Logger extends Context.Tag('Logger')<Logger, { log: (m: string) => void }>() {}
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const logger = yield* Logger;
|
||||
logger.log('hi');
|
||||
});
|
||||
|
||||
const LoggerLive = Layer.succeed(Logger, { log: console.log });
|
||||
Effect.runSync(Effect.provide(program, LoggerLive));
|
||||
```
|
||||
|
||||
### Schema validation (runtime + static)
|
||||
```typescript
|
||||
import { Schema } from 'effect';
|
||||
|
||||
const User = Schema.Struct({
|
||||
id: Schema.String,
|
||||
age: Schema.Number.pipe(Schema.between(0, 150)),
|
||||
email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+$/)),
|
||||
});
|
||||
type User = Schema.Schema.Type<typeof User>;
|
||||
|
||||
const parseUser = Schema.decodeUnknownEither(User);
|
||||
```
|
||||
|
||||
### Structured concurrency
|
||||
```typescript
|
||||
const fetchAll = Effect.all(
|
||||
[fetchUser('a'), fetchUser('b'), fetchUser('c')],
|
||||
{ concurrency: 2 } // limit
|
||||
).pipe(
|
||||
Effect.timeout('5 seconds'),
|
||||
Effect.retry({ times: 3 }),
|
||||
);
|
||||
```
|
||||
|
||||
### Immutable update with Optic
|
||||
```typescript
|
||||
import { Lens } from 'monocle-ts';
|
||||
|
||||
interface Order { user: { name: string } }
|
||||
const userName = Lens.fromPath<Order>()(['user', 'name']);
|
||||
const upd = userName.modify(s => s.toUpperCase());
|
||||
const next = upd({ user: { name: 'foo' } }); // structural copy
|
||||
```
|
||||
|
||||
### Discriminated union exhaustiveness
|
||||
```typescript
|
||||
type Shape =
|
||||
| { kind: 'circle'; r: number }
|
||||
| { kind: 'square'; s: number };
|
||||
|
||||
const area = (sh: Shape): number => {
|
||||
switch (sh.kind) {
|
||||
case 'circle': return Math.PI * sh.r ** 2;
|
||||
case 'square': return sh.s ** 2;
|
||||
default: { const _: never = sh; return _; } // compile error if missed
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 nullable | Option (or `T \| undefined` + helper) |
|
||||
| 매 expected error | Either / Effect typed error |
|
||||
| 매 unexpected (bug) | throw — 매 error boundary |
|
||||
| 매 async + DI + retry | Effect-TS |
|
||||
| 매 simple validation | Zod (lighter) |
|
||||
| 매 large schema + transform | Effect Schema |
|
||||
|
||||
**기본값**: 매 plain TS + readonly + discriminated union. 매 복잡한 effect orchestration 은 매 Effect-TS.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[Functional Programming]]
|
||||
- 변형: [[Effect TS]] · [[fp-ts]]
|
||||
- 응용: [[Validation]] · [[API Client]] · [[State Management]]
|
||||
- Adjacent: [[Zod]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 typed error model 설계, 매 schema-first API, 매 effect-heavy domain code.
|
||||
**언제 X**: 매 simple CRUD UI — 매 overkill.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Effect 모든 곳**: 매 over-abstraction. 매 leaf function 은 매 plain.
|
||||
- **`any` escape hatch**: 매 type 의 매 의미 손실.
|
||||
- **Mutating in pipe**: 매 immutability 위반.
|
||||
- **Throw inside Effect.try with rethrow**: 매 typed error 회피.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Effect-TS 3.x docs, fp-ts legacy migration guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 Effect-TS 기준 패턴 + Schema |
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
id: wiki-2026-0508-gpu-resources
|
||||
title: GPU Resources
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [GPU Buffer, GPU Texture, Render Target, WebGPU Resources]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [gpu, webgpu, graphics, rendering]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: webgpu
|
||||
---
|
||||
|
||||
# GPU Resources
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 GPU 는 매 buffer + texture + sampler + binding 의 매 4 primitive 위에서 매 모든 것을 그린다."**. Buffer 는 매 raw memory, texture 는 매 sampled grid, render target 은 매 write 가능한 texture. 매 2026 WebGPU / Vulkan / Metal 모두 매 같은 model.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Buffer 종류
|
||||
- **Vertex buffer**: 매 vertex attribute (position, normal, uv).
|
||||
- **Index buffer**: 매 triangle index.
|
||||
- **Uniform buffer (UBO)**: 매 small constant data — 매 16KB 권장.
|
||||
- **Storage buffer (SSBO)**: 매 large read/write — 매 compute shader.
|
||||
- **Staging buffer**: 매 CPU→GPU upload 의 매 intermediate.
|
||||
|
||||
### 매 Texture 종류
|
||||
- **Sampled texture**: 매 shader 에서 매 sample.
|
||||
- **Storage texture**: 매 compute write.
|
||||
- **Depth/Stencil**: 매 depth test.
|
||||
- **Cube map / 3D / Array**: 매 special layout.
|
||||
- **Format**: rgba8unorm, rgba16float, bgra8unorm-srgb, depth32float, etc.
|
||||
|
||||
### 매 Render Target
|
||||
- 매 texture + 매 RENDER_ATTACHMENT usage. 매 framebuffer 의 매 color/depth attachment.
|
||||
- 매 swap chain texture 는 매 surface 가 매 제공 (acquireNextImage / getCurrentTexture).
|
||||
|
||||
### 매 Binding model
|
||||
- **WebGPU/Vulkan**: bind group / descriptor set — 매 group of resources.
|
||||
- **Layout**: 매 shader 와 매 CPU 가 매 합의한 schema.
|
||||
|
||||
### 매 응용
|
||||
1. PBR renderer — 매 g-buffer (multiple render target).
|
||||
2. Post-processing — 매 ping-pong render target.
|
||||
3. Compute particles — 매 storage buffer + indirect draw.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### WebGPU buffer create + write
|
||||
```typescript
|
||||
const buf = device.createBuffer({
|
||||
size: 256,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
device.queue.writeBuffer(buf, 0, new Float32Array([1, 2, 3, 4]));
|
||||
```
|
||||
|
||||
### Vertex buffer + draw
|
||||
```typescript
|
||||
const verts = new Float32Array([0, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0]);
|
||||
const vb = device.createBuffer({
|
||||
size: verts.byteLength,
|
||||
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
device.queue.writeBuffer(vb, 0, verts);
|
||||
|
||||
pass.setVertexBuffer(0, vb);
|
||||
pass.draw(3);
|
||||
```
|
||||
|
||||
### Texture upload
|
||||
```typescript
|
||||
const img = await createImageBitmap(blob);
|
||||
const tex = device.createTexture({
|
||||
size: [img.width, img.height, 1],
|
||||
format: 'rgba8unorm',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: img },
|
||||
{ texture: tex },
|
||||
[img.width, img.height],
|
||||
);
|
||||
```
|
||||
|
||||
### Render target (offscreen)
|
||||
```typescript
|
||||
const colorTex = device.createTexture({
|
||||
size: [w, h, 1],
|
||||
format: 'rgba16float',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
||||
});
|
||||
const depthTex = device.createTexture({
|
||||
size: [w, h, 1],
|
||||
format: 'depth24plus',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const enc = device.createCommandEncoder();
|
||||
const pass = enc.beginRenderPass({
|
||||
colorAttachments: [{
|
||||
view: colorTex.createView(),
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
}],
|
||||
depthStencilAttachment: {
|
||||
view: depthTex.createView(),
|
||||
depthClearValue: 1.0,
|
||||
depthLoadOp: 'clear',
|
||||
depthStoreOp: 'store',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Bind group + sampler
|
||||
```typescript
|
||||
const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||
|
||||
const layout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
||||
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
||||
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
||||
],
|
||||
});
|
||||
|
||||
const group = device.createBindGroup({
|
||||
layout,
|
||||
entries: [
|
||||
{ binding: 0, resource: sampler },
|
||||
{ binding: 1, resource: tex.createView() },
|
||||
{ binding: 2, resource: { buffer: ubo } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Compute shader with storage buffer
|
||||
```typescript
|
||||
const ssbo = device.createBuffer({
|
||||
size: 1024 * 1024,
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
||||
});
|
||||
const pipe = device.createComputePipeline({
|
||||
layout: 'auto',
|
||||
compute: { module: device.createShaderModule({ code: wgsl }), entryPoint: 'main' },
|
||||
});
|
||||
const enc = device.createCommandEncoder();
|
||||
const cp = enc.beginComputePass();
|
||||
cp.setPipeline(pipe);
|
||||
cp.setBindGroup(0, bindGroup);
|
||||
cp.dispatchWorkgroups(1024);
|
||||
cp.end();
|
||||
```
|
||||
|
||||
### Resource lifecycle
|
||||
```typescript
|
||||
buffer.destroy(); // explicit free — required for large resources
|
||||
texture.destroy();
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Resource |
|
||||
|---|---|
|
||||
| 매 small constants per draw | Uniform buffer |
|
||||
| 매 large read-only data | Storage buffer (read) |
|
||||
| 매 read/write GPGPU | Storage buffer |
|
||||
| 매 sampled image | Texture (2D/3D/Cube) |
|
||||
| 매 framebuffer attachment | Texture w/ RENDER_ATTACHMENT |
|
||||
| 매 CPU↔GPU stream | Staging buffer + queue.writeBuffer |
|
||||
|
||||
**기본값**: WebGPU + bind group layout 명시. 매 destroy() 호출 잊지 X.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[WebGPU]] · [[Computer Graphics]]
|
||||
- 응용: [[Post Processing]] · [[Compute Shader]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 GPU pipeline 설계, 매 memory budget, 매 binding layout debug.
|
||||
**언제 X**: 매 high-level engine (Three.js, Babylon) 사용 시 — 매 abstraction 위.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 frame buffer create**: 매 alloc 폭증. 매 reuse / pool.
|
||||
- **UBO 에 매 large data**: 매 16KB cap. 매 SSBO 사용.
|
||||
- **destroy() 누락**: 매 GPU OOM.
|
||||
- **Bind group layout mismatch**: 매 shader / CPU schema drift.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (WebGPU spec, Vulkan spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 WebGPU buffer/texture/RT 패턴 |
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
---
|
||||
id: wiki-2026-0508-gpu-webgl-파이프라인의-미세-지연-micro-lat
|
||||
title: GPU WebGL 파이프라인의 미세 지연(Micro-latency) 측정 사례
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [WebGL Micro-latency, GPU Pipeline Latency, WebGL Profiling]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [webgl, gpu, performance, latency, profiling]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: webgl2
|
||||
---
|
||||
|
||||
# GPU WebGL 파이프라인의 미세 지연(Micro-latency) 측정 사례
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 GPU 작업은 비동기 — `gl.finish()` 의 X, query object 의 O"**. WebGL 의 draw call 은 GPU command buffer 에 enqueue 만 되므로 CPU 측 `performance.now()` 만으로 측정 시 실제 GPU 시간과 ms 단위 misalignment 발생. `EXT_disjoint_timer_query_webgl2` 가 매 정답.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 측정 대상 분리
|
||||
- **CPU time**: JS → WebGL command 인코딩 (validate, draw call dispatch).
|
||||
- **GPU time**: shader 실행 + raster + blend.
|
||||
- **Present latency**: swapchain → display 의 frame pacing (compositor 의존).
|
||||
|
||||
### 매 측정 수단
|
||||
- `EXT_disjoint_timer_query_webgl2` — GPU timestamp query, ns 정밀도.
|
||||
- `gpu.requestAdapter()` (WebGPU) — 매 native timestamp-query feature.
|
||||
- Chrome DevTools Performance → GPU track.
|
||||
- `Spector.js` — frame capture + per-call cost.
|
||||
|
||||
### 매 응용
|
||||
1. 60→120Hz upgrade 시 frame budget shrink (16.6→8.3ms) 의 bottleneck localization.
|
||||
2. Mobile WebGL 의 thermal throttling 감지 (GPU time 의 점진적 increase).
|
||||
3. PWA 게임 의 input→render latency end-to-end 측정.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Timer Query — frame GPU time 측정
|
||||
```javascript
|
||||
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
|
||||
const queries = [];
|
||||
|
||||
function frame() {
|
||||
const q = gl.createQuery();
|
||||
gl.beginQuery(ext.TIME_ELAPSED_EXT, q);
|
||||
drawScene();
|
||||
gl.endQuery(ext.TIME_ELAPSED_EXT);
|
||||
queries.push({ q, frameId: frameCount++ });
|
||||
|
||||
// resolve 매 several frame later (async)
|
||||
for (let i = queries.length - 1; i >= 0; i--) {
|
||||
const { q, frameId } = queries[i];
|
||||
const available = gl.getQueryParameter(q, gl.QUERY_RESULT_AVAILABLE);
|
||||
const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);
|
||||
if (available && !disjoint) {
|
||||
const ns = gl.getQueryParameter(q, gl.QUERY_RESULT);
|
||||
console.log(`frame ${frameId} GPU: ${(ns / 1e6).toFixed(2)} ms`);
|
||||
gl.deleteQuery(q);
|
||||
queries.splice(i, 1);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
```
|
||||
|
||||
### Anti — gl.finish() blocking
|
||||
```javascript
|
||||
// X — pipeline stall, 매 production 에서 절대 사용 X
|
||||
const t0 = performance.now();
|
||||
drawScene();
|
||||
gl.finish(); // CPU↔GPU sync barrier
|
||||
const t1 = performance.now();
|
||||
// t1-t0 는 측정 대상이 아닌 stall 의 cost
|
||||
```
|
||||
|
||||
### Input→render latency (RAIL model)
|
||||
```javascript
|
||||
let inputTs = 0;
|
||||
canvas.addEventListener('pointerdown', e => {
|
||||
inputTs = e.timeStamp; // 매 hardware timestamp
|
||||
});
|
||||
|
||||
function frame(now) {
|
||||
if (inputTs > 0 && pendingInputProcessed) {
|
||||
const latency = now - inputTs;
|
||||
if (latency > 100) console.warn(`input→render: ${latency}ms`);
|
||||
inputTs = 0;
|
||||
}
|
||||
drawScene();
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
```
|
||||
|
||||
### Disjoint detection (thermal throttling)
|
||||
```javascript
|
||||
function checkThermal() {
|
||||
const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT);
|
||||
if (disjoint) {
|
||||
// 매 GPU 가 context switch / power state 변경 — measurement invalid
|
||||
metricsBuffer.flush('thermal_event');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WebGPU — native timestamp query
|
||||
```javascript
|
||||
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
|
||||
const encoder = device.createCommandEncoder();
|
||||
const pass = encoder.beginRenderPass({
|
||||
...,
|
||||
timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 }
|
||||
});
|
||||
pass.draw(...);
|
||||
pass.end();
|
||||
encoder.resolveQuerySet(querySet, 0, 2, resolveBuffer, 0);
|
||||
// readback async
|
||||
```
|
||||
|
||||
### Frame pacing (requestVideoFrameCallback)
|
||||
```javascript
|
||||
videoEl.requestVideoFrameCallback((now, meta) => {
|
||||
const presentLatency = meta.presentationTime - meta.captureTime;
|
||||
// 매 actual display 까지의 latency 측정
|
||||
});
|
||||
```
|
||||
|
||||
### Sliding-window p99 측정
|
||||
```javascript
|
||||
const samples = new Float32Array(120); // 2s @ 60Hz
|
||||
let idx = 0;
|
||||
function record(ms) {
|
||||
samples[idx++ % samples.length] = ms;
|
||||
if (idx % 60 === 0) {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
console.log('p50:', sorted[60], 'p99:', sorted[118]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| WebGL2 + 매 정밀 측정 | `EXT_disjoint_timer_query_webgl2` |
|
||||
| WebGPU 가능 | timestampWrites |
|
||||
| Quick debug | Spector.js capture |
|
||||
| Production telemetry | sliding-window p99 + disjoint flag |
|
||||
| Cross-browser fallback | CPU time only + acknowledged limitation |
|
||||
|
||||
**기본값**: WebGL2 + timer query async readback, p99/disjoint 매 telemetry.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[WebGL]] · [[GPU]]
|
||||
- 변형: [[WebGPU]] · [[Vulkan]]
|
||||
- 응용: [[Three.js]]
|
||||
- Adjacent: [[Browser-Compositor]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: WebGL/WebGPU 앱 의 frame budget 초과 진단, 60→120Hz 마이그레이션, mobile thermal 분석.
|
||||
**언제 X**: 일반 DOM perf (use Performance API), Canvas2D (no GPU query).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **gl.finish() in frame loop**: pipeline stall, 매 측정값 자체 가 왜곡.
|
||||
- **performance.now() only**: GPU async 의 무시 — frame 의 N+2 까지 결과 가 안 나올 수 있음.
|
||||
- **disjoint flag 무시**: thermal/power state 변경 시 measurement 가 garbage 인데 그대로 report.
|
||||
- **single sample**: GC, compositor jitter 의 무시 — 매 p99 측정 필수.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Khronos `EXT_disjoint_timer_query_webgl2` spec, Chrome DevTools docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — WebGL micro-latency 측정 패턴 정리 |
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
id: wiki-2026-0508-graphql-code-generator
|
||||
title: GraphQL Code Generator
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [graphql-codegen, GQL Codegen, Type-safe GraphQL]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [graphql, typescript, codegen, type-safety]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: graphql
|
||||
---
|
||||
|
||||
# GraphQL Code Generator
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 schema → typed client 의 자동화"**. `.graphql` schema + operation 으로부터 TypeScript types, hook, fragment 의 생성. 매 schema drift 의 compile-time 차단 — `any` 의 X, `User.email` typo 의 즉시 error.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 plugin
|
||||
- **typescript**: schema → TS types (scalar, enum, input, object).
|
||||
- **typescript-operations**: query/mutation operation → typed result.
|
||||
- **typed-document-node**: TypedDocumentNode (apollo-client / urql 의 input).
|
||||
- **client-preset** (modern, 2026 default): 매 fragment-masking, persisted-query 의 통합 preset.
|
||||
|
||||
### 매 fragment masking
|
||||
- Component A 가 fragment X 정의 → component B 가 fragment X 의 field 접근 시 compile error.
|
||||
- 매 over-fetching 의 prevention 강제.
|
||||
|
||||
### 매 응용
|
||||
1. React + Apollo / urql 매 typed hook 자동 생성.
|
||||
2. Backend schema change 시 client compile error 즉시 감지.
|
||||
3. Persisted queries (production safety, query whitelisting).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### codegen.ts (client-preset)
|
||||
```typescript
|
||||
import { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: 'https://api.example.com/graphql',
|
||||
documents: ['src/**/*.{ts,tsx}', '!src/gql/**/*'],
|
||||
generates: {
|
||||
'./src/gql/': {
|
||||
preset: 'client',
|
||||
presetConfig: {
|
||||
fragmentMasking: { unmaskFunctionName: 'getFragmentData' },
|
||||
},
|
||||
config: {
|
||||
useTypeImports: true,
|
||||
scalars: { DateTime: 'string', UUID: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hooks: { afterAllFileWrite: ['prettier --write'] },
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm graphql-codegen --watch
|
||||
```
|
||||
|
||||
### Operation — typed result
|
||||
```typescript
|
||||
// src/components/UserCard.tsx
|
||||
import { graphql } from '../gql';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
const USER_QUERY = graphql(`
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) { id name email avatarUrl }
|
||||
}
|
||||
`);
|
||||
|
||||
export function UserCard({ id }: { id: string }) {
|
||||
const { data } = useQuery(USER_QUERY, { variables: { id } });
|
||||
// 매 data?.user 의 type 의 fully inferred
|
||||
return <div>{data?.user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Fragment masking
|
||||
```typescript
|
||||
const USER_AVATAR_FRAGMENT = graphql(`
|
||||
fragment UserAvatar on User { avatarUrl name }
|
||||
`);
|
||||
|
||||
function Avatar({ user }: { user: FragmentType<typeof USER_AVATAR_FRAGMENT> }) {
|
||||
const u = getFragmentData(USER_AVATAR_FRAGMENT, user);
|
||||
return <img src={u.avatarUrl} alt={u.name} />;
|
||||
}
|
||||
|
||||
// parent — 매 user 의 email 의 access X (fragment 의 declare X)
|
||||
function Parent({ user }) {
|
||||
return <Avatar user={user} />;
|
||||
// user.email 의 access 시 TS error
|
||||
}
|
||||
```
|
||||
|
||||
### Persisted queries
|
||||
```typescript
|
||||
{
|
||||
generates: {
|
||||
'./persisted-operations.json': {
|
||||
preset: 'client',
|
||||
presetConfig: { persistedDocuments: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// runtime — query string 의 X, hash 만 전송
|
||||
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
|
||||
const link = createPersistedQueryLink({ generateHash: doc => doc['__meta__']['hash'] });
|
||||
```
|
||||
|
||||
### Custom scalar mapping
|
||||
```typescript
|
||||
config: {
|
||||
scalars: {
|
||||
DateTime: 'string', // ISO 8601
|
||||
JSON: 'Record<string, unknown>',
|
||||
BigInt: 'string',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Watch + CI
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
"codegen:watch": "graphql-codegen --watch",
|
||||
"ci:codegen-check": "graphql-codegen && git diff --exit-code src/gql/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-schema (federation)
|
||||
```typescript
|
||||
{
|
||||
schema: ['./schema/users.graphql', './schema/products.graphql'],
|
||||
// 매 federated graph 의 single typed client
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 새 React + GraphQL | client-preset + fragment masking |
|
||||
| Apollo Client (legacy) | typescript + typescript-react-apollo |
|
||||
| urql | client-preset (urql 의 native 지원) |
|
||||
| Production 보안 | persisted queries 의 enable |
|
||||
| Backend schema 의 evolve | CI 의 codegen drift check |
|
||||
|
||||
**기본값**: client-preset + fragmentMasking, CI 의 codegen drift check.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]]
|
||||
- Adjacent: [[OpenAPI-Codegen]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: GraphQL schema 가 있는 TS project, multi-team 의 schema drift 방지, persisted query 도입.
|
||||
**언제 X**: REST-only, 매 GraphQL schema 의 unstable 한 prototype phase.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **codegen output 의 manual edit**: 매 next run 시 overwrite, 매 변경 의 lost.
|
||||
- **fragment 의 component 외 정의**: fragment masking 의 weak — co-location 강제.
|
||||
- **DateTime 의 `Date` mapping**: GraphQL response 는 string, 매 runtime mismatch 유발.
|
||||
- **CI 에 codegen drift check 의 X**: schema 의 silent breakage.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (graphql-code-generator.com docs, client-preset migration guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — GraphQL Codegen client-preset 패턴 정리 |
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
id: wiki-2026-0508-guilty-gear-xrd-rendering-pipeli
|
||||
title: Guilty Gear Xrd Rendering Pipeline
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Xrd Toon Shading, Arc System Works Anime Render, Guilty Gear Cel Shading]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [graphics, cel-shading, npr, anime, arc-system-works]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: hlsl
|
||||
framework: unreal-engine
|
||||
---
|
||||
|
||||
# Guilty Gear Xrd Rendering Pipeline
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 3D model 을 매 2D anime 처럼 보이게 매 render — 매 artist control 이 매 algorithm 보다 매 우선."**. Arc System Works (Junya C. Motomura, GDC 2014) 의 매 Xrd 파이프라인은 매 NPR (non-photorealistic rendering) 의 매 landmark. 매 hand-painted normal + custom shadow + ink line + post-processing 의 매 stack.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Pipeline stage
|
||||
1. **Modeling**: 매 low-poly model + 매 hand-tuned vertex normal.
|
||||
2. **Base color shading**: 매 ramp texture (1D LUT) — 매 toon step.
|
||||
3. **Custom shadow direction**: 매 light vector 무시 + 매 artist 가 매 manual paint.
|
||||
4. **Outline (ink line)**: 매 inverted hull / 매 post-process edge.
|
||||
5. **Specular & rim**: 매 hand-placed highlight texture.
|
||||
6. **Post-processing**: 매 chromatic aberration, 매 bloom, 매 color grading.
|
||||
|
||||
### 매 Vertex normal trick
|
||||
- 매 face 의 매 normal 을 매 sphere 로 매 매뉴얼 average — 매 2D 에서 매 깔끔한 shadow shape.
|
||||
- 매 hair 는 매 normal 을 매 head 의 매 center 로 매 향하게 — 매 anisotropic specular 의 매 illusion.
|
||||
|
||||
### 매 Ramp texture
|
||||
- 1D texture, 매 N·L → matrix lookup. 매 step 2-3 단계 — 매 hard edge.
|
||||
- 매 character 별 매 다른 ramp — 매 다른 mood.
|
||||
|
||||
### 매 Outline
|
||||
- **Backface inverted hull**: 매 model 을 매 normal 방향 outward extrude + cull front. 매 cheap, 매 thickness 균일.
|
||||
- **Vertex color mask**: 매 detail line 의 매 thickness 제어.
|
||||
|
||||
### 매 응용
|
||||
1. Anime fighting game (Dragon Ball FighterZ, Granblue Versus 도 매 변형).
|
||||
2. Mobile RPG (Genshin Impact 의 매 cel).
|
||||
3. VTuber rendering.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Toon shader (HLSL)
|
||||
```hlsl
|
||||
float NdotL = saturate(dot(normalize(worldNormal), lightDir));
|
||||
float ramp = tex2D(_RampTex, float2(NdotL, 0.5)).r;
|
||||
float3 base = tex2D(_MainTex, uv).rgb;
|
||||
float3 col = base * lerp(_ShadowColor, 1.0, ramp);
|
||||
return float4(col, 1);
|
||||
```
|
||||
|
||||
### Inverted hull outline
|
||||
```hlsl
|
||||
// Vertex pass — render with cull = Front
|
||||
float3 normalWS = TransformObjectToWorldNormal(IN.normal);
|
||||
float3 posWS = TransformObjectToWorld(IN.positionOS) + normalWS * _OutlineWidth;
|
||||
OUT.positionCS = TransformWorldToHClip(posWS);
|
||||
OUT.color = _OutlineColor;
|
||||
```
|
||||
|
||||
### Custom shadow direction (face)
|
||||
```hlsl
|
||||
// Replace light dir with artist-specified vector for face
|
||||
float3 fixedLight = mul((float3x3)unity_ObjectToWorld, _FaceLightDir);
|
||||
float NdotL = dot(worldNormal, normalize(fixedLight));
|
||||
```
|
||||
|
||||
### Hair tangent trick
|
||||
```hlsl
|
||||
// Hair anisotropic — tangent points along hair flow
|
||||
float3 H = normalize(viewDir + lightDir);
|
||||
float TdotH = dot(hairTangent, H);
|
||||
float spec = pow(1.0 - TdotH * TdotH, _HairGloss);
|
||||
```
|
||||
|
||||
### Edge detect post (Sobel on depth+normal)
|
||||
```hlsl
|
||||
float3 nC = SampleNormal(uv);
|
||||
float dC = SampleDepth(uv);
|
||||
float edge = 0;
|
||||
[unroll] for (int i = 0; i < 4; ++i) {
|
||||
float3 nS = SampleNormal(uv + offset[i] * _TexelSize);
|
||||
float dS = SampleDepth(uv + offset[i] * _TexelSize);
|
||||
edge += saturate(1.0 - dot(nC, nS)) + abs(dC - dS) * _DepthWeight;
|
||||
}
|
||||
return lerp(sceneColor, _EdgeColor, saturate(edge - _Threshold));
|
||||
```
|
||||
|
||||
### Ramp texture authoring (Python)
|
||||
```python
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
ramp = np.zeros((1, 256, 3), dtype=np.uint8)
|
||||
ramp[0, :64] = (60, 50, 80) # deep shadow
|
||||
ramp[0, 64:160] = (140, 130, 150) # mid
|
||||
ramp[0, 160:] = (250, 245, 240) # lit
|
||||
Image.fromarray(ramp).save('toon_ramp.png')
|
||||
```
|
||||
|
||||
### Specular hand-placed mask
|
||||
```hlsl
|
||||
// Per-pixel mask painted by artist — multiplies spec
|
||||
float specMask = tex2D(_SpecMaskTex, uv).r;
|
||||
float3 spec = pow(saturate(dot(N, H)), _Power) * specMask * _SpecColor;
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 효과 | Approach |
|
||||
|---|---|
|
||||
| 매 hard cel shadow | Ramp 1D texture + step |
|
||||
| 매 face shadow control | Custom light vector |
|
||||
| 매 cheap outline | Inverted hull (backface) |
|
||||
| 매 detail outline | Post-process Sobel on N+depth |
|
||||
| 매 anime hair | Tangent-based aniso + ramp |
|
||||
| 매 emotional rim | Hand-placed highlight mask |
|
||||
|
||||
**기본값**: ramp + inverted hull + face light override. 매 modern (Unreal/Unity) 모두 매 stable.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Computer Graphics]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 anime / cel-shading 재현, 매 NPR 학습, 매 outline / shadow 의 매 trick 분석.
|
||||
**언제 X**: 매 photorealistic — 매 PBR 파이프라인.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Auto-generated normal**: 매 anime look 의 매 핵심 — 매 hand-tune 필수.
|
||||
- **Light dir 만 의존**: 매 face 매 dirty shadow.
|
||||
- **Outline 두께 globally same**: 매 detail loss — 매 vertex color mask.
|
||||
- **Bloom 만 의존한 mood**: 매 ramp / grading 이 매 base.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Junya C. Motomura, "Guilty Gear Xrd's Art Style", GDC 2014).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 Xrd pipeline + HLSL 패턴 |
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
id: wiki-2026-0508-hermes
|
||||
title: Hermes
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Hermes Engine, React Native Hermes, Hermes JS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react-native, javascript-engine, hermes, mobile]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: cpp
|
||||
framework: react-native
|
||||
---
|
||||
|
||||
# Hermes
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 mobile 에 매 최적화된 매 ahead-of-time bytecode 매 JS engine."**. Meta 가 매 React Native 용으로 매 만든 engine. 매 startup time + 매 memory + 매 binary size 가 매 우선. 매 2026 의 매 RN 0.70+ 부터 매 default — 매 JSC 대비 매 cold start 30-50% 단축.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 설계 목표
|
||||
- **Fast startup**: 매 AOT bytecode → 매 parse 비용 0.
|
||||
- **Low memory**: 매 generational GC, 매 NaN-boxing 없는 매 Hades collector (concurrent).
|
||||
- **Small binary**: 매 SymbolTable / StringTable 의 매 dedupe.
|
||||
- **Mobile first**: 매 ARM64 + 매 small heap 의 매 가정.
|
||||
|
||||
### 매 Architecture
|
||||
- **Compiler**: hbc (Hermes ByteCode) — 매 build 시 매 .hbc 파일 생성.
|
||||
- **VM**: register-based interpreter — 매 stack-based JSC 와 매 다름.
|
||||
- **GC**: Hades — 매 concurrent + generational.
|
||||
- **No JIT** (default): 매 mobile binary 정책 (iOS) + 매 startup 우선. 매 static Hermes (sh) 가 매 AOT native compile 실험.
|
||||
|
||||
### 매 vs JSC vs V8
|
||||
| 항목 | Hermes | JSC | V8 |
|
||||
|---|---|---|---|
|
||||
| Startup | 매 빠름 (AOT) | 매 보통 | 매 느림 (parse + JIT) |
|
||||
| Throughput | 매 낮음 (no JIT) | 매 보통 | 매 매우 높음 |
|
||||
| Memory | 매 낮음 | 매 보통 | 매 높음 |
|
||||
| 사용처 | RN | iOS Safari, RN (legacy) | Chrome, Node |
|
||||
|
||||
### 매 응용
|
||||
1. RN app cold start 단축.
|
||||
2. Bundle size 절감 (.hbc 가 .js 보다 작음).
|
||||
3. 매 Hermes-specific debugger (Chrome DevTools 호환).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Enable in RN (already default in 0.70+)
|
||||
```javascript
|
||||
// android/app/build.gradle
|
||||
project.ext.react = [
|
||||
enableHermes: true,
|
||||
hermesCommand: "../../node_modules/react-native/sdks/hermes/destroot/bin/hermesc",
|
||||
]
|
||||
|
||||
// ios/Podfile
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => true,
|
||||
)
|
||||
```
|
||||
|
||||
### Verify Hermes is running
|
||||
```javascript
|
||||
const isHermes = () => !!global.HermesInternal;
|
||||
console.log('engine:', isHermes() ? 'Hermes' : 'JSC');
|
||||
```
|
||||
|
||||
### Compile JS to bytecode (CLI)
|
||||
```bash
|
||||
hermesc -emit-binary -out=index.hbc index.js
|
||||
# Inspect
|
||||
hermes -dump-bytecode index.hbc
|
||||
```
|
||||
|
||||
### Profile Hermes startup
|
||||
```bash
|
||||
# Sample profiler
|
||||
hermes --sample-profiling -O index.js
|
||||
# → output trace.json (Chrome DevTools profile format)
|
||||
```
|
||||
|
||||
### Memory leak detection (Hermes-specific)
|
||||
```javascript
|
||||
// Heap snapshot
|
||||
import { HermesInternal } from 'react-native';
|
||||
const snap = HermesInternal?.getInstrumentedStats?.();
|
||||
console.log(snap); // js_heapSize, js_allocatedBytes, ...
|
||||
```
|
||||
|
||||
### Avoid Hermes pitfalls
|
||||
```javascript
|
||||
// 1. eval / new Function — supported but slow (no JIT)
|
||||
// 2. Proxy — supported in 2026 but heavier than V8
|
||||
// 3. Intl — opt-in (intl=true in build flag), default 작음
|
||||
|
||||
// Prefer static patterns
|
||||
const handler = handlers[type]; // O(1) lookup
|
||||
// over
|
||||
const handler = eval(`handle_${type}`);
|
||||
```
|
||||
|
||||
### Static Hermes (experimental 2026)
|
||||
```bash
|
||||
# AOT to native — bypass interpreter
|
||||
shermes -typed -exec index.ts
|
||||
# Significant throughput gain when types annotated
|
||||
```
|
||||
|
||||
### Source map for Hermes bundle
|
||||
```bash
|
||||
react-native bundle \
|
||||
--platform android \
|
||||
--dev false \
|
||||
--entry-file index.js \
|
||||
--bundle-output index.android.bundle \
|
||||
--sourcemap-output index.android.bundle.map \
|
||||
--minify true
|
||||
# Hermes ingests .map for symbolicated stack traces
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Engine |
|
||||
|---|---|
|
||||
| 매 RN app | Hermes (default) |
|
||||
| 매 cold start critical | Hermes |
|
||||
| 매 heavy compute | JSC + worker / native module |
|
||||
| 매 typed perf-critical RN | Static Hermes (experimental) |
|
||||
| 매 web | V8 / JSC (browser native) |
|
||||
|
||||
**기본값**: Hermes ON. 매 RN 0.70+ 자동.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React Native]]
|
||||
- 변형: [[V8]] · [[JavaScriptCore]]
|
||||
- Adjacent: [[Bytecode]] · [[GC]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 RN startup 분석, 매 bundle size 최적화, 매 JS engine trade-off.
|
||||
**언제 X**: 매 web — 매 browser engine 직접 제어 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **eval 의존**: 매 JIT 없음 — 매 매우 느림.
|
||||
- **Bundle 비분할**: 매 .hbc 1개 거대 — 매 RAM bundle / inline-requires 활용.
|
||||
- **JSC 가정 코드**: 매 Symbol toPrimitive 등 매 corner case.
|
||||
- **Hermes 끄고 RN**: 매 거의 매 lose-lose (2026 기준).
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (hermesengine.dev, RN 0.74+ release notes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 Hermes architecture + RN integration |
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Hermes Engine
|
||||
description: "Hermes는 React Native 애플리케이션에서 자바스크립트 로직을 실행하는 데 사용되는 엔진이다 [1, 2]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Hermes Engine
|
||||
|
||||
## 📌 Brief Summary
|
||||
Hermes는 React Native 애플리케이션에서 자바스크립트 로직을 실행하는 데 사용되는 엔진이다 [1, 2]. 고도로 최적화된 환경을 제공하며, React Native의 새로운 브릿지리스 아키텍처의 핵심인 JSI(JavaScript Interface)와 완벽하게 호환되어 작동한다 [3].
|
||||
|
||||
## 📖 Core Content
|
||||
* **애플리케이션 로직 실행:** React Native는 기본적으로 Hermes와 같은 자바스크립트 코어 엔진을 탑재하여 애플리케이션의 내부 로직을 실행하고, 실제 렌더링은 네이티브 구성 요소를 호출하여 처리하는 아키텍처를 따른다 [1, 2, 4].
|
||||
* **신규 아키텍처(JSI) 호환:** React Native의 새로운 아키텍처를 구성하는 필수 요소인 JSI(JavaScript Interface)는 고도로 최적화된 Hermes 엔진을 포함한 다양한 자바스크립트 엔진을 지원하여 자바스크립트와 네이티브 간의 효율적인 통신을 돕는다 [3].
|
||||
* **앱 크기(App Size) 기준점 형성:** React Native 앱을 배포할 때 Hermes 자바스크립트 엔진이 함께 포함되어 제공되며, 이로 인해 최소 앱 크기(baseline APK size)는 대략 5~8MB 수준을 구성하게 된다 [1].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **초기 구동 지연(Startup Latency):** 앱이 콜드 스타트(Cold Start)를 할 때 Hermes 자바스크립트 엔진을 초기화하고 JS 번들을 파싱하는 과정을 반드시 거쳐야 한다 [1]. 이로 인해 코드를 직접 기계어로 사전 컴파일(AOT)하는 프레임워크(예: Flutter)와 비교했을 때, 앱 실행 시 대략 100~300 밀리초(ms) 정도의 측정 가능한 지연 시간이 추가로 발생한다는 단점이 있다 [1].
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
id: wiki-2026-0508-hydration-mismatch-and-ssr-debug
|
||||
title: Hydration Mismatch and SSR Debugging
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Hydration Mismatch, SSR Debug, Hydration Error]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, ssr, hydration, debugging]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-next
|
||||
---
|
||||
|
||||
# Hydration Mismatch and SSR Debugging
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 server-rendered HTML 과 매 client first render 가 매 다르면 매 React 가 매 throw — 매 일관된 input 이 매 핵심."**. 매 typical 원인: Date.now / Math.random / window 접근 / locale / 매 third-party DOM 조작. React 19 부터 매 error message 가 매 어떤 attribute 가 매 mismatch 인지 매 정확히 표시.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 원인 분류
|
||||
- **Time / random**: server 의 매 시각 ≠ client 의 매 시각.
|
||||
- **Locale / timezone**: Intl.DateTimeFormat 의 매 다른 결과.
|
||||
- **Window / document**: server 에 매 없음.
|
||||
- **User-agent branching**: useragent 의 매 다른 처리.
|
||||
- **Third-party script**: AdSense, Cookiebot 의 매 DOM 변경.
|
||||
- **Browser extension**: Grammarly, Dark Reader 의 매 inject.
|
||||
- **Conditional rendering on `typeof window`**: 매 안티패턴.
|
||||
|
||||
### 매 React 의 매 행동 (19)
|
||||
- 매 mismatch detection — 매 server HTML 을 매 polluted attribute 로 매 표시.
|
||||
- 매 partial recovery — 매 mismatch boundary 만 매 client re-render.
|
||||
- 매 보존되는 attribute (data-, aria-, custom): 매 dev warning.
|
||||
|
||||
### 매 Debug 도구
|
||||
- **Next.js**: `?_rsc_no_cache` 로 매 server payload 확인. `next dev` 의 매 colored diff.
|
||||
- **React DevTools**: Profiler — hydration milestone.
|
||||
- **Browser**: 매 view-source vs DOM inspect 비교.
|
||||
|
||||
### 매 응용
|
||||
1. e-commerce — 매 product price (locale), cart count (cookie).
|
||||
2. Auth — 매 logged-in / logged-out 분기.
|
||||
3. A/B test — 매 server vs client variant 의 매 일치.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Detect mismatch source (Next.js 15)
|
||||
```tsx
|
||||
// Bad — Date.now() differs
|
||||
function Now() { return <span>{Date.now()}</span>; }
|
||||
|
||||
// Good — pass server time as prop, render same on both
|
||||
function Now({ serverTime }: { serverTime: number }) {
|
||||
return <span suppressHydrationWarning>{new Date(serverTime).toISOString()}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
### useEffect for client-only side effects
|
||||
```tsx
|
||||
function ClientGreeting() {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
useEffect(() => setName(localStorage.getItem('name')), []);
|
||||
if (!name) return null; // server returns null, client fills after mount
|
||||
return <p>Hi {name}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
### dynamic() with ssr: false (Next.js)
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic';
|
||||
const Chart = dynamic(() => import('./Chart'), { ssr: false });
|
||||
// Renders nothing on server, only on client
|
||||
```
|
||||
|
||||
### Avoid Math.random in render
|
||||
```tsx
|
||||
// Bad
|
||||
function Card() { return <div data-id={Math.random()}>...</div>; }
|
||||
|
||||
// Good — useId
|
||||
function Card() { const id = useId(); return <div id={id}>...</div>; }
|
||||
```
|
||||
|
||||
### suppressHydrationWarning (last resort)
|
||||
```tsx
|
||||
<time dateTime={iso} suppressHydrationWarning>
|
||||
{new Intl.DateTimeFormat('ko').format(new Date(iso))}
|
||||
</time>
|
||||
```
|
||||
|
||||
### Diagnose third-party DOM mutation
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
const obs = new MutationObserver(records => {
|
||||
for (const r of records) {
|
||||
console.warn('mutation', r.target, r.attributeName, r.addedNodes);
|
||||
}
|
||||
});
|
||||
obs.observe(document.body, { subtree: true, childList: true, attributes: true });
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Locale-stable rendering
|
||||
```tsx
|
||||
// Server and client must agree — pass timezone explicitly
|
||||
const fmt = new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul' });
|
||||
return <span>{fmt.format(new Date(props.iso))}</span>;
|
||||
```
|
||||
|
||||
### Replay server HTML offline
|
||||
```bash
|
||||
# Capture server HTML
|
||||
curl -s https://app.example.com/page > server.html
|
||||
# Open in fresh browser, compare with hydrated DOM via outerHTML diff
|
||||
```
|
||||
|
||||
### React 19 onRecoverableError
|
||||
```tsx
|
||||
hydrateRoot(document, <App />, {
|
||||
onRecoverableError(err, info) {
|
||||
fetch('/log/hydration-error', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ msg: String(err), digest: info.digest }),
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 원인 | Fix |
|
||||
|---|---|
|
||||
| 매 time / random | useId / passed-down server value |
|
||||
| 매 locale / TZ | 매 explicit Intl 의 매 timeZone |
|
||||
| 매 window / localStorage | 매 useEffect 후 setState |
|
||||
| 매 SSR 무가치 component | 매 dynamic ssr:false |
|
||||
| 매 third-party inject | 매 suppressHydrationWarning + 매 root 외부 |
|
||||
| 매 extension (Grammarly) | 매 무시 — 매 알려진 false positive |
|
||||
|
||||
**기본값**: 매 server / client 를 매 동일 input 으로 매 강제. 매 last resort suppressHydrationWarning.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Hydration]] · [[SSR]]
|
||||
- 변형: [[Selective Hydration]] · [[Streaming SSR]]
|
||||
- 응용: [[Remix]] · [[Astro]]
|
||||
- Adjacent: [[useEffect]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 hydration error stack 분석, 매 SSR 코드 review, 매 timezone bug 진단.
|
||||
**언제 X**: 매 pure CSR — 매 hydration 자체 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`typeof window !== 'undefined'` in render**: 매 server / client output 차이 — 매 mismatch 보장.
|
||||
- **Date.now() in render**: 매 항상 mismatch.
|
||||
- **suppressHydrationWarning 남발**: 매 진짜 bug 숨김.
|
||||
- **try/catch around hydrateRoot 만**: 매 root cause 추적 X.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 19 docs — Hydration Errors, Next.js 15 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 mismatch 원인 + Next.js 15 debug |
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Hydration (하이드레이션)
|
||||
description: "하이드레이션(Hydration)은 서버에서 렌더링되어 전달된 정적인 HTML('건조한' HTML)에 상호작용성과 이벤트 핸들러라는 '물'을 주어 동적인 웹 페이지로 만드는 과정입니다 [1]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Hydration (하이드레이션)
|
||||
|
||||
## 📌 Brief Summary
|
||||
하이드레이션(Hydration)은 서버에서 렌더링되어 전달된 정적인 HTML('건조한' HTML)에 상호작용성과 이벤트 핸들러라는 '물'을 주어 동적인 웹 페이지로 만드는 과정입니다 [1]. 프론트엔드 프레임워크는 전체 DOM을 새로 다시 그리지 않고, 기존 DOM 트리를 재사용하여 이벤트 리스너를 부착하는 방식으로 작동합니다 [2, 3]. 최근에는 초기 로딩 시간 단축과 상호작용성 개선을 위해 React의 선택적 하이드레이션(Selective Hydration)이나 Vue의 지연 하이드레이션(Lazy Hydration) 같은 최적화 기법이 활용되고 있습니다 [4, 5].
|
||||
|
||||
## 📖 Core Content
|
||||
* **작동 원리 (Mechanism):** 하이드레이션은 페이지를 처음부터 렌더링하는 과정이 아닙니다 [3]. 리액트(React)의 경우, 구축 중인 Fiber 트리와 병렬로 기존의 서버 렌더링 DOM 트리를 순회하면서 각 Fiber를 해당하는 DOM 노드에 일치시키고(`fiber.stateNode`에 참조 저장), 이벤트 리스너를 부착합니다 [3]. 이처럼 **DOM을 완전히 새로 만들지 않고 재사용**하기 때문에 새로운 렌더링(Fresh render)보다 더 빠르게 동작합니다 [3].
|
||||
* **선택적 하이드레이션 (Selective Hydration):** 과거에는 전체 트리가 순차적으로 하이드레이션될 때까지 페이지의 어떤 부분도 상호작용할 수 없는 한계가 있었습니다 [3]. 이를 해결하기 위해 React 18은 **우선순위 기반의 선택적 하이드레이션**을 도입했습니다 [4]. 사용자가 특정 요소를 클릭하면 React는 기존의 하이드레이션 작업을 중단하고, 사용자가 상호작용한 부분을 처리하는 데 필요한 부분만 우선적으로 하이드레이션한 뒤, 나머지 트리의 작업을 재개합니다 [4].
|
||||
* **지연 하이드레이션 (Lazy Hydration):** Vue 3.5 버전에서는 서버 사이드 렌더링(SSR)의 성능 향상을 위해 지연 하이드레이션을 도입했습니다 [5]. 이 기능은 **컴포넌트가 뷰포트(Viewport)에 보일 때만 하이드레이션되도록 허용**하여, 필요할 때까지 컴포넌트의 활성화를 지연시킴으로써 초기 로드 시간을 줄이고 전반적인 애플리케이션 성능을 최적화합니다 [5]. e-커머스 등 콘텐츠가 많은 애플리케이션에서 매우 유용합니다 [6].
|
||||
* **서버 컴포넌트와의 관계:** 리액트 서버 컴포넌트(React Server Components, RSC)는 클라이언트로 자바스크립트 코드가 전송되지 않고 오직 서버에서만 실행되므로 **하이드레이션 과정 자체가 아예 존재하지 않습니다** [7].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **하이드레이션 갭 (Hydration Gap):** SSR을 통해 사용자는 렌더링이 완료된 것처럼 보이는 페이지를 즉시 볼 수 있지만, 자바스크립트가 완전히 다운로드되고 **하이드레이션이 끝나기 전까지는 버튼을 클릭해도 아무런 반응이 없는 '하이드레이션 갭' 현상**이 발생합니다 [2]. 페이지가 상호작용 가능해 보이지만 실제로는 그렇지 않은 시기가 존재합니다 [2].
|
||||
* **클라이언트 병목 현상:** DOM 트리를 재사용하더라도 결국 브라우저는 대량의 자바스크립트 번들을 모두 다운로드하고 실행해야 합니다 [8]. 또한 트리 상단에 있는 단일의 느린 컴포넌트가 트리를 순회하는 하이드레이션 과정을 막고 있다면, 트리의 하단에 있는 버튼의 클릭 핸들러 부착까지 지연되어 **전체 페이지의 응답성이 차단되는 병목**을 초래할 수 있습니다 [3].
|
||||
* **이중 작업 구조 (Two-Trip Problem):** 서버에서 HTML을 그리기 위해 실행했던 작업과 데이터 패칭을, 클라이언트에서 하이드레이션을 진행하며 브라우저가 자바스크립트를 다시 실행하여 중복 수행하게 되는 구조적 비효율성이 존재합니다 [8]. 이러한 서버 렌더링의 근본적인 한계는 결국 클라이언트로 코드를 보내지 않는 '리액트 서버 컴포넌트(RSC)'가 등장하게 된 핵심 배경이 되었습니다 [8, 9].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
id: wiki-2026-0508-ide-stability-fix
|
||||
title: IDE Stability Fix
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [IDE crash fix, VSCode stability, Cursor stability]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [ide, vscode, cursor, debugging, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: VSCode/Cursor
|
||||
---
|
||||
|
||||
# IDE Stability Fix
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 IDE crash / freeze / OOM 의 root cause 는 대부분 extension memory leak, large file indexing, TS server overload"**. 매 2026 의 Electron-based IDE (VSCode, Cursor, Windsurf) — 매 동일 패턴. 매 systematic disable + heap profiling 으로 해결.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 흔한 원인
|
||||
- **Extension memory leak**: 매 disposable 미해제, listener 누적.
|
||||
- **TS Server OOM**: 매 large monorepo (>500k LOC), `--max-old-space-size` 부족.
|
||||
- **File watcher exhaust**: 매 `node_modules` watch → fs.inotify limit.
|
||||
- **Renderer process freeze**: 매 large file (>10MB) 또는 minified bundle 열기.
|
||||
- **GPU process crash**: 매 macOS Metal driver 충돌.
|
||||
|
||||
### 매 진단 도구
|
||||
- `Developer: Open Process Explorer` (VSCode)
|
||||
- `--inspect-extensions=9229` + Chrome DevTools
|
||||
- `code --status` — running extensions + memory.
|
||||
- macOS Activity Monitor — Code Helper (Renderer) 의 RAM 추적.
|
||||
|
||||
### 매 응용
|
||||
1. Monorepo 의 TS server tuning.
|
||||
2. AI extension (Copilot, Cursor) leak 진단.
|
||||
3. WSL2 / Remote SSH 환경 stability.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### TS Server memory raise
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"typescript.tsserver.maxTsServerMemory": 8192,
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
|
||||
"typescript.disableAutomaticTypeAcquisition": true
|
||||
}
|
||||
```
|
||||
|
||||
### File watcher exclude
|
||||
```json
|
||||
{
|
||||
"files.watcherExclude": {
|
||||
"**/node_modules/**": true,
|
||||
"**/.git/objects/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/.next/**": true,
|
||||
"**/target/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bisect extensions
|
||||
```bash
|
||||
# 매 extension 중 어떤 것이 crash 원인인지 binary search
|
||||
code --disable-extensions # 매 모두 disable → 재현 X = extension 문제
|
||||
# Help → Start Extension Bisect 로 자동 binary search
|
||||
```
|
||||
|
||||
### Heap snapshot 분석
|
||||
```bash
|
||||
# 매 extension host heap snapshot
|
||||
# Cmd+Shift+P → "Developer: Take Process Heap Snapshot"
|
||||
# Chrome DevTools 에서 .heapsnapshot 열어 분석
|
||||
```
|
||||
|
||||
### Linux file watcher limit
|
||||
```bash
|
||||
# inotify limit raise (default 8192 매 부족)
|
||||
echo fs.inotify.max_user_watches=524288 | \
|
||||
sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
### Disable GPU acceleration (macOS crash)
|
||||
```bash
|
||||
# 매 Metal driver issue → software rendering
|
||||
code --disable-gpu
|
||||
# 또는 settings.json
|
||||
"window.experimental.useSandbox": false
|
||||
```
|
||||
|
||||
### Cursor / AI extension throttle
|
||||
```json
|
||||
{
|
||||
"cursor.cpp.disabledLanguages": ["plaintext", "markdown"],
|
||||
"github.copilot.editor.enableAutoCompletions": true,
|
||||
"github.copilot.advanced": { "length": 500 }
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 증상 | 첫 시도 |
|
||||
|---|---|
|
||||
| TS Server OOM | maxTsServerMemory 8GB |
|
||||
| 전체 freeze | --disable-extensions bisect |
|
||||
| GPU artifact | --disable-gpu |
|
||||
| File watch exhaust | watcherExclude + inotify limit |
|
||||
| Indexing 끝없음 | search.exclude + remove large dirs |
|
||||
|
||||
**기본값**: settings.json 의 watcherExclude + maxTsServerMemory 부터 시작.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Cursor IDE]]
|
||||
- 응용: [[Monorepo|Monorepo Setup]] · [[Large-scale Application Refactoring]]
|
||||
- Adjacent: [[TypeScript Performance]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: IDE crash log 분석, settings.json tuning, extension 충돌 진단.
|
||||
**언제 X**: 매 일반 app crash — 매 Electron-specific 패턴 만.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **무작정 reinstall**: 매 cause 찾지 않음 — 매 재발.
|
||||
- **Disable all extensions 영구**: 매 productivity 손실 — 매 bisect 후 specific 만 disable.
|
||||
- **Ignore log**: 매 `~/Library/Logs/Cursor/` 또는 `code --status` 가 직접적 단서.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (VSCode docs, Cursor support forum, GitHub issue tracker patterns).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — IDE crash 진단 + 7 fix patterns |
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
category: Frontend
|
||||
tags: [auto-wikified, technical-documentation, frontend]
|
||||
title: Impeller
|
||||
description: "Impeller는 모바일 개발 프레임워크 Flutter에서 기존의 Skia 엔진을 대체하기 위해 새롭게 도입된 자체 그래픽 렌더링 엔진이다 [1-3]."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Impeller
|
||||
|
||||
## 📌 Brief Summary
|
||||
Impeller는 모바일 개발 프레임워크 Flutter에서 기존의 Skia 엔진을 대체하기 위해 새롭게 도입된 자체 그래픽 렌더링 엔진이다 [1-3]. 기존 런타임 환경에서 셰이더(Shader)를 컴파일할 때 발생하던 프레임 끊김(Jank) 문제를 해결하기 위해, 빌드 시점에 셰이더를 미리 컴파일하는 방식을 채택했다 [2, 4, 5]. 이를 통해 첫 프레임부터 일관되고 부드러운 UI 렌더링과 최적화된 성능을 제공하는 것이 핵심적인 특징이다 [4, 5].
|
||||
|
||||
## 📖 Core Content
|
||||
* **셰이더 컴파일 최적화 및 Jank 문제 해결**: 과거 Flutter의 기본 그래픽 엔진이었던 Skia는 사용자가 앱과 상호작용할 때 런타임에서 UI를 네이티브 코드로 컴파일해야 했고, 이로 인해 처음 복잡한 애니메이션이 실행될 때 심각한 버벅임(Shader compilation jank)이 발생했다 [2, 3, 5]. Impeller는 애플리케이션 빌드 단계에서 더 작고 단순하며 최적화된 셰이더 세트를 미리 컴파일(Pre-compile)해 두어 이러한 렌더링 병목 현상을 근본적으로 제거한다 [4-6].
|
||||
* **자체 렌더링 철학 유지**: React Native처럼 플랫폼의 네이티브 컴포넌트(iOS의 UIKit 등)에 의존하지 않고, Impeller를 통해 화면의 모든 픽셀을 직접 그린다 [7-9]. 이를 통해 플랫폼에 구애받지 않는 일관된 커스텀 위젯과 '픽셀 퍼펙트(Pixel-perfect)' 룩앤필을 구현할 수 있다 [7, 10].
|
||||
* **최신 모바일 GPU 최적화**: Impeller는 테셀레이션(Tessellation) 기반 렌더링 방식을 사용하여 최신 모바일 GPU에 맞춰 설계되었다 [5]. 또한 Metal, Vulkan과 같은 고급 GPU API를 활용하도록 만들어져 더 낮은 전력 소비와 효율적인 렌더링 효율성을 자랑한다 [3].
|
||||
* **극대화된 애니메이션 성능**: 셰이더가 이미 준비된 상태로 앱이 실행되기 때문에, 고주사율 기기에서도 일관된 60fps 또는 120fps의 프레임 속도를 매끄럽게 보장한다 [4, 11, 12]. 이로 인해 시각적으로 복잡한 전환 및 애니메이션 구현에 탁월한 강점을 지닌다 [4].
|
||||
* **진행 및 성숙도**: 2023년 Flutter 3.10 버전부터 iOS 환경에서는 기본 렌더러로 안정화되어 프로덕션 레벨에 투입되었으며 [5, 13], Android 환경에서도 지속적인 발전을 거치며 적용을 확대하고 있다 [2, 5, 11, 13]. 향후 업데이트에서는 커스텀 셰이더 지원 및 3D 지원 기능까지 확장될 계획이다 [14].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
* **기본 앱 용량(APK Size) 증가**: 기기에 앱을 배포할 때 Dart 런타임과 함께 자체 Impeller 렌더링 엔진 자체가 파일에 포함되어야 한다 [12]. 이로 인해 최소 앱 크기가 약 8~12MB 정도로 형성되며, 네이티브 뷰를 직접 활용하여 별도의 렌더링 엔진 탑재가 필요 없는 프레임워크(예: React Native의 5~8MB)에 비해 베이스라인 파일 크기가 상대적으로 크다 [12].
|
||||
* **메모리 사용량 오버헤드**: 플랫폼의 네이티브 UI 컴포넌트를 사용하지 않고 독자적인 위젯 트리, 레이어 트리 및 Impeller 렌더링 파이프라인을 유지해야 하므로 기본적으로 메모리 오버헤드가 발생한다 [15]. 대다수 앱에서 20~50MB 정도의 메모리 차이가 나며, 플래그십 기기에서는 무시할 만한 수준이지만 저사양 기기에서는 유의미한 단점이 될 수 있다 [15].
|
||||
* **플랫폼별 도입 속도의 불균형**: iOS 생태계에서는 이미 문제점들이 해결되어 기본값으로 훌륭히 작동하고 있으나, Android 플랫폼의 경우 상대적으로 프리뷰 단계에서 출발하여 빠르게 성숙해 나가는 과정에 있다 [5, 11]. 따라서 플랫폼 간 Impeller의 최적화 수준이나 적용 시기에 일시적인 과도기적 불균형이 존재할 수 있다 [5, 13].
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: wiki-2026-0508-impeller-engine
|
||||
title: Impeller Engine
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Impeller, Flutter Impeller, Skia Replacement]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [flutter, rendering, gpu, metal, vulkan]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: dart
|
||||
framework: flutter
|
||||
---
|
||||
|
||||
# Impeller Engine
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 shader compilation jank 의 제거 — precompiled, predictable"**. Flutter 의 새 rendering engine, Skia 의 replacement. 매 first-frame shader hitch (iOS 특히) 의 근본 해결 — 매 shader 를 build-time 에 precompile 하여 runtime JIT 의 회피.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Skia 의 문제
|
||||
- Skia 의 SkSL shader 는 매 runtime 에 platform 별 (Metal MSL / Vulkan SPIR-V) 로 compile.
|
||||
- 매 first encounter 시 ms-단위 stall — janky animation onset.
|
||||
- iOS 의 특히 심각 (Metal pipeline state object 의 cost).
|
||||
|
||||
### 매 Impeller 의 솔루션
|
||||
- **Ahead-of-time shader compilation**: build 시 모든 shader 를 platform IR 로 변환.
|
||||
- **Predictable performance**: runtime 에 매 shader compile 의 X — frame budget 안정.
|
||||
- **Tessellation**: GPU-friendly geometry pipeline (path → triangles 의 CPU offload).
|
||||
- **Backend**: iOS Metal (stable, Flutter 3.10+ default), Android Vulkan (stable, 3.27+ default), macOS Metal.
|
||||
|
||||
### 매 응용
|
||||
1. iOS Flutter 앱 의 매 launch animation jank 제거.
|
||||
2. 매 complex path animation (Lottie, custom painters) 의 안정 frame rate.
|
||||
3. 매 game-like Flutter UI 의 60/120Hz 유지.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Impeller 활성화 확인 (iOS)
|
||||
```dart
|
||||
// ios/Runner/Info.plist
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
### Custom shader (Impeller-compatible)
|
||||
```glsl
|
||||
// shaders/wave.frag
|
||||
#version 460 core
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
uniform vec2 uSize;
|
||||
uniform float uTime;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec2 uv = FlutterFragCoord().xy / uSize;
|
||||
float wave = sin(uv.x * 10.0 + uTime) * 0.5 + 0.5;
|
||||
fragColor = vec4(wave, uv.y, 1.0 - wave, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# pubspec.yaml
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/wave.frag
|
||||
```
|
||||
|
||||
```dart
|
||||
// 매 build-time precompiled, 매 runtime jank 의 X
|
||||
final program = await FragmentProgram.fromAsset('shaders/wave.frag');
|
||||
final shader = program.fragmentShader()
|
||||
..setFloat(0, size.width)
|
||||
..setFloat(1, size.height)
|
||||
..setFloat(2, time);
|
||||
canvas.drawRect(rect, Paint()..shader = shader);
|
||||
```
|
||||
|
||||
### Performance overlay
|
||||
```dart
|
||||
MaterialApp(
|
||||
showPerformanceOverlay: true, // 매 GPU/UI thread frame time 의 visual
|
||||
home: MyApp(),
|
||||
);
|
||||
```
|
||||
|
||||
### Disable Impeller (debugging)
|
||||
```bash
|
||||
flutter run --no-enable-impeller
|
||||
```
|
||||
|
||||
### CustomPainter — Impeller 의 fast path
|
||||
```dart
|
||||
class WavePainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final path = Path();
|
||||
for (double x = 0; x <= size.width; x += 2) {
|
||||
path.lineTo(x, sin(x * 0.05) * 20 + size.height / 2);
|
||||
}
|
||||
// 매 Impeller tessellator 가 GPU 친화 triangle 로 변환
|
||||
canvas.drawPath(path, Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2);
|
||||
}
|
||||
@override bool shouldRepaint(_) => true;
|
||||
}
|
||||
```
|
||||
|
||||
### Backdrop blur (Impeller 의 optimized)
|
||||
```dart
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), // 매 Impeller GPU blur
|
||||
child: Container(color: Colors.black.withOpacity(0.3)),
|
||||
)
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Flutter 3.10+ iOS | Impeller default 유지 |
|
||||
| Flutter 3.27+ Android | Impeller (Vulkan) default 유지 |
|
||||
| 매 legacy device (Android API <29) | Skia fallback 자동 |
|
||||
| Custom shader 사용 | Impeller 의 IR precompile 활용 |
|
||||
| Engine bug 의심 | `--no-enable-impeller` 로 A/B |
|
||||
|
||||
**기본값**: 매 Impeller 활성화 유지 (Flutter 3.27+).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Flutter]]
|
||||
- 변형: [[Skia]] · [[Metal]] · [[Vulkan]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Flutter 앱 의 jank 진단, custom shader 작성, iOS/Android rendering 차이 디버깅.
|
||||
**언제 X**: web target (Flutter Web 은 CanvasKit/Skia), 매 Skia-specific API 의존 코드.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **runtime shader string compile**: Impeller 의 AOT 의 우회 — jank 재발.
|
||||
- **CustomPainter shouldRepaint=true 남발**: 매 frame 의 unnecessary repaint.
|
||||
- **legacy SKSL caching workaround 유지**: Impeller 환경에서 의미 없음, 코드 cleanup 필요.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Flutter docs `flutter.dev/perf/impeller`, Flutter 3.27 release notes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Impeller engine architecture 정리 |
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
id: wiki-2026-0508-indirect-draw
|
||||
title: Indirect Draw
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Indirect Drawing, GPU-Driven Rendering, drawIndirect]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [graphics, gpu, webgpu, vulkan, rendering, performance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: wgsl
|
||||
framework: webgpu
|
||||
---
|
||||
|
||||
# Indirect Draw
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 indirect draw 는 draw call args 의 GPU buffer 의 read — CPU roundtrip 없이 GPU 의 self-dispatch"**. 2026 의 GPU-driven rendering pipeline 의 foundation: Vulkan/D3D12/Metal/WebGPU 의 support. 매 culling, LOD, instancing 의 GPU 에서 결정 → CPU draw-call overhead 의 elimination.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 vs Direct Draw
|
||||
- **Direct**: `draw(vertexCount, instanceCount, firstVertex, firstInstance)` — args from CPU.
|
||||
- **Indirect**: `drawIndirect(buffer, offset)` — args read from GPU buffer.
|
||||
- **Multi-draw indirect (MDI)**: thousands of draws from one CPU command.
|
||||
|
||||
### 매 Args Layout (WebGPU)
|
||||
```
|
||||
struct DrawIndirectArgs {
|
||||
vertexCount: u32,
|
||||
instanceCount: u32,
|
||||
firstVertex: u32,
|
||||
firstInstance: u32,
|
||||
}
|
||||
struct DrawIndexedIndirectArgs {
|
||||
indexCount: u32,
|
||||
instanceCount: u32,
|
||||
firstIndex: u32,
|
||||
baseVertex: i32,
|
||||
firstInstance: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### 매 Pipeline (GPU-driven)
|
||||
1. Compute shader: per-object frustum/occlusion cull → write visible list.
|
||||
2. Compute shader: write indirect args buffer (instanceCount=0 for culled).
|
||||
3. `drawIndexedIndirect` (or MDI) reads buffer → renders only visible.
|
||||
|
||||
### 매 응용
|
||||
1. Massive instanced scenes (foliage, crowds, particles).
|
||||
2. GPU-driven culling (frustum, occlusion via Hi-Z).
|
||||
3. LOD selection on GPU.
|
||||
4. Variable-rate / batched rendering (cluster culling, Nanite-style).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### WebGPU Indirect Draw Setup
|
||||
```ts
|
||||
// Args buffer (visible after compute)
|
||||
const indirectBuffer = device.createBuffer({
|
||||
size: 16, // 4 u32
|
||||
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
// Initialize: 36 verts, 1000 instances, offset 0/0
|
||||
device.queue.writeBuffer(indirectBuffer, 0,
|
||||
new Uint32Array([36, 1000, 0, 0]));
|
||||
|
||||
// In render pass
|
||||
pass.setPipeline(pipeline);
|
||||
pass.setVertexBuffer(0, vertices);
|
||||
pass.drawIndirect(indirectBuffer, 0);
|
||||
```
|
||||
|
||||
### Culling Compute Shader (WGSL)
|
||||
```wgsl
|
||||
struct DrawArgs { vertexCount: u32, instanceCount: u32,
|
||||
firstVertex: u32, firstInstance: u32 }
|
||||
|
||||
@group(0) @binding(0) var<storage, read> objects: array<Object>;
|
||||
@group(0) @binding(1) var<storage, read_write> drawArgs: DrawArgs;
|
||||
@group(0) @binding(2) var<storage, read_write> visibleInstances: array<u32>;
|
||||
@group(0) @binding(3) var<uniform> camera: Camera;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn cullCS(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let i = gid.x;
|
||||
if (i >= arrayLength(&objects)) { return; }
|
||||
let obj = objects[i];
|
||||
if (frustumTest(obj.bounds, camera.frustum)) {
|
||||
let slot = atomicAdd(&drawArgs.instanceCount, 1u);
|
||||
visibleInstances[slot] = i;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reset Pass (clear instanceCount)
|
||||
```ts
|
||||
// Each frame, before culling, zero out instanceCount
|
||||
device.queue.writeBuffer(indirectBuffer, 4, new Uint32Array([0]));
|
||||
```
|
||||
|
||||
### Multi-Draw Indirect (Vulkan)
|
||||
```cpp
|
||||
// Draw N different meshes from one buffer
|
||||
vkCmdDrawIndexedIndirect(cmd, indirectBuf, 0,
|
||||
/*drawCount*/ N,
|
||||
/*stride*/ sizeof(VkDrawIndexedIndirectCommand));
|
||||
|
||||
// Or with count buffer (drawCount is itself on GPU)
|
||||
vkCmdDrawIndexedIndirectCount(cmd, indirectBuf, 0,
|
||||
countBuf, 0, /*maxDraws*/ N,
|
||||
sizeof(VkDrawIndexedIndirectCommand));
|
||||
```
|
||||
|
||||
### Three.js (R175+ has WebGPU)
|
||||
```js
|
||||
import { WebGPURenderer, BatchedMesh } from 'three';
|
||||
const renderer = new WebGPURenderer();
|
||||
// BatchedMesh internally uses indirect draw + instancing
|
||||
const batched = new BatchedMesh(maxInstances, maxVerts, maxIndices);
|
||||
batched.addGeometry(geom1);
|
||||
batched.addGeometry(geom2);
|
||||
// One draw call, GPU handles per-instance state
|
||||
```
|
||||
|
||||
### Hi-Z Occlusion Culling (sketch)
|
||||
```wgsl
|
||||
// Sample Hi-Z mip — fastest mip where bounding sphere covers >1 texel
|
||||
fn occluded(bsphere: vec4<f32>) -> bool {
|
||||
let screenRect = projectToScreen(bsphere);
|
||||
let mip = computeMip(screenRect);
|
||||
let depth = textureSampleLevel(hiZ, samp, screenRect.center, mip).r;
|
||||
return bsphereMinDepth(bsphere) > depth;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| <100 unique objects | Direct draw / instancing — overhead 의 not worth |
|
||||
| 1k-1M instances | Indirect draw + GPU cull |
|
||||
| Many distinct meshes | Multi-draw indirect (Vulkan/D3D12); WebGPU 의 batched |
|
||||
| Foliage/crowd | Indirect + GPU LOD selection |
|
||||
| Mobile / low-end | Direct draw (compute overhead 의 watch) |
|
||||
|
||||
**기본값**: large dynamic scene 의 GPU-driven indirect pipeline. Small scene 의 direct draw.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Graphics Pipeline]]
|
||||
- 변형: [[GPU-Driven Rendering]]
|
||||
- 응용: [[Frustum Culling]] · [[Nanite]]
|
||||
- Adjacent: [[WebGPU]] · [[Vulkan]] · [[Compute Shader]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: GPU-driven pipeline 의 design, culling 의 implement, draw-call overhead 의 reduce.
|
||||
**언제 X**: simple scene 의 indirect draw 의 over-engineering — direct 의 fine.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **CPU readback of indirect buffer**: 매 stall. GPU 의 self-contained 의 keep.
|
||||
- **Per-frame full buffer rewrite**: defeats purpose. 매 GPU compute 의 update.
|
||||
- **No Hi-Z for occlusion**: false positives — Hi-Z 또는 conservative AABB 의 사용.
|
||||
- **Indirect for tiny scenes**: compute dispatch overhead > savings.
|
||||
- **WebGL fallback assumed**: WebGL 의 no indirect draw — WebGPU required.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (WebGPU spec, Vulkan spec, GPU Gems / Activision Nanite paper).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — indirect draw / GPU-driven rendering full content |
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: wiki-2026-0508-instancing
|
||||
title: Instancing
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [GPU instancing, instanced rendering, drawInstanced]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [graphics, gpu, webgl, webgpu, threejs, performance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: GLSL/WGSL
|
||||
framework: Three.js/WebGPU
|
||||
---
|
||||
|
||||
# Instancing
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 동일 mesh 를 N 번 그릴 때 single draw call 로 묶는 GPU 기법 — 매 per-instance attribute (transform, color) 만 다르게"**. 매 thousands → millions of objects 가 60fps 로 가능. 매 grass, particles, crowd, foliage 의 핵심. WebGL 2 / WebGPU / Vulkan / Metal 모두 native 지원.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 왜 빠른가
|
||||
- **Draw call overhead 제거**: 매 CPU→GPU command 1번 만.
|
||||
- **Vertex buffer reuse**: 매 base mesh 1개, instance attr 만 streaming.
|
||||
- **GPU parallelism**: 매 instance 가 각 SM/CU 에 분산.
|
||||
|
||||
### 매 per-instance attribute 종류
|
||||
- **Transform matrix** (mat4) — 매 가장 흔함.
|
||||
- **Color / tint** (vec4)
|
||||
- **Texture index** / atlas UV offset
|
||||
- **Animation frame** (skinned crowd)
|
||||
|
||||
### 매 응용
|
||||
1. Three.js `InstancedMesh` — 매 50k tree, 100k particle.
|
||||
2. Unreal HISM / Niagara, Unity GPU Instancer.
|
||||
3. WebGPU compute-driven instancing — 매 frustum culling on GPU.
|
||||
4. Game KvK map — 매 thousands of city/troop sprite.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Three.js InstancedMesh
|
||||
```javascript
|
||||
import * as THREE from "three";
|
||||
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 });
|
||||
const count = 10000;
|
||||
const mesh = new THREE.InstancedMesh(geometry, material, count);
|
||||
|
||||
const m = new THREE.Matrix4();
|
||||
for (let i = 0; i < count; i++) {
|
||||
m.setPosition(
|
||||
(Math.random() - 0.5) * 100,
|
||||
0,
|
||||
(Math.random() - 0.5) * 100,
|
||||
);
|
||||
mesh.setMatrixAt(i, m);
|
||||
}
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
scene.add(mesh);
|
||||
```
|
||||
|
||||
### Per-instance color
|
||||
```javascript
|
||||
const colors = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
colors[i * 3] = Math.random();
|
||||
colors[i * 3 + 1] = Math.random();
|
||||
colors[i * 3 + 2] = Math.random();
|
||||
}
|
||||
geometry.setAttribute(
|
||||
"instanceColor",
|
||||
new THREE.InstancedBufferAttribute(colors, 3),
|
||||
);
|
||||
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.vertexShader = shader.vertexShader
|
||||
.replace("#include <common>", "#include <common>\nattribute vec3 instanceColor; varying vec3 vColor;")
|
||||
.replace("#include <begin_vertex>", "#include <begin_vertex>\nvColor = instanceColor;");
|
||||
shader.fragmentShader = shader.fragmentShader
|
||||
.replace("#include <common>", "#include <common>\nvarying vec3 vColor;")
|
||||
.replace("vec4 diffuseColor = vec4( diffuse, opacity );", "vec4 diffuseColor = vec4( diffuse * vColor, opacity );");
|
||||
};
|
||||
```
|
||||
|
||||
### Update single instance (game troop move)
|
||||
```javascript
|
||||
function updateTroop(idx: number, x: number, z: number) {
|
||||
m.setPosition(x, 0, z);
|
||||
mesh.setMatrixAt(idx, m);
|
||||
mesh.instanceMatrix.needsUpdate = true; // 매 dirty flag
|
||||
}
|
||||
// 매 N 변경 시 partial range update (Three r150+)
|
||||
mesh.instanceMatrix.addUpdateRange(start * 16, count * 16);
|
||||
```
|
||||
|
||||
### WebGPU instanced draw
|
||||
```javascript
|
||||
// WGSL vertex shader
|
||||
const wgsl = `
|
||||
struct Instance { @location(3) m0: vec4f, @location(4) m1: vec4f, @location(5) m2: vec4f, @location(6) m3: vec4f };
|
||||
@vertex fn vs(@location(0) pos: vec3f, instance: Instance) -> @builtin(position) vec4f {
|
||||
let model = mat4x4f(instance.m0, instance.m1, instance.m2, instance.m3);
|
||||
return uniforms.viewProj * model * vec4f(pos, 1.0);
|
||||
}`;
|
||||
|
||||
// JS — drawIndexedIndirect 또는 draw with instanceCount
|
||||
pass.draw(vertexCount, instanceCount, 0, 0);
|
||||
```
|
||||
|
||||
### GPU frustum culling (compute → instanced draw)
|
||||
```wgsl
|
||||
@compute @workgroup_size(64)
|
||||
fn cull(@builtin(global_invocation_id) gid: vec3u) {
|
||||
let idx = gid.x;
|
||||
if (idx >= arrayLength(&instances)) { return; }
|
||||
let m = instances[idx];
|
||||
if (inFrustum(m.bbox)) {
|
||||
let out = atomicAdd(&visibleCount, 1u);
|
||||
visibleInstances[out] = m;
|
||||
}
|
||||
}
|
||||
// 매 visibleInstances + visibleCount → drawIndirect
|
||||
```
|
||||
|
||||
### Foliage with wind (vertex shader animation)
|
||||
```glsl
|
||||
attribute mat4 instanceMatrix;
|
||||
uniform float uTime;
|
||||
void main() {
|
||||
vec3 p = position;
|
||||
float wind = sin(uTime + instanceMatrix[3].x * 0.1) * 0.1 * p.y;
|
||||
p.x += wind;
|
||||
gl_Position = projectionMatrix * viewMatrix * instanceMatrix * vec4(p, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Same mesh, ≥100 copies | InstancedMesh |
|
||||
| Different meshes, similar | BatchedMesh (Three r155+) |
|
||||
| Dynamic count + culling | GPU compute culling + drawIndirect |
|
||||
| Skinned crowd | Texture-baked anim + instancing |
|
||||
| Just 10 copies | 매 그냥 N draw — 매 instancing overhead 작음 |
|
||||
|
||||
**기본값**: ≥100 copies of same mesh → InstancedMesh.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Draw Call Optimization]]
|
||||
- 변형: [[BatchedMesh]] · [[Indirect Drawing]]
|
||||
- 응용: [[Three.js]] · [[WebGPU]]
|
||||
- Adjacent: [[Frustum Culling]] · [[LOD]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: large-scale rendering 설계, particle / foliage / crowd 구현.
|
||||
**언제 X**: 매 single object, 매 매우 다른 mesh — 매 instancing overhead 정당화 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **needsUpdate = true on every frame**: 매 모든 instance 변경 안 했어도 전체 upload — 매 partial update range 사용.
|
||||
- **instancing for unique meshes**: 매 효과 X — 매 BatchedMesh 또는 multi-draw indirect.
|
||||
- **CPU-side culling per instance**: 매 GPU compute 가 더 빠름 (10k+).
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Three.js docs, WebGPU spec, Real-Time Rendering 4th ed.).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Three.js + WebGPU instancing 패턴 |
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
id: wiki-2026-0508-jsi-javascript-interface
|
||||
title: JSI (JavaScript Interface)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Native JSI, JavaScript Interface, RN New Architecture]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react-native, jsi, performance, native-modules]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: cpp
|
||||
framework: react-native
|
||||
---
|
||||
|
||||
# JSI (JavaScript Interface)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 bridge 의 JSON serialization 의 제거 — 매 sync, direct C++ binding"**. React Native 의 New Architecture 의 foundation. 매 native function 을 C++ 객체로 expose, JS 가 매 sync 호출 + shared memory 접근. 매 old bridge 의 async-only / serialize-everything 의 근본 해결.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 old bridge 문제
|
||||
- 모든 JS↔Native 호출 의 JSON serialize → message queue → async dispatch.
|
||||
- 매 frame 마다 hundreds of message — main thread blocking.
|
||||
- Image, animation 의 latency 누적.
|
||||
|
||||
### 매 JSI 솔루션
|
||||
- **HostObject**: C++ object 가 JS prototype 처럼 expose.
|
||||
- **Sync calls**: 매 native function 의 즉시 invoke (no queue).
|
||||
- **Shared memory (ArrayBuffer)**: 매 zero-copy data exchange.
|
||||
- **TurboModules**: JSI 기반 native module — lazy-loaded, typed.
|
||||
- **Fabric**: JSI 기반 renderer — synchronous layout.
|
||||
|
||||
### 매 응용
|
||||
1. Reanimated 3+: UI thread 에서 매 worklet 의 sync 실행 (60→120Hz).
|
||||
2. MMKV: native key-value store 의 sync access (AsyncStorage 의 100x).
|
||||
3. VisionCamera: frame processor 의 native↔JS 의 zero-copy.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### TurboModule — JS 측 spec
|
||||
```typescript
|
||||
// NativeCalculator.ts
|
||||
import type { TurboModule } from 'react-native';
|
||||
import { TurboModuleRegistry } from 'react-native';
|
||||
|
||||
export interface Spec extends TurboModule {
|
||||
add(a: number, b: number): number; // 매 sync return
|
||||
sha256(input: string): Promise<string>;
|
||||
getConstants(): { version: string };
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');
|
||||
```
|
||||
|
||||
### TurboModule — iOS 구현
|
||||
```objc
|
||||
// Calculator.mm
|
||||
#import "Calculator.h"
|
||||
#import <RNCalculatorSpec/RNCalculatorSpec.h>
|
||||
|
||||
@implementation Calculator
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (NSNumber *)add:(double)a b:(double)b {
|
||||
return @(a + b); // 매 sync, no bridge
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<facebook::react::NativeCalculatorSpecJSI>(params);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
### HostObject — direct C++ binding
|
||||
```cpp
|
||||
// MyHostObject.cpp
|
||||
#include <jsi/jsi.h>
|
||||
using namespace facebook::jsi;
|
||||
|
||||
class MyHostObject : public HostObject {
|
||||
public:
|
||||
Value get(Runtime& rt, const PropNameID& name) override {
|
||||
auto n = name.utf8(rt);
|
||||
if (n == "fastFunction") {
|
||||
return Function::createFromHostFunction(
|
||||
rt, name, 1,
|
||||
[](Runtime& rt, const Value&, const Value* args, size_t) -> Value {
|
||||
double x = args[0].asNumber();
|
||||
return Value(x * 2.0); // 매 sync, no JSON
|
||||
});
|
||||
}
|
||||
return Value::undefined();
|
||||
}
|
||||
};
|
||||
|
||||
void install(Runtime& rt) {
|
||||
auto obj = std::make_shared<MyHostObject>();
|
||||
rt.global().setProperty(rt, "myNative", Object::createFromHostObject(rt, obj));
|
||||
}
|
||||
```
|
||||
|
||||
### Reanimated worklet (JSI 활용)
|
||||
```typescript
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||
|
||||
function Box() {
|
||||
const x = useSharedValue(0); // 매 UI thread shared
|
||||
const style = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: x.value }],
|
||||
})); // 매 worklet, UI thread 에서 sync 실행
|
||||
|
||||
return (
|
||||
<Animated.View style={style} onTouchEnd={() => {
|
||||
x.value = withSpring(100); // 매 JSI 의 sync update
|
||||
}} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### MMKV (sync storage)
|
||||
```typescript
|
||||
import { MMKV } from 'react-native-mmkv';
|
||||
const storage = new MMKV();
|
||||
|
||||
storage.set('user.id', '123'); // 매 sync, no Promise
|
||||
const id = storage.getString('user.id'); // 매 sync
|
||||
// AsyncStorage: await storage.getItem(...) — 매 ms 단위 latency
|
||||
```
|
||||
|
||||
### VisionCamera frame processor
|
||||
```typescript
|
||||
import { useFrameProcessor } from 'react-native-vision-camera';
|
||||
import { detectFaces } from 'vision-camera-face-detector';
|
||||
|
||||
function Scanner() {
|
||||
const frameProcessor = useFrameProcessor((frame) => {
|
||||
'worklet';
|
||||
const faces = detectFaces(frame); // 매 native, zero-copy
|
||||
runOnJS(setFaces)(faces);
|
||||
}, []);
|
||||
return <Camera frameProcessor={frameProcessor} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Fabric component (synchronous layout)
|
||||
```typescript
|
||||
// MyViewNativeComponent.ts
|
||||
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
||||
import type { ViewProps } from 'react-native';
|
||||
|
||||
interface NativeProps extends ViewProps {
|
||||
color?: string;
|
||||
}
|
||||
export default codegenNativeComponent<NativeProps>('MyView');
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| RN 0.68+ 새 project | New Architecture (Fabric+TurboModules) enable |
|
||||
| Sync native 필요 | TurboModule + JSI |
|
||||
| Animation 60+ FPS | Reanimated 3 worklet |
|
||||
| Storage sync | MMKV (JSI) over AsyncStorage |
|
||||
| Frame-by-frame ML | VisionCamera + JSI worklet |
|
||||
|
||||
**기본값**: New Architecture, TurboModules, Reanimated 3.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React Native]] · [[Hermes]]
|
||||
- 변형: [[Fabric]] · [[TurboModules]]
|
||||
- 응용: [[MMKV]]
|
||||
- Adjacent: [[New Architecture (React Native Fabric/TurboModules)]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: RN 의 native module 작성, performance bottleneck 진단, sync API 필요.
|
||||
**언제 X**: 매 plain JS-only feature, Expo Go 환경 (custom native 의 X).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **JSI sync function 에서 heavy work**: JS thread block — 매 worklet / native thread 사용.
|
||||
- **HostObject 의 reference 누수**: shared_ptr cycle — Runtime invalidate 시 crash.
|
||||
- **Old bridge + JSI 혼용**: serialization overhead 잔존, 매 New Architecture full migration.
|
||||
- **Reanimated worklet 에서 closure capture 무분별**: 매 'worklet' directive 누락 시 silent fall-back to JS thread.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (reactnative.dev/architecture, RN 0.76 New Architecture default 발표).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — JSI + TurboModules 패턴 정리 |
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
id: wiki-2026-0508-jsx
|
||||
title: JSX
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [JavaScript XML, React JSX, TSX]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [jsx, react, typescript, transpilation]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# JSX
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 syntactic sugar over `createElement` — XML-like JS expression"**. React 의 발명, 매 declarative UI 의 핵심. 매 `<div>x</div>` → `jsx('div', null, 'x')` 로 transpile. 2026 default 는 **automatic runtime** (`react/jsx-runtime`) — 매 `import React` 의 불필요.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 transpile model
|
||||
- **Classic runtime** (legacy): `<div/>` → `React.createElement('div', null)` — 매 file 마다 `import React` 필요.
|
||||
- **Automatic runtime** (2020+, default): `<div/>` → `_jsx('div', null)`, runtime 의 자동 import.
|
||||
- **Preserve**: TS `--jsx preserve` — Babel 등 다른 tool 에 위임.
|
||||
|
||||
### 매 element types
|
||||
- **Lowercase** (`div`, `span`): 매 string tag, host component.
|
||||
- **Uppercase** (`Foo`): 매 reference, component identifier.
|
||||
- **Member access** (`Lib.Btn`): 매 namespaced component.
|
||||
- **Fragment** (`<>...</>`): 매 wrapper-less group.
|
||||
|
||||
### 매 응용
|
||||
1. React / Preact / Solid / Qwik 의 매 component definition.
|
||||
2. MDX — Markdown + JSX.
|
||||
3. JSX as data (Astro, hyperscript variant).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Automatic runtime (2026 default)
|
||||
```tsx
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx", // 매 automatic runtime
|
||||
"jsxImportSource": "react" // or "preact", "@emotion/react"
|
||||
}
|
||||
}
|
||||
|
||||
// Component.tsx — 매 import React 의 X
|
||||
export function Hello({ name }: { name: string }) {
|
||||
return <h1>Hello, {name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional rendering
|
||||
```tsx
|
||||
function Status({ user }: { user?: User }) {
|
||||
if (!user) return null;
|
||||
return (
|
||||
<>
|
||||
{user.isAdmin && <AdminBadge />}
|
||||
{user.posts.length > 0
|
||||
? <PostList posts={user.posts} />
|
||||
: <EmptyState />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Children pattern
|
||||
```tsx
|
||||
type Props = { title: string; children: React.ReactNode };
|
||||
function Card({ title, children }: Props) {
|
||||
return (
|
||||
<section className="card">
|
||||
<h2>{title}</h2>
|
||||
<div className="body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Render prop / function-as-children
|
||||
```tsx
|
||||
type DataLoaderProps<T> = {
|
||||
url: string;
|
||||
children: (data: T | undefined, loading: boolean) => React.ReactNode;
|
||||
};
|
||||
function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
||||
const [data, setData] = useState<T>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false); });
|
||||
}, [url]);
|
||||
return <>{children(data, loading)}</>;
|
||||
}
|
||||
```
|
||||
|
||||
### Polymorphic component (`as` prop)
|
||||
```tsx
|
||||
type AsProp<C extends React.ElementType> = { as?: C };
|
||||
type PolymorphicProps<C extends React.ElementType> =
|
||||
AsProp<C> & React.ComponentPropsWithoutRef<C>;
|
||||
|
||||
function Box<C extends React.ElementType = 'div'>({
|
||||
as, ...rest
|
||||
}: PolymorphicProps<C>) {
|
||||
const Comp = as ?? 'div';
|
||||
return <Comp {...rest} />;
|
||||
}
|
||||
|
||||
<Box as="a" href="/x" /> // 매 anchor props 의 typed
|
||||
<Box as={MyButton} onClick={...} />
|
||||
```
|
||||
|
||||
### JSX spread + override
|
||||
```tsx
|
||||
function PrimaryButton(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return <button {...props} className={`btn-primary ${props.className ?? ''}`} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Fragment + key
|
||||
```tsx
|
||||
function List({ items }: { items: Item[] }) {
|
||||
return (
|
||||
<dl>
|
||||
{items.map(it => (
|
||||
<React.Fragment key={it.id}>
|
||||
<dt>{it.term}</dt>
|
||||
<dd>{it.def}</dd>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript — JSX.IntrinsicElements override
|
||||
```tsx
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'my-web-component': React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLElement> & { value?: string },
|
||||
HTMLElement
|
||||
>;
|
||||
}
|
||||
}
|
||||
}
|
||||
<my-web-component value="x" />
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 새 React project | `"jsx": "react-jsx"` (automatic) |
|
||||
| Preact / Solid | `jsxImportSource` 적절 설정 |
|
||||
| Babel 사용 | `"jsx": "preserve"` + babel preset |
|
||||
| Web component 통합 | `JSX.IntrinsicElements` 확장 |
|
||||
| 매 server component | `"jsx": "react-jsx"` (RSC 호환) |
|
||||
|
||||
**기본값**: automatic runtime + TypeScript strict.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[TypeScript]]
|
||||
- 변형: [[TSX]] · [[MDX]]
|
||||
- Adjacent: [[SWC]] · [[esbuild]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: React/Preact/Solid component 작성, TS JSX 설정 디버깅, polymorphic API 설계.
|
||||
**언제 X**: 매 plain HTML template (no transpile needed), 매 Vue SFC (`<template>` 별 syntax).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`React` import 누락 (classic runtime)**: 매 `react-jsx` 로 migrate.
|
||||
- **lowercase component name**: `<myButton/>` 매 host element 로 처리됨 — 매 PascalCase 강제.
|
||||
- **`key` 누락 in list**: reconciliation 비용 폭증.
|
||||
- **inline function child 의 남발**: render 마다 새 reference — memoized child re-render 유발.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (react.dev JSX docs, TS Handbook JSX, React 19 RC notes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — JSX automatic runtime + polymorphic patterns 정리 |
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
id: wiki-2026-0508-javascript-async-and-event-loop
|
||||
title: JavaScript Async and Event Loop
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Event Loop, JS Event Loop, Microtask Queue, Macrotask Queue]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [javascript, async, event-loop, concurrency]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: node-browser
|
||||
---
|
||||
|
||||
# JavaScript Async and Event Loop
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 single-threaded JS 가 매 cooperative scheduler 위에서 매 async 를 흉내낸다."**. Call stack + task queue + microtask queue + render pipeline 이 매 tick 의 단위. Promise/async-await 는 매 syntactic sugar — runtime 은 매 microtask drain rule 로 결정.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Stack vs Queues
|
||||
- **Call stack**: 매 동기 frame. LIFO. 매 비어야 매 tick 진행.
|
||||
- **Task queue (macrotask)**: setTimeout, setInterval, MessageChannel, I/O callback, UI event. 매 tick 당 1개 drain.
|
||||
- **Microtask queue**: Promise.then, queueMicrotask, MutationObserver. 매 tick 마지막에 매 전부 drain (재진입 포함).
|
||||
- **Animation frame queue**: requestAnimationFrame. 매 paint 직전.
|
||||
- **Render steps**: style → layout → paint → composite. 매 vsync 와 매 동기화.
|
||||
|
||||
### 매 Tick 순서 (browser)
|
||||
1. 매 task queue 에서 매 1 task pop → 실행.
|
||||
2. 매 microtask queue 매 전부 drain (recursively).
|
||||
3. 매 rAF callbacks.
|
||||
4. 매 render (필요 시).
|
||||
5. 매 idle callbacks (requestIdleCallback).
|
||||
6. 매 다음 tick.
|
||||
|
||||
### 매 Node.js phases
|
||||
- timers → pending callbacks → idle/prepare → poll → check (setImmediate) → close → microtask drain (between phases since Node 11).
|
||||
- `process.nextTick` 는 매 microtask 보다도 매 우선.
|
||||
|
||||
### 매 응용
|
||||
1. UI freeze 방지 — 매 long task 를 매 chunk + scheduler.yield().
|
||||
2. Race condition 분석 — 매 await 사이 매 state mutation 가능.
|
||||
3. Server backpressure — 매 event loop lag (`perf_hooks.monitorEventLoopDelay`).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Microtask vs Macrotask 순서
|
||||
```javascript
|
||||
console.log('1');
|
||||
setTimeout(() => console.log('2'), 0);
|
||||
Promise.resolve().then(() => console.log('3'));
|
||||
queueMicrotask(() => console.log('4'));
|
||||
console.log('5');
|
||||
// 1, 5, 3, 4, 2 — microtasks drain before next macrotask
|
||||
```
|
||||
|
||||
### Long task chunking with scheduler.yield (2026)
|
||||
```javascript
|
||||
async function processLargeArray(items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
doWork(items[i]);
|
||||
if (i % 100 === 0 && 'scheduler' in window) {
|
||||
await scheduler.yield(); // Chrome 129+ stable
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Promise.allSettled with concurrency limit
|
||||
```javascript
|
||||
async function mapLimit(items, limit, fn) {
|
||||
const results = new Array(items.length);
|
||||
let cursor = 0;
|
||||
const workers = Array.from({ length: limit }, async () => {
|
||||
while (cursor < items.length) {
|
||||
const i = cursor++;
|
||||
results[i] = await fn(items[i]).catch(e => ({ error: e }));
|
||||
}
|
||||
});
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
### AbortController for cancellation
|
||||
```javascript
|
||||
const ac = new AbortController();
|
||||
fetch('/slow', { signal: ac.signal })
|
||||
.then(r => r.json())
|
||||
.catch(e => { if (e.name === 'AbortError') console.log('cancelled'); });
|
||||
setTimeout(() => ac.abort(), 3000);
|
||||
```
|
||||
|
||||
### Async iterator drain
|
||||
```javascript
|
||||
async function* readChunks(stream) {
|
||||
const reader = stream.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return;
|
||||
yield value;
|
||||
}
|
||||
} finally { reader.releaseLock(); }
|
||||
}
|
||||
|
||||
for await (const chunk of readChunks(resp.body)) {
|
||||
process(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid microtask starvation
|
||||
```javascript
|
||||
// BAD — recursive Promise.resolve() blocks rendering
|
||||
function bad() {
|
||||
Promise.resolve().then(bad); // browser never paints
|
||||
}
|
||||
// GOOD — use setTimeout(0) or MessageChannel for yielding
|
||||
function good() {
|
||||
setTimeout(good, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Event loop lag monitor (Node)
|
||||
```javascript
|
||||
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
||||
const h = monitorEventLoopDelay({ resolution: 20 });
|
||||
h.enable();
|
||||
setInterval(() => {
|
||||
console.log('p99 lag ms', h.percentile(99) / 1e6);
|
||||
h.reset();
|
||||
}, 1000);
|
||||
```
|
||||
|
||||
### Top-level await (ESM)
|
||||
```javascript
|
||||
// module.mjs
|
||||
const config = await fetch('/config.json').then(r => r.json());
|
||||
export default config;
|
||||
// importer awaits parent module graph — beware deadlock cycles
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 즉시 yield 필요 | queueMicrotask / Promise.resolve().then |
|
||||
| 매 paint 후 작업 | requestAnimationFrame |
|
||||
| 매 idle 시간 작업 | requestIdleCallback / scheduler.postTask('background') |
|
||||
| 매 long task chunking | scheduler.yield() (modern) |
|
||||
| 매 cancellation | AbortController + signal |
|
||||
| 매 backpressure 측정 | perf_hooks.monitorEventLoopDelay |
|
||||
|
||||
**기본값**: async/await + AbortController. 매 long task 는 매 scheduler.yield. 매 nextTick / process.nextTick 남용 X.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[JavaScript]] · [[Concurrency]]
|
||||
- 변형: [[Browser Rendering]]
|
||||
- 응용: [[React Concurrent Mode]] · [[Web Worker (웹 워커)|Web Workers]]
|
||||
- Adjacent: [[AbortController]] · [[Streams]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 race / ordering bug 분석, 매 long-task profiling, 매 SSR streaming 설계.
|
||||
**언제 X**: 매 CPU-bound 작업 — 매 worker / native 로 offload.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Recursive microtask loop**: 매 rendering starvation.
|
||||
- **await in tight for loop**: 매 직렬화. 매 Promise.all 사용.
|
||||
- **forgotten unhandled rejection**: 매 process crash (Node 15+).
|
||||
- **setTimeout(fn, 0) for ordering**: 매 microtask 와 매 race — queueMicrotask 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (HTML spec — Event Loop Processing Model, Node.js docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 task/microtask order + scheduler.yield 패턴 |
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
id: wiki-2026-0508-javascript-optimization-patterns
|
||||
title: JavaScript Optimization Patterns
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [JS Performance Patterns, V8 Optimization, JS Hot Path]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [javascript, performance, v8, optimization]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: v8
|
||||
---
|
||||
|
||||
# JavaScript Optimization Patterns
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 hot path 의 type stability + allocation 최소화"**. V8/JSC/SpiderMonkey 모두 매 inline cache + hidden class 의 의존 — 매 monomorphic call site 가 매 fastest. 매 micro-optimization 보다 매 algorithmic / batching 이 매 win 큼, 매 그러나 hot loop 에서는 매 GC pressure 의 의식 필수.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 V8 의 fast path
|
||||
- **Hidden class (Map)**: object shape — 매 동일 property 순서 의 같은 hidden class.
|
||||
- **Inline cache (IC)**: call site 별 type 기록 — monomorphic > polymorphic > megamorphic.
|
||||
- **TurboFan / Maglev**: hot function 의 optimizing compile, type feedback 기반.
|
||||
- **Sparkplug** (2021+): 매 baseline JIT, 매 quick startup.
|
||||
|
||||
### 매 GC pressure
|
||||
- **Young generation (Scavenger)**: 매 short-lived alloc — 매 cheap.
|
||||
- **Old generation (Mark-Compact)**: 매 promoted object — 매 stop-the-world 의 risk.
|
||||
- **Strategy**: 매 hot loop 에서 alloc 의 회피 (object pooling, typed arrays).
|
||||
|
||||
### 매 응용
|
||||
1. Animation / game loop 의 60+ FPS 유지.
|
||||
2. 매 large data transformation (millions of records).
|
||||
3. Server-side hot path (Node, Bun).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Object shape stability
|
||||
```javascript
|
||||
// X — hidden class 가 다름 (assignment 순서)
|
||||
function A() { this.x = 1; this.y = 2; }
|
||||
function B() { this.y = 2; this.x = 1; }
|
||||
const arr = [new A(), new B()]; // 매 polymorphic IC
|
||||
|
||||
// O — 매 동일 shape
|
||||
class Point { constructor(x, y) { this.x = x; this.y = y; } }
|
||||
const arr2 = [new Point(1, 2), new Point(3, 4)];
|
||||
```
|
||||
|
||||
### Monomorphic call site
|
||||
```javascript
|
||||
function add(a, b) { return a + b; } // V8 의 type feedback 기록
|
||||
|
||||
// X — megamorphic (string + number + array)
|
||||
add(1, 2); add('a', 'b'); add([], []);
|
||||
|
||||
// O — monomorphic (always number)
|
||||
for (let i = 0; i < 1e6; i++) add(i, i + 1);
|
||||
```
|
||||
|
||||
### Typed arrays (no boxing)
|
||||
```javascript
|
||||
// X — 매 Array of number — boxed double, GC pressure
|
||||
const heights = new Array(1_000_000);
|
||||
for (let i = 0; i < heights.length; i++) heights[i] = Math.random();
|
||||
|
||||
// O — Float64Array — flat, no boxing
|
||||
const heights2 = new Float64Array(1_000_000);
|
||||
for (let i = 0; i < heights2.length; i++) heights2[i] = Math.random();
|
||||
```
|
||||
|
||||
### Object pooling (hot loop)
|
||||
```javascript
|
||||
class Vec3Pool {
|
||||
#pool = [];
|
||||
acquire(x = 0, y = 0, z = 0) {
|
||||
const v = this.#pool.pop() ?? { x: 0, y: 0, z: 0 };
|
||||
v.x = x; v.y = y; v.z = z;
|
||||
return v;
|
||||
}
|
||||
release(v) { this.#pool.push(v); }
|
||||
}
|
||||
|
||||
const pool = new Vec3Pool();
|
||||
function frame() {
|
||||
const tmp = pool.acquire(1, 2, 3);
|
||||
// ... compute ...
|
||||
pool.release(tmp);
|
||||
}
|
||||
```
|
||||
|
||||
### Loop hoisting
|
||||
```javascript
|
||||
// X — length 매 iter 마다 access
|
||||
for (let i = 0; i < items.length; i++) { ... }
|
||||
|
||||
// O — modern V8 의 자동 hoist 하지만 매 explicit 가 안전
|
||||
const n = items.length;
|
||||
for (let i = 0; i < n; i++) { ... }
|
||||
|
||||
// O+ — for-of with iterator (매 modern, V8 fast path)
|
||||
for (const it of items) { ... }
|
||||
```
|
||||
|
||||
### Map vs object lookup
|
||||
```javascript
|
||||
// 매 string key 의 dynamic — Map 의 매 fast & predictable
|
||||
const cache = new Map();
|
||||
cache.set(key, value);
|
||||
cache.get(key);
|
||||
|
||||
// 매 known small fixed keys — object 매 inline cache 의 winning
|
||||
const config = { mode: 'fast', retries: 3 };
|
||||
```
|
||||
|
||||
### Avoid `arguments`, use rest
|
||||
```javascript
|
||||
// X — arguments 의 deopt (non-array, function-bound)
|
||||
function sum() {
|
||||
let s = 0;
|
||||
for (let i = 0; i < arguments.length; i++) s += arguments[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
// O
|
||||
function sum(...nums) {
|
||||
let s = 0;
|
||||
for (const n of nums) s += n;
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### Batching DOM / state updates
|
||||
```javascript
|
||||
// X — 매 update 의 layout thrash
|
||||
items.forEach(it => container.appendChild(make(it)));
|
||||
|
||||
// O — DocumentFragment batch
|
||||
const frag = document.createDocumentFragment();
|
||||
items.forEach(it => frag.appendChild(make(it)));
|
||||
container.appendChild(frag);
|
||||
```
|
||||
|
||||
### Web Workers (off-main-thread)
|
||||
```javascript
|
||||
// main.js
|
||||
const worker = new Worker('worker.js', { type: 'module' });
|
||||
worker.postMessage({ data: bigArray }, [bigArray.buffer]); // 매 transfer, zero-copy
|
||||
|
||||
// worker.js
|
||||
self.onmessage = (e) => {
|
||||
const result = heavy(e.data.data);
|
||||
self.postMessage(result);
|
||||
};
|
||||
```
|
||||
|
||||
### Structured cloning vs transferable
|
||||
```javascript
|
||||
// 매 1MB+ data — transferable 의 50-100x faster
|
||||
const buf = new ArrayBuffer(1_000_000);
|
||||
worker.postMessage(buf, [buf]); // 매 ownership transfer
|
||||
```
|
||||
|
||||
### Lazy evaluation (generator)
|
||||
```javascript
|
||||
function* range(from, to) {
|
||||
for (let i = from; i < to; i++) yield i;
|
||||
}
|
||||
function* map(it, f) { for (const v of it) yield f(v); }
|
||||
function* filter(it, p) { for (const v of it) if (p(v)) yield v; }
|
||||
|
||||
// 매 1B element 의 first 10 — alloc 의 X
|
||||
const result = [];
|
||||
for (const v of filter(map(range(0, 1e9), x => x * 2), x => x % 3 === 0)) {
|
||||
result.push(v);
|
||||
if (result.length === 10) break;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Hot loop alloc | typed array + object pool |
|
||||
| Large data transform | streaming / generator |
|
||||
| Heavy compute | Web Worker + transferable |
|
||||
| DOM batch | DocumentFragment / requestAnimationFrame |
|
||||
| Type stability 의심 | DevTools Profiler "ICs" tab |
|
||||
| 매 micro-bench 결과 의 의심 | always profile in production-like build |
|
||||
|
||||
**기본값**: 매 algorithmic win 우선, 매 hot path 만 micro-optimize.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[JavaScript]] · [[V8]]
|
||||
- 변형: [[JSC]]
|
||||
- 응용: [[Web Worker (웹 워커)|Web-Workers]] · [[Animation-Performance]]
|
||||
- Adjacent: [[Garbage Collection]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 measurable bottleneck 진단 후, hot loop 작성, large data transform.
|
||||
**언제 X**: 매 cold path / one-shot script (premature optimization), 매 readability cost > perf gain.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Premature micro-opt**: 매 readability 희생, 매 measure 없이 추측.
|
||||
- **`delete obj.prop`**: hidden class transition 유발, IC deopt.
|
||||
- **Hot loop 의 closure alloc**: `arr.map(x => x*2)` 매 frame — 매 함수 hoist.
|
||||
- **`try/catch` in hot loop**: 매 V8 의 optimization 일부 비활성 (개선되었지만 여전히 cost).
|
||||
- **string concat in loop**: `+=` 매 hot loop — 매 array.join.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (V8 blog, Chrome DevTools Performance docs, Bun benchmark methodology).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — V8 hot-path optimization 패턴 정리 |
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
id: wiki-2026-0508-javascriptcore
|
||||
title: JavaScriptCore
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [JSC, WebKit JSC, Nitro]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [javascript, engine, webkit, jit]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: C++
|
||||
framework: WebKit
|
||||
---
|
||||
|
||||
# JavaScriptCore
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Apple WebKit 의 JavaScript 엔진 — 4-tier JIT pipeline (LLInt → Baseline → DFG → FTL/B3)"**. Safari, iOS WebView, React Native (Hermes 도입 전), Bun runtime의 core. 매 V8 대비 startup 빠름, peak performance 비슷 — 매 mobile-first 최적화.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4-Tier JIT Pipeline
|
||||
- **LLInt** (Low-Level Interpreter): 매 첫 실행, profiling 시작.
|
||||
- **Baseline JIT**: 매 hot 코드 → simple machine code, type feedback 수집.
|
||||
- **DFG** (Data Flow Graph): 매 speculative optimization, OSR exit 가능.
|
||||
- **FTL** (Faster Than Light) / B3 backend: 매 최상위 tier, LLVM-style IR.
|
||||
|
||||
### 매 GC
|
||||
- **Riptide**: 매 generational + concurrent mark-and-sweep.
|
||||
- **Eden GC**: 매 young objects, frequent.
|
||||
- **Full GC**: 매 entire heap, infrequent.
|
||||
|
||||
### 매 응용
|
||||
1. Safari / WKWebView (iOS, macOS).
|
||||
2. Bun runtime — 매 Node.js 대안, JSC 채택 (V8 X).
|
||||
3. React Native (legacy iOS, Hermes 이전).
|
||||
4. GNOME / GTK WebKitGTK.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Bun runtime — JSC embed
|
||||
```javascript
|
||||
// bun runs JS via JSC (not V8)
|
||||
// startup: ~10ms vs Node ~30ms
|
||||
import { serve } from "bun";
|
||||
|
||||
serve({
|
||||
port: 3000,
|
||||
fetch(req) {
|
||||
return new Response("Hello from JSC");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Inline cache observation (JSC IR dump)
|
||||
```bash
|
||||
# JSC bytecode dump
|
||||
JSC_dumpBytecodesAfterGeneration=true \
|
||||
/System/Library/Frameworks/JavaScriptCore.framework/Helpers/jsc script.js
|
||||
|
||||
# DFG/FTL 단계 확인
|
||||
JSC_dumpDFGGraphAtEachPhase=true jsc script.js
|
||||
```
|
||||
|
||||
### Hidden class friendly object
|
||||
```javascript
|
||||
// JSC structure (V8의 hidden class 와 동일 개념)
|
||||
function Point(x, y) {
|
||||
this.x = x; // structure transition T0 → T1
|
||||
this.y = y; // T1 → T2
|
||||
}
|
||||
// T2 가 stable → inline cache hit
|
||||
```
|
||||
|
||||
### Type feedback (monomorphic 유지)
|
||||
```javascript
|
||||
function add(a, b) { return a + b; }
|
||||
add(1, 2); // int+int → DFG int specialization
|
||||
add(1.5, 2.5); // float+float → polymorphic, slower
|
||||
|
||||
// 매 monomorphic 유지 — 매 동일 type 만 호출
|
||||
```
|
||||
|
||||
### OffHeap typed array (Bun)
|
||||
```javascript
|
||||
// JSC 의 ArrayBuffer 는 GC 외부 — large data 적합
|
||||
const buf = new ArrayBuffer(1024 * 1024 * 100); // 100MB
|
||||
const view = new Uint8Array(buf);
|
||||
// 매 GC pressure X
|
||||
```
|
||||
|
||||
### WeakRef (JSC 1.16+)
|
||||
```javascript
|
||||
const cache = new WeakRef(largeObject);
|
||||
// 매 GC 가 collect 가능 — memory pressure 시 free
|
||||
const ref = cache.deref();
|
||||
if (ref) { /* still alive */ }
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| iOS native app + JS | JSC (WKWebView 또는 JavaScriptCore.framework) |
|
||||
| Cross-platform server | V8 (Node.js) — 매 ecosystem 큼 |
|
||||
| Fast startup CLI | Bun (JSC) — 매 startup 압도적 |
|
||||
| RN new architecture | Hermes — 매 mobile 최적화 |
|
||||
|
||||
**기본값**: server는 V8/Node, mobile WebView는 JSC, fast CLI는 Bun.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[WebKit]]
|
||||
- 변형: [[V8]] · [[Hermes]]
|
||||
- 응용: [[React Native]]
|
||||
- Adjacent: [[Garbage Collection]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: iOS WebView 성능 분석, Bun 코드 최적화, JSC-specific bug 진단.
|
||||
**언제 X**: Node.js / Chrome 환경 — 매 V8 별도 문서.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Polymorphic call sites**: 매 inline cache miss → DFG bailout.
|
||||
- **delete operator**: 매 hidden class transition 깨짐 — 매 undefined 할당으로 대체.
|
||||
- **arguments object 남용**: 매 modern JSC 는 rest params 가 빠름.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (WebKit Wiki, JSC source — github.com/WebKit/WebKit).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — JSC 4-tier pipeline + Bun 응용 |
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
id: wiki-2026-0508-judgment
|
||||
title: Judgment
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Engineering Judgment, Trade-off Judgment, Frontend Judgment]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [judgment, engineering, decision-making, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: meta
|
||||
framework: meta
|
||||
---
|
||||
|
||||
# Judgment
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 좋은 frontend 는 매 framework choice 가 아니라 매 trade-off 의 매 명시적 분석에서 나온다."**. Performance / DX / accessibility / bundle size / SEO 의 매 weight 가 매 product context 에 따라 매 다르다. Judgment 는 매 가르칠 수 있는 skill — checklist + heuristic.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Trade-off 축
|
||||
- **Performance vs DX**: SSR + island vs CSR SPA.
|
||||
- **Bundle vs Feature**: tree-shake-able lib vs all-in-one.
|
||||
- **Type safety vs Velocity**: strict TS vs JS prototyping.
|
||||
- **A11y vs Custom**: native element vs custom component.
|
||||
- **Cache vs Freshness**: SWR vs no-store.
|
||||
|
||||
### 매 Heuristic
|
||||
1. **Native first**: 매 platform primitive 가 매 free win.
|
||||
2. **Measure before optimize**: 매 Lighthouse / RUM 먼저.
|
||||
3. **Latency budget**: 매 LCP < 2.5s, INP < 200ms, CLS < 0.1 (Core Web Vitals 2026).
|
||||
4. **Bundle budget**: 매 route 당 매 100KB gzip.
|
||||
5. **Dependency cost**: 매 add 마다 매 maintenance + bundle + supply chain risk.
|
||||
|
||||
### 매 Decision framework
|
||||
- **Reversible vs One-way door**: framework choice = one-way. 매 careful.
|
||||
- **Time horizon**: 매 6 개월 vs 매 5 년 — 매 다른 답.
|
||||
- **Team skill**: 매 unfamiliar tech 의 매 hidden cost.
|
||||
|
||||
### 매 응용
|
||||
1. SSR vs CSR — 매 SEO / TTI / DX matrix.
|
||||
2. State manager 선택 — 매 server vs client state 분리.
|
||||
3. CSS strategy — 매 atomic / module / runtime.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Decision matrix template (Markdown)
|
||||
```markdown
|
||||
| 기준 | Option A | Option B | Weight |
|
||||
|---|---|---|---|
|
||||
| Bundle size | 12KB | 45KB | 0.3 |
|
||||
| DX | High | Medium | 0.2 |
|
||||
| Maintenance | Active | Stale | 0.3 |
|
||||
| TS support | Native | Patchy | 0.2 |
|
||||
| **Score** | 0.84 | 0.51 | — |
|
||||
```
|
||||
|
||||
### Bundle budget enforcement (CI)
|
||||
```javascript
|
||||
// vite.config.ts
|
||||
export default {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: { manualChunks: { vendor: ['react', 'react-dom'] } },
|
||||
},
|
||||
chunkSizeWarningLimit: 100, // KB
|
||||
},
|
||||
};
|
||||
// Then: bundlesize / size-limit in CI
|
||||
```
|
||||
|
||||
### Performance budget gate
|
||||
```yaml
|
||||
# .github/workflows/perf.yml
|
||||
- name: Lighthouse CI
|
||||
uses: treosh/lighthouse-ci-action@v12
|
||||
with:
|
||||
urls: |
|
||||
https://staging.example.com/
|
||||
budgetPath: ./budget.json
|
||||
assertions: |
|
||||
categories.performance: ['error', { minScore: 0.9 }]
|
||||
audits.largest-contentful-paint: ['error', { maxNumericValue: 2500 }]
|
||||
audits.interaction-to-next-paint: ['error', { maxNumericValue: 200 }]
|
||||
```
|
||||
|
||||
### Feature flag for risky migration
|
||||
```typescript
|
||||
import { useFlag } from '@/lib/flags';
|
||||
function Page() {
|
||||
const useNewRouter = useFlag('new-router-2026');
|
||||
return useNewRouter ? <NextAppRouter /> : <PagesRouter />;
|
||||
}
|
||||
```
|
||||
|
||||
### A11y default vs custom
|
||||
```tsx
|
||||
// PREFER native
|
||||
<button onClick={...}>Save</button>
|
||||
|
||||
// Avoid custom unless necessary
|
||||
<div role="button" tabIndex={0} onKeyDown={handle}>...</div>
|
||||
```
|
||||
|
||||
### Reversibility check
|
||||
```markdown
|
||||
- [ ] Can we roll back in 1 day? (file flag, env var)
|
||||
- [ ] Can we roll back in 1 week? (revert PR)
|
||||
- [ ] One-way door? (DB schema, public API) — 매 RFC 필수
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 SEO 우선 | SSR / SSG (Next.js, Astro, SvelteKit) |
|
||||
| 매 dashboard (auth-only) | CSR SPA (Vite + React) |
|
||||
| 매 content site | Astro / 11ty (zero JS default) |
|
||||
| 매 realtime collaborative | CSR + WebSocket / CRDT |
|
||||
| 매 mobile app | RN / Expo (Hermes) |
|
||||
| 매 desktop app | Tauri (Rust) > Electron |
|
||||
|
||||
**기본값**: 매 measure → matrix → smallest reversible step.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[ADR]]
|
||||
- 응용: [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- Adjacent: [[Core Web Vitals Optimization (INP, LCP, CLS)|Core Web Vitals]] · [[Accessibility (A11y)|Accessibility]] · [[DX]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 framework / library 선택 시, 매 RFC 작성, 매 trade-off 명시화.
|
||||
**언제 X**: 매 obvious choice — 매 over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Resume-driven development**: 매 새로운 tech 만 추구.
|
||||
- **Unmeasured optimization**: 매 perf 직감 만.
|
||||
- **Cargo cult**: 매 big company 가 사용 = 매 우리에게 맞다 X.
|
||||
- **Analysis paralysis**: 매 endless matrix — 매 timebox.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Core Web Vitals 2026 thresholds, ADR template).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 trade-off matrix + budget pattern |
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
id: wiki-2026-0508-kingdom-vs-kingdom-events-kvk
|
||||
title: Kingdom vs. Kingdom Events (KvK)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [KvK, Kingdom vs Kingdom, Cross-Kingdom Event]
|
||||
duplicate_of: none
|
||||
source_trust_level: B
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [game, mmo, rok, event, server-architecture]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: GameServer
|
||||
---
|
||||
|
||||
# Kingdom vs. Kingdom Events (KvK)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 KvK 는 multiple game shards (kingdoms) 가 same world map 에서 PvP 경쟁하는 cross-shard event"**. Rise of Kingdoms (Lilith), Lords Mobile (IGG), Evony 등 4X mobile MMO 의 핵심 monetization driver. 매 frontend 입장에서 cross-kingdom matching, real-time map sync, leaderboard 가 challenge.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 매칭 단계
|
||||
- **Pre-KvK**: 매 power matchmaking — 매 비슷한 power 의 kingdoms 매칭.
|
||||
- **Lost Kingdom (LK)** / Pass 1: 매 kingdoms 가 새 map 에 spawn.
|
||||
- **Main KvK (Pass 2-3)**: 매 holy site / pass / altar 점령.
|
||||
- **Post-KvK**: 매 reward distribution, kingdom power recompute.
|
||||
|
||||
### 매 frontend challenge
|
||||
- **Map sync**: 매 1000+ players concurrent → 매 viewport-based delta sync.
|
||||
- **Leaderboard**: 매 cross-shard aggregation — 매 5-min cache.
|
||||
- **Chat**: 매 kingdom-only / alliance-only / cross-kingdom 매 channel 분리.
|
||||
- **Replay**: 매 battle replay — 매 server-authoritative state log.
|
||||
|
||||
### 매 응용
|
||||
1. Rise of Kingdoms — 매 8 kingdom matchup, 70-day season.
|
||||
2. Lords Mobile Kingdom Vs Kingdom.
|
||||
3. Evony Server War.
|
||||
4. Top War: Battle Game — 매 SvS (Server vs Server).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Cross-shard matchmaking
|
||||
```typescript
|
||||
// 매 power-bracket 매칭
|
||||
async function matchKvK(season: number) {
|
||||
const kingdoms = await db.kingdoms
|
||||
.where("season", season)
|
||||
.where("optedIn", true)
|
||||
.orderBy("totalPower", "desc")
|
||||
.get();
|
||||
|
||||
// 매 8 kingdoms / bracket
|
||||
const brackets: Kingdom[][] = [];
|
||||
for (let i = 0; i < kingdoms.length; i += 8) {
|
||||
brackets.push(kingdoms.slice(i, i + 8));
|
||||
}
|
||||
return brackets;
|
||||
}
|
||||
```
|
||||
|
||||
### Viewport-based map sync (frontend)
|
||||
```typescript
|
||||
class KvKMap {
|
||||
private wsUrl = "wss://kvk.game/v1/map";
|
||||
|
||||
subscribe(viewport: Bounds) {
|
||||
this.ws.send({
|
||||
op: "subscribe",
|
||||
bbox: viewport, // 매 viewport bbox 만 sync
|
||||
lod: viewport.zoom < 5 ? "tile" : "unit",
|
||||
});
|
||||
}
|
||||
|
||||
onMessage({ delta }: { delta: TileDelta[] }) {
|
||||
delta.forEach((d) => this.applyTile(d));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Leaderboard cache pattern
|
||||
```typescript
|
||||
// 매 cross-shard aggregation 비쌈 → 매 Redis sorted set + 5min TTL
|
||||
async function getKvKLeaderboard(seasonId: string) {
|
||||
const cached = await redis.get(`kvk:lb:${seasonId}`);
|
||||
if (cached) return JSON.parse(cached);
|
||||
|
||||
const data = await aggregateAcrossShards(seasonId); // 매 fan-out
|
||||
await redis.setex(`kvk:lb:${seasonId}`, 300, JSON.stringify(data));
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time troop march UI
|
||||
```tsx
|
||||
function MarchPath({ march }: { march: MarchEvent }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
useEffect(() => {
|
||||
const start = march.startedAt;
|
||||
const dur = march.duration;
|
||||
const id = requestAnimationFrame(function tick() {
|
||||
setProgress(Math.min(1, (Date.now() - start) / dur));
|
||||
if (Date.now() < start + dur) requestAnimationFrame(tick);
|
||||
});
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [march]);
|
||||
|
||||
return <Line from={march.from} to={march.to} t={progress} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Holy site capture timer (server-authoritative)
|
||||
```typescript
|
||||
interface HolySiteState {
|
||||
ownerKingdom: number | null;
|
||||
captureProgress: number; // 0-1
|
||||
contestedBy: number[];
|
||||
lastUpdate: number; // server timestamp
|
||||
}
|
||||
|
||||
// 매 client 는 server time 만 신뢰
|
||||
function renderProgress(state: HolySiteState, serverTime: number) {
|
||||
const elapsed = serverTime - state.lastUpdate;
|
||||
return state.captureProgress + (elapsed / CAPTURE_DURATION);
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| Aspect | Approach |
|
||||
|---|---|
|
||||
| Matchmaking | Power-bracket, opt-in |
|
||||
| Map sync | Viewport delta (not full snapshot) |
|
||||
| Leaderboard | Cached aggregation, 5min TTL |
|
||||
| Anti-cheat | Server-authoritative timer + replay log |
|
||||
| Chat | Channel separation, rate limit per kingdom |
|
||||
|
||||
**기본값**: server-authoritative state, viewport-based delta, Redis-cached leaderboard.
|
||||
|
||||
## 🔗 Graph
|
||||
- 응용: [[Rise of Kingdoms]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: KvK frontend 설계, cross-shard matchmaking, leaderboard cache 설계.
|
||||
**언제 X**: 매 single-shard PvP — 매 별도 패턴.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Full map snapshot per tick**: 매 BW 폭증 — 매 viewport delta 가 정답.
|
||||
- **Client-side capture timer**: 매 cheat 가능 — 매 server-authoritative.
|
||||
- **Real-time leaderboard**: 매 fan-out 비용 → 매 5min cache 충분.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Lilith Games KvK 매커니즘 documentation, 4X mobile game 공통 패턴).
|
||||
- 신뢰도 B (game-specific).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — KvK matchmaking + frontend sync 패턴 |
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
id: wiki-2026-0508-lanes-model
|
||||
title: Lanes Model
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Lanes, React Priority Lanes, Concurrent Lanes]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, scheduler, concurrent, lanes]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# Lanes Model
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 31-bit bitmask 로 매 scheduling priority 를 매 표현 — 매 concurrent React 의 매 심장."**. React 18+ 의 매 Lanes 는 매 expiration time model 을 매 대체. 매 multiple priority 의 매 update 가 매 동시에 매 진행, 매 batch 가 매 lane group 단위. 매 2026 React 19 도 매 동일 model.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Bitmask 정의 (`ReactFiberLane.js`)
|
||||
- 31 lanes. Bit 0 (rightmost) = 매 highest priority.
|
||||
- **SyncLane** = `0b0000000000000000000000000000001` — 매 click, input.
|
||||
- **InputContinuousLane** — 매 drag, scroll.
|
||||
- **DefaultLane** — 매 useEffect setState 등.
|
||||
- **TransitionLanes** (16 lanes) — 매 startTransition.
|
||||
- **RetryLanes** — 매 Suspense retry.
|
||||
- **IdleLane** — 매 lowest.
|
||||
- **OffscreenLane** — 매 hidden subtree.
|
||||
|
||||
### 매 Lane 연산
|
||||
- **Merge**: `a | b` — 매 여러 update 의 매 lane 합집합.
|
||||
- **Subset**: `(a & b) === a` — 매 a 가 매 b 안에.
|
||||
- **Higher priority**: 매 lower bit. `getHighestPriorityLane = lanes & -lanes`.
|
||||
- **Pending**: 매 fiber.lanes / fiber.childLanes — 매 자기 + 매 subtree 의 매 pending.
|
||||
|
||||
### 매 Lifecycle
|
||||
1. setState → `requestUpdateLane()` → 매 lane 결정 (event type / context).
|
||||
2. `markRootUpdated(root, lane)` → root.pendingLanes 에 매 OR.
|
||||
3. `ensureRootIsScheduled` → 매 highest priority lane 의 매 next render schedule.
|
||||
4. `performConcurrentWorkOnRoot` → 매 lane subset render. 매 yieldable.
|
||||
5. Commit 시 매 finishedLanes 를 매 pendingLanes 에서 매 clear.
|
||||
|
||||
### 매 응용
|
||||
1. startTransition — 매 user input 과 매 분리.
|
||||
2. useDeferredValue — 매 stale value 표시.
|
||||
3. Suspense retry — 매 별도 lane 으로 매 burst 방지.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Lane bitmask 기본
|
||||
```javascript
|
||||
// React internal
|
||||
const SyncLane = /* */ 0b0000000000000000000000000000001;
|
||||
const InputContinuousLane = /* */ 0b0000000000000000000000000000100;
|
||||
const DefaultLane = /* */ 0b0000000000000000000000000010000;
|
||||
const TransitionLane1 = /* */ 0b0000000000000000000000001000000;
|
||||
const IdleLane = /* */ 0b0010000000000000000000000000000;
|
||||
const OffscreenLane = /* */ 0b0100000000000000000000000000000;
|
||||
|
||||
function getHighestPriorityLane(lanes) {
|
||||
return lanes & -lanes; // isolate lowest set bit
|
||||
}
|
||||
function includesNonIdleWork(lanes) {
|
||||
return (lanes & ~IdleLanes) !== 0;
|
||||
}
|
||||
```
|
||||
|
||||
### startTransition (user code)
|
||||
```tsx
|
||||
import { startTransition, useState } from 'react';
|
||||
|
||||
const [tab, setTab] = useState('home');
|
||||
function select(next) {
|
||||
startTransition(() => setTab(next)); // → TransitionLane
|
||||
}
|
||||
```
|
||||
|
||||
### useDeferredValue
|
||||
```tsx
|
||||
const [query, setQuery] = useState('');
|
||||
const deferred = useDeferredValue(query); // lower priority lane
|
||||
return <SearchResults q={deferred} />;
|
||||
```
|
||||
|
||||
### Lane assignment by event
|
||||
```javascript
|
||||
// React internal: getCurrentEventPriority()
|
||||
function eventPriorityFromEvent(eventName) {
|
||||
switch (eventName) {
|
||||
case 'click': case 'input': case 'submit':
|
||||
return DiscreteEventPriority; // → SyncLane
|
||||
case 'drag': case 'scroll': case 'mousemove':
|
||||
return ContinuousEventPriority; // → InputContinuousLane
|
||||
default:
|
||||
return DefaultEventPriority; // → DefaultLane
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Render lane subset
|
||||
```javascript
|
||||
// performConcurrentWorkOnRoot (simplified)
|
||||
function renderRootConcurrent(root, lanes) {
|
||||
workInProgress = createWorkInProgress(root.current, null);
|
||||
workInProgressRootRenderLanes = lanes;
|
||||
while (workInProgress !== null && !shouldYield()) {
|
||||
performUnitOfWork(workInProgress);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Detect priority of update (debug)
|
||||
```javascript
|
||||
import { unstable_getCurrentPriorityLevel } from 'scheduler';
|
||||
console.log(unstable_getCurrentPriorityLevel());
|
||||
// 1 = Immediate, 2 = User-blocking, 3 = Normal, 4 = Low, 5 = Idle
|
||||
```
|
||||
|
||||
### Suspense retry lane
|
||||
```javascript
|
||||
// When boundary catches → throw to nearest Suspense → schedule retry on RetryLane
|
||||
// User code just renders <Suspense fallback={<Spin/>}><Lazy/></Suspense>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Lane |
|
||||
|---|---|
|
||||
| 매 click / input | SyncLane |
|
||||
| 매 scroll / drag | InputContinuousLane |
|
||||
| 매 setState in effect | DefaultLane |
|
||||
| 매 startTransition | TransitionLane |
|
||||
| 매 Suspense retry | RetryLane |
|
||||
| 매 hidden subtree pre-render | OffscreenLane |
|
||||
| 매 background prefetch | IdleLane |
|
||||
|
||||
**기본값**: 매 React 가 매 자동 선택. 매 user 는 매 startTransition / useDeferredValue 만 매 명시.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[React Fiber]]
|
||||
- 변형: (legacy) · [[Scheduler]]
|
||||
- 응용: [[startTransition]] · [[useDeferredValue]] · [[Suspense]]
|
||||
- Adjacent: [[Concurrent Features|Concurrent Rendering]] · [[Time_Slicing|Time Slicing]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 React internal 분석, 매 perf debug, 매 transition 설계.
|
||||
**언제 X**: 매 일반 product code — 매 자동 lane 선택 신뢰.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 setState 를 startTransition**: 매 input 의 매 instant feedback 손실.
|
||||
- **useDeferredValue 의 매 너무 깊은 위치**: 매 메모이제이션 풀림.
|
||||
- **Lane 직접 조작 시도**: 매 unstable internal API.
|
||||
- **Sync 강제 (flushSync) 남발**: 매 concurrent 이점 무.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 18/19 source — `ReactFiberLane.js`, Andrew Clark 의 매 RFC).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 매 lane bitmask + lifecycle |
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
id: wiki-2026-0508-large-scale-application-refactor
|
||||
title: Large-scale Application Refactoring
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [large refactor, monorepo refactor, architectural refactoring]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [refactoring, architecture, monorepo, frontend, migration]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Monorepo
|
||||
---
|
||||
|
||||
# Large-scale Application Refactoring
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 large-scale refactor 의 성공은 incremental migration + automated codemod + tight feedback loop"**. 매 big-bang rewrite 는 매 90% fail. 매 strangler fig pattern, type-safe boundaries, CI-enforced invariants 가 정답. 2026 의 AI-assisted refactor (Claude, Cursor) 는 매 codemod 를 augment.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 전략 종류
|
||||
- **Strangler Fig**: 매 old + new 동시 운영, gradual cutover.
|
||||
- **Branch by Abstraction**: 매 interface 추가 → impl 교체.
|
||||
- **Codemod**: 매 AST-based bulk rewrite (jscodeshift, ts-morph).
|
||||
- **Feature flag**: 매 new 코드 toggle, rollback 즉시.
|
||||
|
||||
### 매 사전 준비
|
||||
- **Test coverage 가 안전망**: 매 critical path 80%+ 권장.
|
||||
- **Type system 활용**: 매 TS strict, branded types 로 invariant.
|
||||
- **Metrics baseline**: 매 perf, bundle size, error rate 의 before/after.
|
||||
- **Rollback plan**: 매 feature flag, blue-green deploy.
|
||||
|
||||
### 매 응용
|
||||
1. JS → TS migration (Airbnb, Stripe).
|
||||
2. Pages Router → App Router (Next.js).
|
||||
3. Redux → Zustand / Jotai.
|
||||
4. CRA → Vite.
|
||||
5. Single repo → Turborepo / Nx monorepo.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Strangler Fig — old/new gateway
|
||||
```typescript
|
||||
// gateway.ts
|
||||
export async function fetchUser(id: string) {
|
||||
if (await flags.isEnabled("new-user-api", id)) {
|
||||
return newUserApi.get(id); // 매 new
|
||||
}
|
||||
return legacyUserApi.get(id); // 매 old fallback
|
||||
}
|
||||
// 매 traffic % gradually 100%
|
||||
```
|
||||
|
||||
### Codemod with ts-morph (rename API)
|
||||
```typescript
|
||||
import { Project } from "ts-morph";
|
||||
const project = new Project({ tsConfigFilePath: "./tsconfig.json" });
|
||||
|
||||
project.getSourceFiles().forEach((sf) => {
|
||||
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
||||
const expr = call.getExpression();
|
||||
if (expr.getText() === "oldFetch") {
|
||||
expr.replaceWithText("newFetch");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await project.save();
|
||||
// 매 매 file 수동 X — 매 1000 file 도 1초
|
||||
```
|
||||
|
||||
### Branch by Abstraction
|
||||
```typescript
|
||||
// 매 step 1: interface 도입
|
||||
interface UserStore {
|
||||
get(id: string): Promise<User>;
|
||||
save(u: User): Promise<void>;
|
||||
}
|
||||
|
||||
// 매 step 2: old impl 을 interface 뒤로
|
||||
class ReduxUserStore implements UserStore { /* ... */ }
|
||||
|
||||
// 매 step 3: 매 callsite 가 interface 사용 — 매 internal 구조 변경 가능
|
||||
const store: UserStore = useFlag("zustand") ? new ZustandUserStore() : new ReduxUserStore();
|
||||
```
|
||||
|
||||
### Type-driven migration (branded type)
|
||||
```typescript
|
||||
type UserId = string & { __brand: "UserId" };
|
||||
type LegacyUserId = string & { __brand: "LegacyUserId" };
|
||||
|
||||
function migrate(legacy: LegacyUserId): UserId {
|
||||
return `u_${legacy}` as UserId;
|
||||
}
|
||||
// 매 compiler 가 매 미migrate 사이트 catch
|
||||
```
|
||||
|
||||
### Dependency cruiser (architectural invariant)
|
||||
```javascript
|
||||
// .dependency-cruiser.cjs
|
||||
module.exports = {
|
||||
forbidden: [
|
||||
{
|
||||
name: "ui-no-import-server",
|
||||
severity: "error",
|
||||
from: { path: "^src/ui" },
|
||||
to: { path: "^src/server" },
|
||||
},
|
||||
],
|
||||
};
|
||||
// 매 CI 에서 violation block — 매 layer 침범 방지
|
||||
```
|
||||
|
||||
### Bundle size budget (CI enforce)
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"size-limit": [
|
||||
{ "path": "dist/main.js", "limit": "200 KB" },
|
||||
{ "path": "dist/vendor.js", "limit": "300 KB" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Mikado method (refactor 의 dependency tree)
|
||||
```
|
||||
Goal: Move auth from monolith to service
|
||||
├── Pre: extract auth interface (BLOCKED by tight coupling)
|
||||
│ ├── Pre: extract user model (DONE)
|
||||
│ └── Pre: remove direct DB access from auth (DONE)
|
||||
└── Pre: setup auth service skeleton (DONE)
|
||||
```
|
||||
|
||||
### AI-assisted bulk refactor (2026)
|
||||
```bash
|
||||
# 매 Cursor / Claude Code 에 codemod 보다 의도 기반 refactor
|
||||
# 매 "convert all class components to hooks" 같은 task 는
|
||||
# AI 가 context 이해하며 수정 — 매 codemod 의 limit 넘음
|
||||
|
||||
# 매 검증: dry-run + diff review + test run
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 변경 규모 | 전략 |
|
||||
|---|---|
|
||||
| 1-3 file | manual edit |
|
||||
| 10-100 file pattern | codemod (jscodeshift / ts-morph) |
|
||||
| Architectural | strangler fig + feature flag |
|
||||
| Cross-cutting (TS migration) | incremental + branded types |
|
||||
| Monorepo split | Nx / Turborepo migration |
|
||||
|
||||
**기본값**: incremental + feature flag + CI invariant. Big-bang rewrite 의 X.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Software Architecture]] · [[Refactoring_Best_Practices|Refactoring]]
|
||||
- 변형: [[Feature Flag]]
|
||||
- 응용: [[Monorepo|Monorepo Setup]]
|
||||
- Adjacent: [[Dependency Cruiser]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: refactor 전략 설계, codemod 작성, dependency rule 작성.
|
||||
**언제 X**: 매 trivial change (3 line) — 매 manual 가 빠름.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Big-bang rewrite**: 매 6 month freeze, 매 ship 못 함, 매 abandon 률 높음.
|
||||
- **No baseline metrics**: 매 "더 좋아졌다" 증명 X.
|
||||
- **No rollback plan**: 매 prod 사고 시 panic.
|
||||
- **Codemod without test**: 매 silent regression.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Martin Fowler "Refactoring", "Working Effectively with Legacy Code", Trunk-Based Dev).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — strangler fig + codemod + AI refactor |
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
id: wiki-2026-0508-levels-of-understanding
|
||||
title: Levels of Understanding
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Knowledge Hierarchy, Bloom's Taxonomy for Code]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [learning, mental-model, pedagogy, knowledge]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: meta
|
||||
framework: cognitive
|
||||
---
|
||||
|
||||
# Levels of Understanding
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 knowledge 의 매 layered model — 매 surface mimicry 매 deep generative comprehension 까지"**. Bloom's Taxonomy (1956) 와 Feynman Technique 의 매 영향, 매 software engineering 매 context 매 "can copy" → "can use" → "can debug" → "can teach" → "can extend" 매 progression.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 5 levels (engineering 적용)
|
||||
1. **Recognition**: 매 코드/concept 매 본 적 있음. 매 시 read-only.
|
||||
2. **Reproduction**: 매 example 매 따라 작성 가능. 매 copy-modify.
|
||||
3. **Application**: 매 새 context 매 동일 pattern 적용. 매 transfer.
|
||||
4. **Debugging**: 매 broken case 매 root cause 까지 trace. 매 inverse reasoning.
|
||||
5. **Generation**: 매 from-scratch design + teach others. 매 generative model 보유.
|
||||
|
||||
### 매 signals
|
||||
- **Recognition**: "본 적 있어" — but cannot describe.
|
||||
- **Reproduction**: tutorial 따라 동작하는 결과물 — but breaks on변형.
|
||||
- **Application**: 매 다른 problem 매 동일 solution 매 적용 — but mechanism 모름.
|
||||
- **Debugging**: error message 의 매 원인 가설 + 매 fix path 제안.
|
||||
- **Generation**: 매 design decision 매 trade-off 매 articulate + 매 alternative 매 비교.
|
||||
|
||||
### 매 응용
|
||||
1. Self-assessment: 매 새 tech 매 학습 매 어느 level 인지 정직히 평가.
|
||||
2. Hiring: interview 매 level 별 question 설계 (recognition vs generation).
|
||||
3. Onboarding: 매 신규 팀원 매 level 별 task 배정.
|
||||
4. Documentation 작성: target level 명시 (tutorial=reproduction, ADR=generation).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Self-test 매 Feynman Technique
|
||||
```
|
||||
1. 주제 선택 (e.g., "React Suspense")
|
||||
2. 매 8세 아이에게 매 설명 매 작성 (no jargon)
|
||||
3. 매 막힌 지점 = 매 understanding gap
|
||||
4. Source 매 다시 학습
|
||||
5. Repeat
|
||||
→ 매 step 4 매 도달하면 매 generation level
|
||||
```
|
||||
|
||||
### Code review 매 level probe
|
||||
```
|
||||
Reviewer: "왜 useMemo 사용?"
|
||||
- L1 답: "튜토리얼에서 봐서"
|
||||
- L2 답: "성능 최적화"
|
||||
- L3 답: "expensive computation 결과 캐시"
|
||||
- L4 답: "render마다 reference 변하면 child re-render — useMemo 매 stable reference"
|
||||
- L5 답: "이 case 는 child memo 안 했으니 useMemo 무용지물 — 제거 권장"
|
||||
```
|
||||
|
||||
### Bloom's mapping (engineering)
|
||||
```
|
||||
Remember → Recognition (L1)
|
||||
Understand → Reproduction (L2)
|
||||
Apply → Application (L3)
|
||||
Analyze → Debugging (L4)
|
||||
Evaluate → Debugging (L4)
|
||||
Create → Generation (L5)
|
||||
```
|
||||
|
||||
### Dreyfus 모델 비교
|
||||
```
|
||||
Novice ≈ L1 Recognition
|
||||
Advanced Beg ≈ L2 Reproduction
|
||||
Competent ≈ L3 Application
|
||||
Proficient ≈ L4 Debugging
|
||||
Expert ≈ L5 Generation
|
||||
```
|
||||
|
||||
### 매 progression 매 actionable steps
|
||||
```
|
||||
L1 → L2: Tutorial 매 따라 매 직접 type (copy-paste 금지)
|
||||
L2 → L3: 매 다른 problem 매 적용 (변형 challenge)
|
||||
L3 → L4: Bug 매 fix (intentional break + restore)
|
||||
L4 → L5: Source code 읽기 + 매 design doc 작성
|
||||
```
|
||||
|
||||
### Learning log template
|
||||
```markdown
|
||||
## <Topic>: <Date>
|
||||
- Level before: L?
|
||||
- 매 학습 source: <link>
|
||||
- 매 핵심 insight: <one sentence>
|
||||
- 매 confused 지점: <gap>
|
||||
- Level after: L?
|
||||
- Next: <action to advance>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Required Level |
|
||||
|---|---|
|
||||
| Use library in side project | L2 Reproduction |
|
||||
| Use in production | L3 Application |
|
||||
| Own subsystem in production | L4 Debugging |
|
||||
| Lead architecture / teach team | L5 Generation |
|
||||
| Write spec / RFC | L5 Generation |
|
||||
| Code review approver | L4+ |
|
||||
|
||||
**기본값**: 매 production code 매 책임지려면 매 L3 minimum, 매 L4 권장.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Mental_Models|Mental-Models]]
|
||||
- 응용: [[Code-Review]] · [[Onboarding]]
|
||||
- Adjacent: [[Deliberate-Practice]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 self-assessment, 매 학습 priorities 결정, 매 team capability mapping, 매 documentation target audience 명시.
|
||||
**언제 X**: 매 trivial task (level distinction overkill), 매 emotional/soft skills (different framework).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **L2 → "할 줄 안다" 자칭**: 매 Dunning-Kruger — tutorial 통과 ≠ application.
|
||||
- **L1 결과물 매 production 배포**: 매 Stack Overflow copy-paste — 매 silent failure 보장.
|
||||
- **Level 매 fixed trait 가정**: 매 매 domain 별 level 다름 — React L5 + Rust L1 매 가능.
|
||||
- **Recognition 매 understanding 혼동**: 매 "들어봤다" ≠ "이해한다".
|
||||
- **Generation 매 가정 매 검증 X**: 매 자칭 L5 매 실제 L3 매 case 흔함 — Feynman test.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Bloom 1956, Dreyfus 1980, Feynman lectures).
|
||||
- 신뢰도 A (cognitive science consensus).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 5-level engineering taxonomy with progression patterns |
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
id: wiki-2026-0508-memory-leak-prevention-메모리-누수-방지
|
||||
title: Memory Leak Prevention 메모리 누수 방지
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [memory leak, JS leak, frontend memory leak]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [memory, leak, performance, javascript, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: Browser/Node
|
||||
---
|
||||
|
||||
# Memory Leak Prevention 메모리 누수 방지
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 GC 가 있어도 reachable reference 는 free 안 됨 — leak 의 본질은 매 잊혀진 reference"**. SPA 의 long-lived session 에서 매 점진적 RAM 증가 → tab crash. 매 listener cleanup, closure escape, detached DOM, timer 가 4대 leak. 매 Chrome DevTools Memory + WeakRef 가 무기.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4대 leak 패턴
|
||||
- **Listener leak**: 매 `addEventListener` 후 `removeEventListener` 없음.
|
||||
- **Closure escape**: 매 long-lived obj 에 short-lived 의 reference 가 capture.
|
||||
- **Detached DOM**: 매 DOM remove 했지만 JS reference 가 살아있음.
|
||||
- **Timer leak**: 매 `setInterval` clear 안 함 → callback 의 closure 누적.
|
||||
|
||||
### 매 진단 도구
|
||||
- **Chrome DevTools → Memory → Heap snapshot**: 매 N times 비교 → growing object.
|
||||
- **Performance → Memory checkbox**: 매 timeline.
|
||||
- **`performance.memory` API**: 매 ad-hoc check.
|
||||
- **Node `--inspect` + `--expose-gc`**: 매 server-side.
|
||||
|
||||
### 매 응용
|
||||
1. SPA route change 마다 cleanup.
|
||||
2. React component unmount cleanup.
|
||||
3. WebGL / Three.js 의 `dispose()` 호출.
|
||||
4. Worker `postMessage` cycle 깨기.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Listener cleanup (vanilla)
|
||||
```javascript
|
||||
class Component {
|
||||
constructor(el) {
|
||||
this.el = el;
|
||||
this.onClick = this.onClick.bind(this);
|
||||
el.addEventListener("click", this.onClick);
|
||||
}
|
||||
destroy() {
|
||||
this.el.removeEventListener("click", this.onClick); // 매 필수
|
||||
this.el = null;
|
||||
}
|
||||
onClick() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### React useEffect cleanup
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
const id = setInterval(tick, 1000);
|
||||
const ws = new WebSocket(url);
|
||||
ws.onmessage = handle;
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
ws.close();
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [url]);
|
||||
```
|
||||
|
||||
### AbortController (modern listener cleanup)
|
||||
```javascript
|
||||
const ac = new AbortController();
|
||||
window.addEventListener("scroll", onScroll, { signal: ac.signal });
|
||||
fetch(url, { signal: ac.signal });
|
||||
// 매 한 번에 모두 cancel + listener remove
|
||||
ac.abort();
|
||||
```
|
||||
|
||||
### Three.js dispose chain
|
||||
```javascript
|
||||
function disposeMesh(mesh) {
|
||||
mesh.geometry.dispose();
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach((m) => disposeMaterial(m));
|
||||
} else {
|
||||
disposeMaterial(mesh.material);
|
||||
}
|
||||
scene.remove(mesh);
|
||||
}
|
||||
function disposeMaterial(m) {
|
||||
Object.values(m).forEach((v) => {
|
||||
if (v && v.isTexture) v.dispose();
|
||||
});
|
||||
m.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### WeakMap for metadata (no leak)
|
||||
```javascript
|
||||
// 매 strong Map 은 key 가 leak — WeakMap 은 GC 가능
|
||||
const meta = new WeakMap();
|
||||
function tag(node, info) {
|
||||
meta.set(node, info); // 매 node remove 시 자동 free
|
||||
}
|
||||
```
|
||||
|
||||
### WeakRef for cache
|
||||
```javascript
|
||||
class ImageCache {
|
||||
cache = new Map();
|
||||
get(url) {
|
||||
const ref = this.cache.get(url);
|
||||
const img = ref?.deref();
|
||||
if (img) return img;
|
||||
const fresh = new Image();
|
||||
fresh.src = url;
|
||||
this.cache.set(url, new WeakRef(fresh));
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
// 매 메모리 압박 시 GC 가 free
|
||||
```
|
||||
|
||||
### Detached DOM 진단
|
||||
```javascript
|
||||
// Chrome DevTools Memory → Filter "Detached"
|
||||
// 또는 코드로:
|
||||
function findDetached() {
|
||||
const nodes = [];
|
||||
const all = performance.memory; // (chrome only ad-hoc)
|
||||
// 매 heap snapshot 이 정확
|
||||
}
|
||||
// 매 React 의 stale ref 가 흔한 원인
|
||||
```
|
||||
|
||||
### Closure leak (and fix)
|
||||
```javascript
|
||||
// 매 BAD: 매 onClose 가 매 hugeBuffer 의 reference 유지
|
||||
function setup() {
|
||||
const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024);
|
||||
const ws = new WebSocket(url);
|
||||
ws.onclose = () => console.log("closed", Date.now());
|
||||
// 매 onclose closure 가 entire scope capture → hugeBuffer leak
|
||||
}
|
||||
|
||||
// 매 GOOD
|
||||
function setup() {
|
||||
const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024);
|
||||
process(hugeBuffer);
|
||||
const ws = new WebSocket(url);
|
||||
ws.onclose = onCloseStandalone; // 매 module-level fn
|
||||
}
|
||||
function onCloseStandalone() { console.log("closed", Date.now()); }
|
||||
```
|
||||
|
||||
### Heap diff workflow
|
||||
```
|
||||
1. Open DevTools → Memory → Heap snapshot (S1)
|
||||
2. 매 user action repeat 10 times
|
||||
3. Take snapshot (S2)
|
||||
4. Compare: S2 vs S1 → "Allocation" tab
|
||||
5. 매 N times grew by 10 → suspect
|
||||
6. Inspect retainer chain — 매 root 까지 추적
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 의심 | 첫 진단 |
|
||||
|---|---|
|
||||
| Slow degradation | 3-snapshot heap diff |
|
||||
| Sudden spike | timeline + allocation sampling |
|
||||
| Timer suspicion | `clearInterval` audit |
|
||||
| WebGL app | `renderer.info.memory` watch |
|
||||
| React | StrictMode + DevTools Profiler |
|
||||
|
||||
**기본값**: AbortController 로 listener, useEffect cleanup, WeakMap metadata, dispose() WebGL.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Garbage Collection]] · [[Performance Optimization]]
|
||||
- 변형: [[Detached DOM]]
|
||||
- Adjacent: [[WeakRef]] · [[AbortController]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: SPA leak 진단, useEffect cleanup 작성, dispose chain 설계.
|
||||
**언제 X**: 매 short-lived script — 매 overhead 정당화 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No cleanup in useEffect**: 매 unmount 후 listener 살아있음.
|
||||
- **Strong Map for DOM metadata**: 매 WeakMap 사용.
|
||||
- **setInterval without ref**: 매 clear 불가능.
|
||||
- **Forgetting WebGL dispose**: 매 GPU memory leak — JS GC 가 도움 X.
|
||||
- **Global cache unbounded**: 매 LRU + size limit 또는 WeakRef.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Chrome Developers "Fix memory problems", MDN, web.dev/memory-leaks).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 4대 leak + 9 patterns + WeakRef/AbortController |
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
id: wiki-2026-0508-memory-leak-debugging-in-javascr
|
||||
title: Memory Leak Debugging in JavaScript
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [JS Memory Leak, Heap Leak Debugging]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [javascript, performance, debugging, memory]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript
|
||||
framework: Chrome DevTools
|
||||
---
|
||||
|
||||
# Memory Leak Debugging in JavaScript
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 unintended retention — 매 GC 매 reach 가능한 reference chain 매 끊지 못해 매 heap 매 grows unbounded"**. JS 매 mark-and-sweep GC 자동이지만 매 closure/listener/global/timer 매 long-lived reference 매 object lifecycle 매 의도와 분리시키면 매 leak 발생, 매 Chrome DevTools Heap Snapshot 매 진단 standard.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 leak sources (top 5)
|
||||
- **Detached DOM nodes**: 매 element removed from tree 매 JS reference 잔존.
|
||||
- **Event listeners**: 매 addEventListener 매 removeEventListener 없이 매 component unmount.
|
||||
- **Timers**: setInterval/setTimeout 매 cleanup 누락 매 closure 매 모두 retain.
|
||||
- **Closures**: outer scope variables 매 inner function 매 capture 후 매 long-lived.
|
||||
- **Global accumulation**: window/globalThis 매 cache/array 매 unbounded push.
|
||||
|
||||
### 매 detection tools
|
||||
- **Chrome DevTools Memory**: Heap snapshot, allocation timeline, allocation sampling.
|
||||
- **performance.measureUserAgentSpecificMemory()** (Chrome 89+): 매 cross-origin isolated context.
|
||||
- **Node.js**: --inspect + Chrome DevTools, heapdump module, --heap-prof flag.
|
||||
- **WeakRef + FinalizationRegistry**: 매 GC 관찰 (debugging only).
|
||||
|
||||
### 매 응용
|
||||
1. SPA 매 route navigation 매 retain leak 진단.
|
||||
2. Long-running dashboard 매 hour-scale leak 감시.
|
||||
3. Node.js server 매 RSS growth 매 root cause.
|
||||
4. React/Vue component lifecycle leak detection.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Heap snapshot 3-snapshot technique
|
||||
```
|
||||
1. App 초기 load → Snapshot 1 (baseline)
|
||||
2. Suspect action 수행 (modal open/close ×10) → Snapshot 2
|
||||
3. 동일 action 재수행 → Snapshot 3
|
||||
4. Snapshot 3 의 Comparison → Snapshot 1
|
||||
5. "Allocated between snapshots 1 and 3" 의 still-alive objects = leak
|
||||
```
|
||||
|
||||
### Detached DOM 탐색 (DevTools Console)
|
||||
```js
|
||||
// Heap snapshot Class filter:
|
||||
// "Detached HTMLDivElement"
|
||||
// "Detached HTMLElement"
|
||||
// 매 instance 매 retainer chain 매 inspect — 매 root retainer 매 leak 출처
|
||||
```
|
||||
|
||||
### Event listener leak — fix pattern
|
||||
```js
|
||||
// 매 BAD
|
||||
class Widget {
|
||||
constructor() {
|
||||
window.addEventListener('resize', this.onResize.bind(this));
|
||||
}
|
||||
onResize() { /* ... */ }
|
||||
destroy() { /* listener still attached */ }
|
||||
}
|
||||
|
||||
// 매 GOOD
|
||||
class Widget {
|
||||
constructor() {
|
||||
this.onResize = this.onResize.bind(this);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
}
|
||||
onResize() { /* ... */ }
|
||||
destroy() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AbortController 매 modern cleanup
|
||||
```js
|
||||
class Component {
|
||||
constructor() {
|
||||
this.ac = new AbortController();
|
||||
const { signal } = this.ac;
|
||||
window.addEventListener('scroll', this.onScroll, { signal });
|
||||
window.addEventListener('resize', this.onResize, { signal });
|
||||
fetch('/api', { signal });
|
||||
}
|
||||
destroy() {
|
||||
this.ac.abort(); // 매 모든 listener + fetch 매 한 번에 cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timer leak fix
|
||||
```js
|
||||
// 매 BAD — closure captures large data
|
||||
function startPolling(bigData) {
|
||||
setInterval(() => {
|
||||
console.log(bigData.length); // bigData retained forever
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 매 GOOD — explicit handle + cleanup
|
||||
const handle = setInterval(poll, 1000);
|
||||
function stop() { clearInterval(handle); }
|
||||
```
|
||||
|
||||
### WeakMap 매 cache without leak
|
||||
```js
|
||||
// 매 BAD — Map 매 key 매 GC X
|
||||
const cache = new Map();
|
||||
function getMeta(node) {
|
||||
if (!cache.has(node)) cache.set(node, computeMeta(node));
|
||||
return cache.get(node); // node removed from DOM but still in cache
|
||||
}
|
||||
|
||||
// 매 GOOD — WeakMap key 매 GC 가능
|
||||
const cache = new WeakMap();
|
||||
function getMeta(node) {
|
||||
if (!cache.has(node)) cache.set(node, computeMeta(node));
|
||||
return cache.get(node);
|
||||
}
|
||||
```
|
||||
|
||||
### performance.measureUserAgentSpecificMemory
|
||||
```js
|
||||
// crossOriginIsolated context (COOP+COEP headers) 필요
|
||||
if (crossOriginIsolated && performance.measureUserAgentSpecificMemory) {
|
||||
const result = await performance.measureUserAgentSpecificMemory();
|
||||
console.log('bytes:', result.bytes);
|
||||
console.table(result.breakdown);
|
||||
}
|
||||
```
|
||||
|
||||
### FinalizationRegistry 매 GC 관찰 (debug only)
|
||||
```js
|
||||
const registry = new FinalizationRegistry((tag) => {
|
||||
console.log(`GC'd: ${tag}`);
|
||||
});
|
||||
|
||||
class Tracked {
|
||||
constructor(name) {
|
||||
registry.register(this, name);
|
||||
}
|
||||
}
|
||||
|
||||
new Tracked('widget-1'); // → "GC'd: widget-1" eventually (or never)
|
||||
```
|
||||
|
||||
### Node.js heap snapshot
|
||||
```bash
|
||||
node --inspect server.js
|
||||
# 매 chrome://inspect → Memory → Take heap snapshot
|
||||
# 또는 programmatic:
|
||||
```
|
||||
```js
|
||||
import { writeHeapSnapshot } from 'node:v8';
|
||||
const path = writeHeapSnapshot(); // .heapsnapshot file
|
||||
console.log(`Snapshot: ${path}`);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Tool/Approach |
|
||||
|---|---|
|
||||
| Browser SPA growing memory | DevTools Heap Snapshot 3-snapshot |
|
||||
| 매 frame allocation hotspot | Allocation timeline (sampling) |
|
||||
| Detached DOM 의심 | Class filter "Detached " in snapshot |
|
||||
| Node.js RSS growth | writeHeapSnapshot + Chrome DevTools |
|
||||
| Continuous monitoring (production) | performance.measureUserAgentSpecificMemory |
|
||||
| Event listener leak | AbortController 매 unified cleanup |
|
||||
|
||||
**기본값**: 매 Heap Snapshot 3-snapshot diff — 매 retainer chain 매 따라 root 매 식별.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Garbage Collection]]
|
||||
- Adjacent: [[Chrome DevTools]] · [[AbortController]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 SPA/long-running app 의 메모리 증가, 매 unmount 후 referent 잔존, 매 production memory metrics 의 anomaly.
|
||||
**언제 X**: 매 short-lived script (CLI tool), 매 GC pause 문제 (different — GC tuning territory).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`delete` keyword 의존**: 매 reference 매 nullify 안 함 — 매 다른 reference 매 retain.
|
||||
- **`window.gc()` 매 production**: 매 only with --expose-gc flag, 매 hint 일 뿐.
|
||||
- **Allocation timeline 매 production trace**: 매 overhead 매 큼 — 매 staging 에서.
|
||||
- **One-snapshot 진단**: 매 baseline 없으면 매 noise 와 leak 매 구분 불가.
|
||||
- **DevTools 매 incognito 가정**: 매 extension 매 heap pollution — 매 incognito + 매 disabled extensions.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Chrome DevTools docs, V8 blog, Node.js v8 module).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — leak source taxonomy + DevTools workflow + AbortController/WeakMap patterns |
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
id: wiki-2026-0508-meta-quest-store
|
||||
title: Meta Quest Store
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Quest Store, Oculus Store, Horizon Store]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [vr, xr, meta-quest, distribution, store]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: csharp
|
||||
framework: unity-unreal
|
||||
---
|
||||
|
||||
# Meta Quest Store
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 VR 앱 의 primary distribution channel — Quest 2/3/Pro 의 default storefront"**. Meta 가 운영하는 curated VR 앱 store. 매 standalone Quest device 에서 매 install 의 표준 경로. 2024+ 부터 App Lab merge → 매 single Horizon Store 로 통합 (curated + open submission).
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 distribution tier (2024+ 통합 후)
|
||||
- **Main Store (curated)**: 매 quality bar (performance, content, polish) 통과, 매 marketing 노출.
|
||||
- **App Lab (legacy → integrated)**: 매 lower bar, 매 deeplink/search 만 으로 발견. 2024 부터 Main Store 와 merge.
|
||||
- **Sideload (SideQuest)**: 매 store 외 distribution, dev mode 활성화 필요.
|
||||
|
||||
### 매 submission requirements
|
||||
- **VRC (Virtual Reality Check)**: TOS, performance (72/90/120Hz target), comfort (locomotion, vection 경고).
|
||||
- **Privacy policy + data use disclosure**.
|
||||
- **App rating** (IARC).
|
||||
- **Unity / Unreal / Native** 모두 지원, OpenXR 권장.
|
||||
|
||||
### 매 응용
|
||||
1. Indie VR game launch (App Lab → graduation to curated).
|
||||
2. B2B training app (Quest for Business channel).
|
||||
3. WebXR app (browser-based, store 우회 가능).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Unity OpenXR project setup (Quest target)
|
||||
```csharp
|
||||
// ProjectSettings — XR Plug-in Management → Android → Oculus
|
||||
// Edit/Project Settings/Player/Android:
|
||||
// - Minimum API Level: Android 10 (29) — Quest 2/3 baseline
|
||||
// - Target API Level: Android 13 (33)
|
||||
// - Scripting Backend: IL2CPP
|
||||
// - Target Architectures: ARM64
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.OpenXR;
|
||||
using UnityEngine.XR.OpenXR.Features.MetaQuestSupport;
|
||||
|
||||
public class QuestBootstrap : MonoBehaviour {
|
||||
void Start() {
|
||||
Application.targetFrameRate = 90; // Quest 3 default
|
||||
OVRManager.fixedFoveatedRenderingLevel =
|
||||
OVRManager.FixedFoveatedRenderingLevel.High;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manifest — Quest features
|
||||
```xml
|
||||
<!-- AndroidManifest.xml -->
|
||||
<manifest>
|
||||
<uses-feature android:name="android.hardware.vr.headtracking"
|
||||
android:required="true" android:version="1" />
|
||||
<uses-feature android:name="oculus.software.handtracking"
|
||||
android:required="false" />
|
||||
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
|
||||
<application>
|
||||
<meta-data android:name="com.oculus.supportedDevices"
|
||||
android:value="quest2|quest3|questpro" />
|
||||
<activity android:name=".UnityPlayerActivity">
|
||||
<intent-filter>
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### Performance budget (Quest 3 target)
|
||||
```csharp
|
||||
public class PerfMonitor : MonoBehaviour {
|
||||
void Update() {
|
||||
float gpuTime = OVRPlugin.GetAppGpuTimeInSeconds() * 1000f;
|
||||
float cpuTime = OVRPlugin.GetAppCpuTimeInSeconds() * 1000f;
|
||||
if (gpuTime > 11f) Debug.LogWarning($"GPU over budget: {gpuTime}ms");
|
||||
// 매 90Hz = 11.1ms budget, 매 120Hz = 8.3ms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hand tracking
|
||||
```csharp
|
||||
using Oculus.Interaction.Input;
|
||||
|
||||
public class HandPinch : MonoBehaviour {
|
||||
[SerializeField] Hand hand;
|
||||
void Update() {
|
||||
if (hand.GetFingerIsPinching(HandFinger.Index)) {
|
||||
// 매 trigger interaction
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build → store upload (CLI)
|
||||
```bash
|
||||
# 매 ovr-platform-util 의 store upload
|
||||
ovr-platform-util upload-quest-build \
|
||||
--app-id $QUEST_APP_ID \
|
||||
--app-secret $QUEST_APP_SECRET \
|
||||
--apk build/MyApp.apk \
|
||||
--channel ALPHA \
|
||||
--notes "Build 1.2.0 — 90Hz support"
|
||||
```
|
||||
|
||||
### Asset bundle / DLC (Quest)
|
||||
```csharp
|
||||
// 매 Cloud Storage API 로 DLC delivery
|
||||
var cloudStorage = new CloudStorage2();
|
||||
cloudStorage.Save("save.dat", saveBytes); // 매 cross-device sync
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 새 VR title | OpenXR + Unity/Unreal, Quest 3 baseline (90Hz) |
|
||||
| 매 broad reach | Main Store curated submission (VRC 통과) |
|
||||
| 매 fast iteration | App Lab tier (lower bar) |
|
||||
| 매 enterprise | Quest for Business channel |
|
||||
| Cross-platform (PSVR2/Pico) | OpenXR runtime abstraction |
|
||||
|
||||
**기본값**: OpenXR + Quest 3 (90Hz, hand tracking, MR passthrough).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Virtual-Reality]]
|
||||
- Adjacent: [[WebXR]] · [[Mixed-Reality]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Quest 앱 출시 계획, performance budget 책정, store policy 검토.
|
||||
**언제 X**: 매 PCVR-only (Steam), 매 mobile AR (ARKit/ARCore).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **PCVR-quality asset 의 Quest 그대로 ship**: GPU/memory 한계 — 매 LOD/texture compression 강제.
|
||||
- **Locomotion 의 comfort 옵션 의 X**: VRC fail.
|
||||
- **TargetFrameRate 미설정**: 매 OS default 의 의존, 매 inconsistent.
|
||||
- **Privacy policy 의 dummy URL**: submission reject.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (developer.oculus.com, Meta Horizon OS docs 2024).
|
||||
- 신뢰도 B+ (policy 가 매 evolve).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Quest Store distribution + perf 정리 |
|
||||
@@ -0,0 +1,263 @@
|
||||
---
|
||||
id: wiki-2026-0508-micro-frontends
|
||||
title: Micro frontends
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [MFE, Micro-frontend Architecture]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, architecture, modular]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Module Federation / Single-SPA
|
||||
---
|
||||
|
||||
# Micro frontends
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 microservices 매 frontend 적용 — 매 large UI 매 independent deployable team-owned vertical slices 매 분할"**. ThoughtWorks Tech Radar (2016) 매 이름 채택, 매 Webpack 5 Module Federation (2020) 매 mainstream 진입, 매 2026 매 large enterprise frontend (Spotify, IKEA, DAZN) 매 standard, 매 Vite Module Federation + Native Federation 매 modern stack.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 가치 (vs monolith)
|
||||
- **Independent deploy**: 매 team 매 자기 slice 매 release pipeline.
|
||||
- **Tech heterogeneity**: 매 React + Vue + Svelte 매 공존 (단 cost ↑).
|
||||
- **Team scalability**: 매 vertical team ownership — 매 Conway's law 매 align.
|
||||
- **Incremental migration**: 매 legacy → modern 매 점진적 교체.
|
||||
|
||||
### 매 cost
|
||||
- **Bundle duplication**: 매 framework 매 여러 번 load.
|
||||
- **Cross-MFE state**: 매 shared store 매 design.
|
||||
- **Routing coordination**: 매 shell + child route 매 sync.
|
||||
- **Versioning skew**: 매 shared dep 매 version conflict.
|
||||
- **DX overhead**: 매 dev 매 multi-repo / multi-server.
|
||||
|
||||
### 매 composition strategies
|
||||
- **Build-time**: npm package — 매 monorepo + 매 publish (closest to monolith).
|
||||
- **Server-side**: SSI/ESI/Tailor — 매 edge composition (Podium, Mosaic).
|
||||
- **Run-time iframe**: 매 ultimate isolation — 매 worst UX.
|
||||
- **Run-time Web Components**: 매 framework-agnostic embed.
|
||||
- **Run-time Module Federation** (Webpack 5 / Vite): 매 sharing dep + dynamic import — 매 dominant 2026.
|
||||
- **Run-time Single-SPA**: 매 lifecycle orchestration framework.
|
||||
|
||||
### 매 응용
|
||||
1. Large e-commerce (header/cart/product/checkout 매 별도 team).
|
||||
2. Multi-tenant SaaS (workspace + apps marketplace).
|
||||
3. Legacy modernization (strangler fig pattern).
|
||||
4. White-label platforms (per-customer customization).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Webpack Module Federation — host (shell)
|
||||
```js
|
||||
// shell/webpack.config.js
|
||||
const { ModuleFederationPlugin } = require('webpack').container
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: 'shell',
|
||||
remotes: {
|
||||
cart: 'cart@https://cart.example.com/remoteEntry.js',
|
||||
product: 'product@https://product.example.com/remoteEntry.js',
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true, requiredVersion: '^18.0.0' },
|
||||
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Module Federation — remote (cart MFE)
|
||||
```js
|
||||
// cart/webpack.config.js
|
||||
new ModuleFederationPlugin({
|
||||
name: 'cart',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./CartWidget': './src/CartWidget',
|
||||
'./useCart': './src/hooks/useCart',
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true },
|
||||
'react-dom': { singleton: true },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Host consume (React lazy)
|
||||
```tsx
|
||||
// shell/App.tsx
|
||||
import React, { lazy, Suspense } from 'react'
|
||||
|
||||
const CartWidget = lazy(() => import('cart/CartWidget'))
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Suspense fallback={<div>Loading cart…</div>}>
|
||||
<CartWidget />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Vite Module Federation
|
||||
```ts
|
||||
// cart/vite.config.ts
|
||||
import federation from '@originjs/vite-plugin-federation'
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
federation({
|
||||
name: 'cart',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: { './CartWidget': './src/CartWidget.tsx' },
|
||||
shared: ['react', 'react-dom'],
|
||||
}),
|
||||
],
|
||||
build: { target: 'esnext', minify: false, cssCodeSplit: true },
|
||||
}
|
||||
```
|
||||
|
||||
### Single-SPA root config
|
||||
```ts
|
||||
// root-config/index.ts
|
||||
import { registerApplication, start } from 'single-spa'
|
||||
|
||||
registerApplication({
|
||||
name: '@org/cart',
|
||||
app: () => System.import('@org/cart'),
|
||||
activeWhen: ['/cart'],
|
||||
})
|
||||
|
||||
registerApplication({
|
||||
name: '@org/product',
|
||||
app: () => System.import('@org/product'),
|
||||
activeWhen: ['/products'],
|
||||
})
|
||||
|
||||
start()
|
||||
```
|
||||
|
||||
### Single-SPA child lifecycle
|
||||
```ts
|
||||
// cart-app/src/main.ts
|
||||
import { h, createApp } from 'vue'
|
||||
import singleSpaVue from 'single-spa-vue'
|
||||
import Root from './Root.vue'
|
||||
|
||||
const lifecycles = singleSpaVue({
|
||||
createApp,
|
||||
appOptions: { render: () => h(Root) },
|
||||
})
|
||||
|
||||
export const bootstrap = lifecycles.bootstrap
|
||||
export const mount = lifecycles.mount
|
||||
export const unmount = lifecycles.unmount
|
||||
```
|
||||
|
||||
### Web Components 매 framework-agnostic embed
|
||||
```ts
|
||||
// cart-mfe.ts
|
||||
class CartElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const root = this.attachShadow({ mode: 'open' })
|
||||
// mount React/Vue/Svelte into shadow DOM
|
||||
mountReact(<CartWidget />, root)
|
||||
}
|
||||
disconnectedCallback() { unmountReact(this.shadowRoot!) }
|
||||
}
|
||||
customElements.define('app-cart', CartElement)
|
||||
```
|
||||
```html
|
||||
<!-- 매 host page (any framework) -->
|
||||
<script src="https://cart.example.com/cart.js" type="module"></script>
|
||||
<app-cart></app-cart>
|
||||
```
|
||||
|
||||
### Cross-MFE communication — Custom Events
|
||||
```ts
|
||||
// 매 publish
|
||||
window.dispatchEvent(new CustomEvent('cart:item-added', {
|
||||
detail: { sku: 'A123', qty: 1 },
|
||||
}))
|
||||
|
||||
// 매 subscribe
|
||||
window.addEventListener('cart:item-added', (e) => {
|
||||
console.log('item added:', (e as CustomEvent).detail)
|
||||
})
|
||||
```
|
||||
|
||||
### Shared shell store (BroadcastChannel)
|
||||
```ts
|
||||
const channel = new BroadcastChannel('app-state')
|
||||
|
||||
// MFE A
|
||||
channel.postMessage({ type: 'auth/login', user })
|
||||
|
||||
// MFE B
|
||||
channel.onmessage = ({ data }) => {
|
||||
if (data.type === 'auth/login') updateLocalState(data.user)
|
||||
}
|
||||
```
|
||||
|
||||
### CSS isolation strategies
|
||||
```
|
||||
1. Shadow DOM (Web Components 매 자동 scope)
|
||||
2. CSS Modules (build-time hash)
|
||||
3. CSS-in-JS (runtime scope, e.g., styled-components)
|
||||
4. PostCSS namespace plugin (legacy)
|
||||
5. Tailwind prefix per MFE: `tw-cart-`, `tw-product-`
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Single team, single product | Monolith (MFE overkill) |
|
||||
| Multiple teams, shared shell | Module Federation |
|
||||
| Legacy → React migration | iframe → Web Components → MF |
|
||||
| Framework heterogeneity | Web Components / Single-SPA |
|
||||
| Maximum isolation (security) | iframe (last resort) |
|
||||
| Tight budget (LCP critical) | Build-time composition |
|
||||
| SSR + MFE | Server-side composition (Tailor, Mosaic) |
|
||||
| Vite stack | Native Federation / vite-plugin-federation |
|
||||
|
||||
**기본값**: 매 React/Vue 균일 stack 매 Module Federation, 매 framework 혼재 매 Single-SPA + Web Components.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[프론트엔드 및 UIUX 표준|Frontend-Architecture]] · [[Microservices]]
|
||||
- 변형: [[Module-Federation]] · [[Web-Components]]
|
||||
- Adjacent: [[Monorepo]] · [[Vite]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 large team (50+ FE devs), 매 independent deploy 필요, 매 legacy modernization, 매 multi-tenant white-label.
|
||||
**언제 X**: 매 small team (<10), 매 single-page simple SPA, 매 LCP-critical landing — 매 monolith 적합.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 component-level MFE**: 매 button 매 MFE — 매 latency overhead — 매 vertical slice 매 단위.
|
||||
- **Shared global state via window**: 매 race condition + 매 hidden coupling.
|
||||
- **Framework version skew**: 매 React 17 + 18 매 host — 매 hooks 깨짐.
|
||||
- **No design system**: 매 visual inconsistency — 매 shared tokens/components 필수.
|
||||
- **Synchronous load chain**: 매 cascade waterfall — 매 lazy + 매 parallel 매 host.
|
||||
- **MFE 매 small team 매 도입**: 매 over-engineering — 매 monolith + 매 module boundary 매 충분.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (ThoughtWorks Tech Radar, micro-frontends.org, Module Federation docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — composition strategies + MF/Single-SPA/WC patterns + comm matrix |
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
category: Architecture
|
||||
tags: [auto-wikified, technical-documentation, architecture]
|
||||
title: Mixins
|
||||
description: "믹스인(Mixins)은 Vue."
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Mixins
|
||||
|
||||
## 📌 Brief Summary
|
||||
믹스인(Mixins)은 Vue.js 프레임워크의 Options API 환경에서 코드 재사용을 위해 주로 사용되던 전통적인 패턴입니다 [1]. 그러나 대규모 애플리케이션으로 확장되는 현대적인 Vue 3 개발 환경에서는 믹스인이 가진 구조적인 한계로 인해 사용이 지양되고 있습니다 [2]. 오늘날 엔터프라이즈 코드베이스에서는 반응형 로직을 더 투명하고 타입 안전(type-safe)하게 공유할 수 있는 컴포저블(Composables) 패턴으로 효과적으로 대체되었습니다 [2].
|
||||
|
||||
## 📖 Core Content
|
||||
* **Options API의 기본 재사용 접근법**: 믹스인은 재사용성 요구가 낮고 단일 기능을 수행하는 작고 단순한 컴포넌트를 설계할 때 Options API가 채택하던 주된 코드 재사용 방식이었습니다 [1].
|
||||
* **Composables로의 진화**: 엔터프라이즈급 대규모 웹 애플리케이션에서는 컴포넌트를 뷰(view) 계층에 집중시키고 상태 저장 로직(stateful logic)을 효과적으로 분리하기 위해 믹스인 대신 컴포저블을 사용합니다 [2].
|
||||
* **상속보다 합성(Composition over Inheritance)**: 비즈니스 규칙 등을 캡슐화할 때 믹스인과 같은 상속 기반의 패턴 대신, 독립적인 함수를 구성하는 '상속보다 합성' 접근 방식이 최신 아키텍처의 핵심으로 자리 잡았습니다 [3].
|
||||
|
||||
## ⚖️ Trade-offs & Caveats
|
||||
믹스인 패턴은 재사용성을 제공하지만, 프로젝트의 규모가 커질 경우 다음과 같은 명확한 기술적 부작용과 제약 사항을 초래합니다.
|
||||
|
||||
* **이름 충돌(Naming Collision) 문제**: 여러 믹스인을 하나의 컴포넌트에 통합하여 사용할 경우, 각 믹스인이 가진 변수명이나 메서드명이 동일하여 런타임 시 충돌이 일어나는 고전적인 문제가 발생합니다 [3].
|
||||
* **숨겨진 데이터(Hidden Data) 문제**: 데이터나 로직의 출처가 불분명해져 어떤 믹스인에서 특정 상태가 유래했는지 추적하기 어려워지는 문제가 발생합니다 [3]. 이는 결과적으로 코드의 가독성을 해치고 디버깅을 어렵게 만듭니다.
|
||||
* **타입 안정성의 한계**: 반환되는 참조(refs)와 함수를 통해 인터페이스를 명시적으로 정의하는 컴포저블과 비교할 때, 믹스인은 타입 안전성(Type-safe)이 크게 떨어집니다 [2, 3]. 이는 코드베이스가 복잡해질수록 예상치 못한 오류를 유발할 위험을 높입니다 [2].
|
||||
|
||||
---
|
||||
*Last updated: 2026-05-03*
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
id: wiki-2026-0508-mobile-augmented-reality
|
||||
title: Mobile Augmented Reality
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Mobile AR, Smartphone AR]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [ar, mobile, computer-vision, 3d]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: Swift / Kotlin / C#
|
||||
framework: ARKit 7 / ARCore / Unity AR Foundation
|
||||
---
|
||||
|
||||
# Mobile Augmented Reality
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 스마트폰 매 camera 매 real-time 3D 매 합성 — 매 SLAM 기반 device-pose tracking + plane detection + lighting estimation"**. Apple ARKit (2017), Google ARCore (2018) 매 starting point, 매 2026 매 ARKit 7 + ARCore 1.45 + Unity AR Foundation 6 매 unified API, 매 LiDAR/depth sensor 매 mesh reconstruction 매 standard.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 capabilities
|
||||
- **6DoF tracking**: 매 device pose (position + rotation) 매 visual-inertial SLAM.
|
||||
- **Plane detection**: 매 horizontal/vertical plane 매 detect.
|
||||
- **Hit testing**: 매 screen ray 매 world surface 매 intersection.
|
||||
- **Lighting estimation**: 매 ambient light + spherical harmonics + shadow.
|
||||
- **Anchors**: 매 world coordinate 매 stable point 매 attach virtual object.
|
||||
- **People occlusion**: 매 ML-based segmentation 매 사람 뒤 / 앞 렌더링.
|
||||
- **Depth API**: 매 LiDAR (iPhone Pro) / ToF (Android) / monocular (ML).
|
||||
- **Cloud anchors / Persistent anchors**: 매 multi-device + multi-session 공유.
|
||||
|
||||
### 매 platforms
|
||||
- **ARKit 7** (iOS 19): RoomPlan, Object Capture (photogrammetry), Geo Anchors, Vision Pro bridge.
|
||||
- **ARCore 1.45** (Android): Geospatial API (Streetscape Geometry), Depth API, Cloud Anchors.
|
||||
- **Unity AR Foundation 6**: cross-platform abstraction over ARKit/ARCore.
|
||||
- **Unreal AR**: Niagara particles + Lumen — 매 high-fidelity preferred.
|
||||
- **WebXR Device API**: browser-based AR (Chrome Android, Quest browser).
|
||||
|
||||
### 매 응용
|
||||
1. IKEA Place — furniture preview in room.
|
||||
2. Snapchat Lenses — face/world filters.
|
||||
3. Pokémon GO — geo-anchored creatures.
|
||||
4. Google Maps Live View — walking AR navigation.
|
||||
5. Industrial maintenance overlays.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### ARKit RealityKit (Swift)
|
||||
```swift
|
||||
import ARKit
|
||||
import RealityKit
|
||||
|
||||
class ARController: NSObject, ARSessionDelegate {
|
||||
let arView = ARView(frame: .zero)
|
||||
|
||||
func start() {
|
||||
let config = ARWorldTrackingConfiguration()
|
||||
config.planeDetection = [.horizontal, .vertical]
|
||||
config.environmentTexturing = .automatic
|
||||
if ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) {
|
||||
config.frameSemantics.insert(.sceneDepth) // LiDAR
|
||||
}
|
||||
arView.session.run(config)
|
||||
arView.session.delegate = self
|
||||
}
|
||||
|
||||
func placeBox(at screenPoint: CGPoint) {
|
||||
guard let result = arView.raycast(from: screenPoint,
|
||||
allowing: .estimatedPlane,
|
||||
alignment: .horizontal).first else { return }
|
||||
let anchor = AnchorEntity(world: result.worldTransform)
|
||||
let box = ModelEntity(mesh: .generateBox(size: 0.1),
|
||||
materials: [SimpleMaterial(color: .red, isMetallic: false)])
|
||||
anchor.addChild(box)
|
||||
arView.scene.addAnchor(anchor)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ARCore (Kotlin)
|
||||
```kotlin
|
||||
class ArSceneView(context: Context) : GLSurfaceView(context) {
|
||||
private lateinit var session: Session
|
||||
|
||||
fun init(activity: Activity) {
|
||||
session = Session(activity)
|
||||
val config = Config(session).apply {
|
||||
planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
|
||||
lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
|
||||
depthMode = if (session.isDepthModeSupported(Config.DepthMode.AUTOMATIC))
|
||||
Config.DepthMode.AUTOMATIC else Config.DepthMode.DISABLED
|
||||
}
|
||||
session.configure(config)
|
||||
}
|
||||
|
||||
fun onTap(x: Float, y: Float) {
|
||||
val frame = session.update()
|
||||
for (hit in frame.hitTest(x, y)) {
|
||||
val trackable = hit.trackable
|
||||
if (trackable is Plane && trackable.isPoseInPolygon(hit.hitPose)) {
|
||||
val anchor = hit.createAnchor()
|
||||
placeModel(anchor)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unity AR Foundation (C#)
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.ARFoundation;
|
||||
using UnityEngine.XR.ARSubsystems;
|
||||
|
||||
public class PlaceObject : MonoBehaviour {
|
||||
public ARRaycastManager raycaster;
|
||||
public GameObject prefab;
|
||||
static List<ARRaycastHit> hits = new();
|
||||
|
||||
void Update() {
|
||||
if (Input.touchCount == 0) return;
|
||||
var touch = Input.GetTouch(0);
|
||||
if (touch.phase != TouchPhase.Began) return;
|
||||
|
||||
if (raycaster.Raycast(touch.position, hits, TrackableType.PlaneWithinPolygon)) {
|
||||
var pose = hits[0].pose;
|
||||
Instantiate(prefab, pose.position, pose.rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### People occlusion (ARKit)
|
||||
```swift
|
||||
config.frameSemantics.insert(.personSegmentationWithDepth)
|
||||
arView.environment.background = .cameraFeed()
|
||||
arView.renderOptions.remove(.disablePersonOcclusion)
|
||||
```
|
||||
|
||||
### Geospatial anchor (ARCore)
|
||||
```kotlin
|
||||
val earth = session.earth ?: return
|
||||
if (earth.trackingState != TrackingState.TRACKING) return
|
||||
|
||||
val anchor = earth.createAnchor(
|
||||
37.7749, -122.4194, // lat/lng
|
||||
altitude,
|
||||
0f, 0f, 0f, 1f // quaternion
|
||||
)
|
||||
```
|
||||
|
||||
### WebXR (browser AR)
|
||||
```js
|
||||
const session = await navigator.xr.requestSession('immersive-ar', {
|
||||
requiredFeatures: ['hit-test', 'local'],
|
||||
})
|
||||
const refSpace = await session.requestReferenceSpace('local')
|
||||
const viewerSpace = await session.requestReferenceSpace('viewer')
|
||||
const hitSource = await session.requestHitTestSource({ space: viewerSpace })
|
||||
|
||||
session.requestAnimationFrame(function onFrame(t, frame) {
|
||||
const hits = frame.getHitTestResults(hitSource)
|
||||
if (hits.length > 0) {
|
||||
const pose = hits[0].getPose(refSpace)
|
||||
placeReticle(pose.transform.matrix)
|
||||
}
|
||||
session.requestAnimationFrame(onFrame)
|
||||
})
|
||||
```
|
||||
|
||||
### Mesh reconstruction (LiDAR)
|
||||
```swift
|
||||
config.sceneReconstruction = .meshWithClassification
|
||||
// ARMeshAnchor 매 frame 별 update
|
||||
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
|
||||
for case let mesh as ARMeshAnchor in anchors {
|
||||
addMeshToScene(mesh)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| iOS-only, 매 native UX | ARKit + RealityKit |
|
||||
| Android-only | ARCore + Sceneform-EQR / Filament |
|
||||
| Cross-platform iOS+Android | Unity AR Foundation |
|
||||
| Web/no-install | WebXR (Chrome Android only currently) |
|
||||
| High-fidelity / game | Unreal AR |
|
||||
| Geo-anchored content | ARCore Geospatial / ARKit Location Anchors |
|
||||
| Indoor mapping | ARKit RoomPlan / ARCore Streetscape |
|
||||
| Multi-user shared session | Cloud Anchors (ARCore) / SharePlay (ARKit) |
|
||||
|
||||
**기본값**: 매 cross-platform 매 Unity AR Foundation, 매 native 매 ARKit/ARCore 직접.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Computer Vision|Computer-Vision]]
|
||||
- 변형: [[ARKit]] · [[ARCore]] · [[WebXR]]
|
||||
- Adjacent: [[SLAM]] · [[OpenGL ES]] · [[Metal]] · [[Vulkan]] · [[Vision-Pro]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 mobile AR feature 설계, 매 ARKit/ARCore 선택, 매 placement/occlusion/lighting 매 implementation.
|
||||
**언제 X**: 매 desktop VR/AR (different platforms), 매 marker-based AR only (legacy — Vuforia 같은 different stack).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **첫 frame 매 immediate placement**: 매 tracking 매 not-converged — 매 plane detection 매 wait.
|
||||
- **매 anchor 매 world origin attach**: 매 drift 누적 — 매 surface anchor 매 use.
|
||||
- **무한 occlusion 활성화 매 mid-tier device**: 매 30fps 이하 — 매 capability check.
|
||||
- **Battery 무시**: 매 AR session 매 30 min 매 device thermal throttle — 매 idle pause.
|
||||
- **Privacy 무시**: 매 camera + location 매 prompt + purpose string 필수 (App Store/Play reject).
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (ARKit 7 docs, ARCore 1.45 docs, Unity AR Foundation 6).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — ARKit/ARCore/Unity/WebXR patterns + capabilities matrix |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user