chore(brain): ASTRA 성장 자산 동기화 — 기능 인벤토리·growth(약점프로필/학습큐)·일화기억·장기기억·회의록 원문
This commit is contained in:
+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 응답 및 상태 모델링 (State Modeling and API Responses)|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 정리 |
|
||||
+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 응답 및 상태 모델링 (State Modeling and API Responses)|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) 정리 |
|
||||
+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 응답 및 상태 모델링 (State Modeling and API Responses)|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 정리 |
|
||||
+181
@@ -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]]
|
||||
- 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,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]] · [[Typed Array]] · [[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 |
|
||||
+187
@@ -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 개선)|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 개선)|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,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 |
|
||||
+167
@@ -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 개선)|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 |
|
||||
+173
@@ -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,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]]
|
||||
- 변형: [[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 |
|
||||
+193
@@ -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 |
|
||||
@@ -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 |
|
||||
+206
@@ -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,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 추가 |
|
||||
+189
@@ -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 |
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: wiki-2026-0508-hydration-progressive-rendering
|
||||
title: "Hydration & Progressive Rendering"
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: hydration
|
||||
duplicate_of: "[[Hydration]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, ssr, hydration]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# Hydration & Progressive Rendering
|
||||
|
||||
> **이 문서는 [[Hydration]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약
|
||||
- **Hydration**: SSR HTML 에 client-side event handler / state 의 attach.
|
||||
- **Progressive rendering**: 매 React 18 streaming SSR + Suspense — 매 chunk 단위 stream + selective hydration.
|
||||
- **Resumability** (Qwik): hydration 자체 의 회피 — 매 serialized state 를 즉시 복원.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Hydration]] (canonical)
|
||||
- Adjacent: [[Streaming-SSR]] · [[React-Server-Components]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
+200
@@ -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]]
|
||||
|
||||
## 🤖 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 패턴 정리 |
|
||||
+225
@@ -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,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 개선)|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 |
|
||||
+209
@@ -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,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,200 @@
|
||||
---
|
||||
id: wiki-2026-0508-nextjs-framework
|
||||
title: Next.js Framework
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Next.js, NextJS, Nextjs]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [nextjs, react, ssr, framework, vercel]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Next.js 15
|
||||
---
|
||||
|
||||
# Next.js Framework
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Next.js 는 React-based full-stack framework — App Router (RSC) + Server Actions + Turbopack 의 조합"**. 2026 의 Next 15 는 매 React 19 RSC + Partial Prerendering (PPR) 안정화 + Turbopack 가 default. 매 Vercel 의 commercial backing.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Render 모드 (App Router)
|
||||
- **Static (default)**: 매 build-time, ISR 가능.
|
||||
- **Dynamic**: 매 request-time, `cookies()` / `headers()` 사용 시 자동 전환.
|
||||
- **Streaming SSR**: 매 RSC + Suspense — 매 progressive HTML.
|
||||
- **PPR (Partial Prerendering)**: 매 static shell + dynamic holes — 매 Next 15 default.
|
||||
|
||||
### 매 핵심 API
|
||||
- **Server Components**: 매 default — 매 zero JS 전송.
|
||||
- **`"use client"`**: 매 client component boundary.
|
||||
- **Server Actions** (`"use server"`): 매 form / mutation, RPC 자동.
|
||||
- **`fetch()` extension**: 매 cache, revalidate, tags.
|
||||
|
||||
### 매 응용
|
||||
1. E-commerce (Shopify Hydrogen 영향).
|
||||
2. Marketing site + blog (ISR 강점).
|
||||
3. SaaS dashboard (RSC 로 bundle 감소).
|
||||
4. AI chat UI — 매 streaming Server Action.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Server Component data fetch
|
||||
```tsx
|
||||
// app/posts/page.tsx — 매 server component, async 가능
|
||||
async function PostsPage() {
|
||||
const posts = await fetch("https://api.example.com/posts", {
|
||||
next: { revalidate: 60, tags: ["posts"] },
|
||||
}).then((r) => r.json());
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
export default PostsPage;
|
||||
```
|
||||
|
||||
### Server Action (form mutation)
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
"use server";
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
await db.post.create({ data: { title: formData.get("title") as string } });
|
||||
revalidateTag("posts");
|
||||
}
|
||||
|
||||
// app/new/page.tsx
|
||||
import { createPost } from "../actions";
|
||||
export default function NewPost() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<input name="title" />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming with Suspense
|
||||
```tsx
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 매 즉시 stream */}
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<SlowProductList /> {/* 매 fetch 끝나면 stream-in */}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Prerendering (Next 15)
|
||||
```tsx
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
experimental: { ppr: "incremental" },
|
||||
};
|
||||
|
||||
// app/page.tsx — static shell + dynamic part
|
||||
export const experimental_ppr = true;
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
async function CartCount() {
|
||||
const session = (await cookies()).get("session");
|
||||
// 매 dynamic — static shell 의 hole 로 stream
|
||||
return <span>{await getCartCount(session?.value)}</span>;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<StaticHeader />
|
||||
<Suspense fallback="0"><CartCount /></Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Route Handler (REST endpoint)
|
||||
```ts
|
||||
// app/api/users/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
export async function GET() {
|
||||
const users = await db.user.findMany();
|
||||
return NextResponse.json(users);
|
||||
}
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json();
|
||||
const user = await db.user.create({ data: body });
|
||||
return NextResponse.json(user, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware (auth gate)
|
||||
```ts
|
||||
// middleware.ts
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
export function middleware(req: NextRequest) {
|
||||
const token = req.cookies.get("token");
|
||||
if (!token && req.nextUrl.pathname.startsWith("/dashboard")) {
|
||||
return NextResponse.redirect(new URL("/login", req.url));
|
||||
}
|
||||
}
|
||||
export const config = { matcher: ["/dashboard/:path*"] };
|
||||
```
|
||||
|
||||
### Image optimization
|
||||
```tsx
|
||||
import Image from "next/image";
|
||||
<Image src="/hero.jpg" width={1200} height={600} alt="" priority />
|
||||
// 매 자동 AVIF/WebP, srcset, lazy
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Marketing site | Static + ISR |
|
||||
| User dashboard | RSC + Server Action |
|
||||
| Realtime chat | "use client" + WebSocket |
|
||||
| Mostly static + cart | PPR (Next 15) |
|
||||
| Pure SPA | 매 Next 대신 Vite + React |
|
||||
|
||||
**기본값**: App Router + RSC + PPR. Pages Router 는 매 legacy.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]]
|
||||
- 변형: [[Remix]] · [[Astro]] · [[SvelteKit]]
|
||||
- Adjacent: [[React Server Components]] · [[Turbopack]] · [[Edge Runtime]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: App Router migration, RSC 설계, Server Action 구현, PPR 적용.
|
||||
**언제 X**: 매 pure backend (Express/Fastify), pure SPA.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`"use client"` on root layout**: 매 RSC 효과 무효 — 매 leaf 만.
|
||||
- **`fetch` 안에 secret**: 매 client 로 leak 가능 — 매 Server Action 사용.
|
||||
- **getInitialProps**: 매 Pages Router legacy — 매 App Router 에서 X.
|
||||
- **Massive client bundle**: 매 Server Component 로 옮길 것.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (nextjs.org docs, React 19 RSC spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Next 15 RSC + PPR + Server Action |
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
---
|
||||
id: wiki-2026-0508-nominal-typing-in-typescript
|
||||
title: Nominal Typing in TypeScript
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Branded Types, Opaque Types TypeScript]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, type-system, branding]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: TypeScript 5.x
|
||||
---
|
||||
|
||||
# Nominal Typing in TypeScript
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 TypeScript 매 structural-type 시스템 매 nominal 효과 매 simulation — 매 phantom field 매 'brand' 매 통해 매 동일 shape 매 다른 type 으로 separate"**. TS 매 native nominal X 매 (Java/C# 의 `class A {} class B {}` 매 다름) — 매 brand pattern 매 community-standard, 매 ID/Currency/Email 매 같은 매 primitive 매 mix-up bug 매 compile-time 차단.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Structural vs Nominal
|
||||
- **Structural** (TS, Go, OCaml): 매 shape 매 동일하면 매 동일 type — `interface A { x: number }` ≡ `interface B { x: number }`.
|
||||
- **Nominal** (Java, C#, Rust): 매 declared name 매 동일해야 — 매 동일 shape 매도 매 다른 type 이면 incompatible.
|
||||
|
||||
### 매 brand pattern 종류
|
||||
- **Class private field**: 매 `#brand` — 매 nominal 한 hint, 매 runtime cost.
|
||||
- **Intersection with brand**: `T & { __brand: 'X' }` — 매 zero runtime, 매 compile-time only.
|
||||
- **Symbol brand**: `T & { [brand]: 'X' }` — 매 collision-safe.
|
||||
- **Unique symbol**: `declare const brand: unique symbol` — 매 strongest TS pattern.
|
||||
- **TypeScript 5.0+ `type` operator** (proposed): 매 still community pattern.
|
||||
|
||||
### 매 응용
|
||||
1. ID type separation: `UserId` vs `OrderId` 매 mix 차단.
|
||||
2. Validated values: `Email`, `Url`, `NonEmptyString`.
|
||||
3. Units: `Meters`, `Seconds`, `USD` vs `EUR`.
|
||||
4. Sanitized strings: `SafeHtml`, `SqlEscaped`.
|
||||
5. Domain primitives (DDD value object 매 lightweight 표현).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic brand
|
||||
```ts
|
||||
type Brand<T, B> = T & { readonly __brand: B }
|
||||
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
type OrderId = Brand<string, 'OrderId'>
|
||||
|
||||
const userId = 'u_123' as UserId
|
||||
const orderId = 'o_456' as OrderId
|
||||
|
||||
function getUser(id: UserId) { /* */ }
|
||||
getUser(userId) // OK
|
||||
getUser(orderId) // 매 compile error: OrderId not assignable to UserId
|
||||
getUser('u_123') // 매 compile error: string not assignable to UserId
|
||||
```
|
||||
|
||||
### Smart constructor (validation)
|
||||
```ts
|
||||
type Email = Brand<string, 'Email'>
|
||||
|
||||
function makeEmail(s: string): Email | null {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) ? (s as Email) : null
|
||||
}
|
||||
|
||||
function sendMail(to: Email, body: string) { /* */ }
|
||||
|
||||
const e = makeEmail('a@b.com')
|
||||
if (e) sendMail(e, 'hi') // 매 narrowed to Email
|
||||
sendMail('raw' as string, 'hi') // 매 compile error
|
||||
```
|
||||
|
||||
### Unique symbol brand (strongest)
|
||||
```ts
|
||||
declare const userIdBrand: unique symbol
|
||||
declare const orderIdBrand: unique symbol
|
||||
|
||||
type UserId = string & { readonly [userIdBrand]: void }
|
||||
type OrderId = string & { readonly [orderIdBrand]: void }
|
||||
|
||||
// 매 여기서는 매 두 type 매 절대 호환 X (unique symbol 매 collision-free)
|
||||
```
|
||||
|
||||
### Reusable Brand utility
|
||||
```ts
|
||||
declare const brandSymbol: unique symbol
|
||||
type Brand<T, Tag extends string> = T & { readonly [brandSymbol]: Tag }
|
||||
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
type Meters = Brand<number, 'Meters'>
|
||||
type Seconds = Brand<number, 'Seconds'>
|
||||
|
||||
function speed(d: Meters, t: Seconds): number {
|
||||
return (d as number) / (t as number)
|
||||
}
|
||||
|
||||
const d = 100 as Meters
|
||||
const t = 5 as Seconds
|
||||
speed(d, t) // OK
|
||||
speed(t, d) // 매 compile error
|
||||
```
|
||||
|
||||
### Class private field (alternative)
|
||||
```ts
|
||||
class UserId {
|
||||
#brand!: void
|
||||
constructor(public readonly value: string) {}
|
||||
}
|
||||
class OrderId {
|
||||
#brand!: void
|
||||
constructor(public readonly value: string) {}
|
||||
}
|
||||
|
||||
function getUser(id: UserId) { /* */ }
|
||||
getUser(new UserId('u_1')) // OK
|
||||
getUser(new OrderId('o_1')) // 매 compile error: #brand 매 only-accessible
|
||||
// 매 cost: 매 wrapping object — 매 runtime allocation
|
||||
```
|
||||
|
||||
### Sanitized string
|
||||
```ts
|
||||
type SafeHtml = Brand<string, 'SafeHtml'>
|
||||
|
||||
function escapeHtml(raw: string): SafeHtml {
|
||||
return raw.replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]!)) as SafeHtml
|
||||
}
|
||||
|
||||
function render(html: SafeHtml) { /* dangerouslySetInnerHTML OK */ }
|
||||
render('<script>' as SafeHtml) // 매 cast 매 강제 — 매 reviewer 매 catch
|
||||
render(escapeHtml(userInput)) // 매 safe path
|
||||
```
|
||||
|
||||
### Currency
|
||||
```ts
|
||||
type USD = Brand<number, 'USD'>
|
||||
type EUR = Brand<number, 'EUR'>
|
||||
|
||||
function add(a: USD, b: USD): USD { return (a + b) as USD }
|
||||
|
||||
const price = 100 as USD
|
||||
const tax = 7 as USD
|
||||
const total = add(price, tax) // OK
|
||||
|
||||
const eu = 50 as EUR
|
||||
add(price, eu) // 매 compile error
|
||||
```
|
||||
|
||||
### Phantom type with generics
|
||||
```ts
|
||||
type Validated<T, V extends string> = T & { readonly __validated: V }
|
||||
|
||||
type Trimmed<T extends string> = Validated<T, 'Trimmed'>
|
||||
type NonEmpty<T extends string> = Validated<T, 'NonEmpty'>
|
||||
|
||||
function trim<T extends string>(s: T): Trimmed<T> {
|
||||
return s.trim() as Trimmed<T>
|
||||
}
|
||||
|
||||
function requireNonEmpty<T extends string>(s: T): NonEmpty<T> {
|
||||
if (s.length === 0) throw new Error('empty')
|
||||
return s as NonEmpty<T>
|
||||
}
|
||||
```
|
||||
|
||||
### Library: ts-brand / Effect-TS
|
||||
```ts
|
||||
// 매 ts-brand
|
||||
import { Brand, make } from 'ts-brand'
|
||||
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
const UserId = make<UserId>()
|
||||
const id = UserId('u_1')
|
||||
|
||||
// 매 Effect-TS Schema
|
||||
import { Schema } from '@effect/schema'
|
||||
|
||||
const UserId = Schema.String.pipe(Schema.brand('UserId'))
|
||||
type UserId = Schema.Schema.Type<typeof UserId>
|
||||
```
|
||||
|
||||
### JSON deserialization 매 brand 복원
|
||||
```ts
|
||||
interface UserJson { id: string; name: string }
|
||||
interface User { id: UserId; name: string }
|
||||
|
||||
function fromJson(j: UserJson): User {
|
||||
return { id: j.id as UserId, name: j.name }
|
||||
}
|
||||
// 매 boundary 매서만 cast — 매 internal 매 type 안전
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| ID separation | Brand<string, 'XId'> |
|
||||
| Validated value (Email, Url) | Brand + smart constructor |
|
||||
| Unit (Meters, USD) | Brand<number, 'Unit'> |
|
||||
| Sanitized string | Brand<string, 'Safe'> + escape function |
|
||||
| Strongest isolation | unique symbol brand |
|
||||
| Runtime check 동반 | Class with #brand + factory |
|
||||
| 매 library 사용 OK | ts-brand / Effect Schema |
|
||||
| Domain layer 매 rich behavior | Class wrapper (DDD value object) |
|
||||
|
||||
**기본값**: 매 `Brand<T, 'Name'>` intersection — 매 zero runtime cost + 매 sufficient nominal effect.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[TypeScript 타입 시스템 (TypeScript Type System)|Type-System]]
|
||||
- 변형: [[Branded-Types]] · [[Opaque-Types]]
|
||||
- 응용: [[Domain-Driven-Design]] · [[Value-Object]] · [[Refinement-Type]]
|
||||
- Adjacent: [[Effect-TS]] · [[Zod]] · [[Type-Safety]] · [[Smart-Constructor]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 ID/string mix-up 매 bug surface 자주, 매 unit/currency 혼동 risk, 매 sanitized vs raw 매 boundary 강제, 매 DDD value object 매 lightweight 표현.
|
||||
**언제 X**: 매 throwaway code, 매 internal-only types 매 1 곳만 사용 (overhead), 매 simple CRUD CRUD 매 brand 폭발.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 모든 string 매 brand**: 매 cognitive overhead 폭증 — 매 confusion-prone boundary 만 brand.
|
||||
- **`as Brand` 매 무절제 cast**: 매 brand 의 의미 없음 — 매 smart constructor 매 통과 필수.
|
||||
- **Brand 매 runtime check 가정**: 매 compile-time only — 매 deserialization 매 raw cast 시 brittle.
|
||||
- **매 deeply nested generic brand**: 매 type error message 매 unreadable.
|
||||
- **`__brand` field 매 runtime expose**: 매 JSON serialize 시 leak — 매 phantom field 매 절대 assign X.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TypeScript handbook discussion, Effect-TS Schema, ts-brand library).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — brand patterns + smart constructor + unit/currency examples |
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
id: wiki-2026-0508-one-way-data-flow
|
||||
title: One-way Data Flow
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [One-way Data Flow, Unidirectional Data Flow, Top-down Props]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, frontend, architecture, data-flow, state-management]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React
|
||||
---
|
||||
|
||||
# One-way Data Flow
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 state 매 down, event 매 up"**. Unidirectional data flow 매 React (Flux 영감) 의 core mental model — parent props 으로 child 에 데이터 전달, child callback 으로 parent 에 event 통보. 매 reasoning 의 단순화, debugging 의 traceability, time-travel 의 가능성. Vue/Solid/Svelte 도 매 동일 원칙.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 규칙
|
||||
- State 매 single owner (component or store).
|
||||
- Owner 만 mutate 가능.
|
||||
- Child 매 read-only props 만 받음.
|
||||
- Child 의 변경 요청 매 callback / event / dispatch.
|
||||
|
||||
### 매 왜 unidirectional
|
||||
- **Predictability**: state 변경 source 매 명확.
|
||||
- **Debugging**: render output 매 props 의 함수.
|
||||
- **Time-travel**: state snapshot 만 으로 UI 재현.
|
||||
- **Concurrency**: 매 React Concurrent 매 mutable 2-way 매 deadlock-prone.
|
||||
|
||||
### 매 응용
|
||||
1. React props/callback 패턴.
|
||||
2. Redux / Zustand / Jotai store dispatch.
|
||||
3. Vue `props down, emit up`.
|
||||
4. Form: lift state up.
|
||||
5. Event sourcing / CQRS frontend.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Lifting state up (React)
|
||||
```tsx
|
||||
function Parent() {
|
||||
const [name, setName] = useState("");
|
||||
return (
|
||||
<>
|
||||
<NameInput value={name} onChange={setName} />
|
||||
<Greeting name={name} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
|
||||
}
|
||||
|
||||
function Greeting({ name }: { name: string }) {
|
||||
return <p>Hello, {name || "stranger"}!</p>;
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer (action up, state down)
|
||||
```tsx
|
||||
type Action = { type: "add"; item: string } | { type: "remove"; idx: number };
|
||||
type State = { items: string[] };
|
||||
|
||||
function reducer(s: State, a: Action): State {
|
||||
switch (a.type) {
|
||||
case "add": return { items: [...s.items, a.item] };
|
||||
case "remove": return { items: s.items.filter((_, i) => i !== a.idx) };
|
||||
}
|
||||
}
|
||||
|
||||
function TodoApp() {
|
||||
const [state, dispatch] = useReducer(reducer, { items: [] });
|
||||
return (
|
||||
<>
|
||||
<AddBox onAdd={(item) => dispatch({ type: "add", item })} />
|
||||
<List items={state.items} onRemove={(idx) => dispatch({ type: "remove", idx })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Zustand (store as single source)
|
||||
```ts
|
||||
import { create } from "zustand";
|
||||
|
||||
interface CartStore {
|
||||
items: { id: string; qty: number }[];
|
||||
add: (id: string) => void;
|
||||
remove: (id: string) => void;
|
||||
}
|
||||
|
||||
export const useCart = create<CartStore>((set) => ({
|
||||
items: [],
|
||||
add: (id) => set((s) => ({ items: [...s.items, { id, qty: 1 }] })),
|
||||
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
|
||||
}));
|
||||
|
||||
function ProductCard({ id }: { id: string }) {
|
||||
const add = useCart((s) => s.add);
|
||||
return <button onClick={() => add(id)}>Add</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Vue 3 props down + emit up
|
||||
```vue
|
||||
<!-- Parent.vue -->
|
||||
<template>
|
||||
<Counter :value="count" @increment="count++" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
const count = ref(0);
|
||||
</script>
|
||||
|
||||
<!-- Counter.vue -->
|
||||
<template>
|
||||
<button @click="$emit('increment')">{{ value }}</button>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps<{ value: number }>();
|
||||
defineEmits<{ increment: [] }>();
|
||||
</script>
|
||||
```
|
||||
|
||||
### Server-driven (React Query + URL state)
|
||||
```tsx
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
function ProductList() {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const category = params.get("category") ?? "all";
|
||||
const { data } = useQuery({
|
||||
queryKey: ["products", category],
|
||||
queryFn: () => fetch(`/api/products?cat=${category}`).then((r) => r.json()),
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<CategoryPicker value={category} onChange={(v) => setParams({ category: v })} />
|
||||
{data?.map((p) => <Card key={p.id} product={p} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Forbidden 2-way leak (anti-pattern shown)
|
||||
```tsx
|
||||
// ❌ child mutates parent's prop directly via mutable ref
|
||||
function BadChild({ obj }: { obj: { count: number } }) {
|
||||
return <button onClick={() => obj.count++}>+</button>; // parent never re-renders
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 2-3 components share state | Lift state up |
|
||||
| Cross-tree state | Context / Zustand / Redux |
|
||||
| Server data | React Query / SWR (single source) |
|
||||
| URL state | useSearchParams / Next router |
|
||||
| Form-heavy local | useReducer + dispatch |
|
||||
| Vue | props + emit (or Pinia) |
|
||||
|
||||
**기본값**: state 매 highest common ancestor 또는 매 store. 매 props down + callback up.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]]
|
||||
- 변형: [[프론트엔드_및_UIUX_표준|Redux]] · [[Zustand]] · [[Pinia]]
|
||||
- 응용: [[useReducer]] · [[Event-Sourcing]]
|
||||
- Adjacent: [[Reactive-Streams]] · [[Signals]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: React/Vue/Solid component design, form lifting, store architecture.
|
||||
**언제 X**: 매 highly local input (e.g. simple text field) — 매 controlled-input over-engineering 회피, uncontrolled OK.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Mutate props in child**: 매 silent re-render miss.
|
||||
- **Shared mutable ref**: 매 React diff 의 invariant 위반.
|
||||
- **Two-way binding in big tree**: 매 cycle / cascade.
|
||||
- **Prop drilling 10층**: 매 Context / store 으로 cut.
|
||||
- **Local form state in 매 sync 매 server**: 매 stale conflict — server 매 source of truth + optimistic update.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React docs — Sharing State Between Components, Vue docs — Component Basics, Redux principles).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — lifting/reducer/Zustand/Vue emit/server-driven 패턴 |
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: wiki-2026-0508-opaque-types
|
||||
title: Opaque Types (TypeScript)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Opaque Types, Branded Types, Nominal Types in TS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, types, nominal-typing, branded-types]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: none
|
||||
---
|
||||
|
||||
# Opaque Types (TypeScript)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 structural type 의 nominal escape hatch"**. TypeScript 매 structural typing 이지만, opaque (branded) type 으로 매 string 끼리 / number 끼리 mix-up 방지. UserId vs PostId, Cents vs Dollars 등 매 domain primitive 의 type-safety 강화.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 왜 필요
|
||||
- 매 TS structural — `type UserId = string; type PostId = string;` 매 interchangeable.
|
||||
- 매 runtime cost X 의 compile-time distinction 필요.
|
||||
- 매 unit confusion (cents/dollars, ms/sec) 의 typing.
|
||||
|
||||
### 매 두 가지 구현
|
||||
- **Symbol brand** (가장 strict, 매 declaration merging 어려움).
|
||||
- **Unique string brand** (가장 ergonomic, TS 4.9+).
|
||||
|
||||
### 매 응용
|
||||
1. ID types — UserId, PostId, OrgId 의 cross-mixing 방지.
|
||||
2. Units — Meters vs Feet, Cents vs Dollars.
|
||||
3. Validated strings — `Email`, `URL`, `SafeHtml` (validator 통과 후 만 cast).
|
||||
4. Crypto — `HashedPassword` vs `PlainPassword`.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic branded type
|
||||
```ts
|
||||
declare const brand: unique symbol;
|
||||
type Brand<T, B> = T & { readonly [brand]: B };
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type PostId = Brand<string, "PostId">;
|
||||
|
||||
const asUserId = (s: string): UserId => s as UserId;
|
||||
|
||||
const u: UserId = asUserId("u_123");
|
||||
const p: PostId = asUserId("u_123"); // ❌ Type error
|
||||
```
|
||||
|
||||
### Smart constructor with validation
|
||||
```ts
|
||||
type Email = Brand<string, "Email">;
|
||||
|
||||
function makeEmail(s: string): Email {
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
|
||||
throw new Error(`Invalid email: ${s}`);
|
||||
}
|
||||
return s as Email;
|
||||
}
|
||||
|
||||
function send(to: Email, body: string) { /* ... */ }
|
||||
|
||||
send(makeEmail("a@b.co"), "hi"); // ✓
|
||||
send("a@b.co" as string, "hi"); // ❌
|
||||
```
|
||||
|
||||
### Numeric units
|
||||
```ts
|
||||
type Cents = Brand<number, "Cents">;
|
||||
type Dollars = Brand<number, "Dollars">;
|
||||
|
||||
const toCents = (d: Dollars): Cents => (d * 100) as Cents;
|
||||
const toDollars = (c: Cents): Dollars => (c / 100) as Dollars;
|
||||
|
||||
function charge(amount: Cents) { /* ... */ }
|
||||
charge(toCents(9.99 as Dollars)); // ✓
|
||||
charge(9.99 as Dollars); // ❌
|
||||
```
|
||||
|
||||
### Result-style smart constructor (no throw)
|
||||
```ts
|
||||
type ValidUrl = Brand<string, "ValidUrl">;
|
||||
|
||||
function parseUrl(s: string): ValidUrl | null {
|
||||
try {
|
||||
new URL(s);
|
||||
return s as ValidUrl;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const u = parseUrl(input);
|
||||
if (u) fetch(u); // u: ValidUrl
|
||||
```
|
||||
|
||||
### Generic Tagged
|
||||
```ts
|
||||
type Tagged<T, Tag extends string> = T & { readonly __tag: Tag };
|
||||
type Meters = Tagged<number, "m">;
|
||||
type Seconds = Tagged<number, "s">;
|
||||
const speed = (m: Meters, s: Seconds): number => m / s;
|
||||
```
|
||||
|
||||
### Zod + brand (runtime + compile-time)
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
const EmailSchema = z.string().email().brand<"Email">();
|
||||
type Email = z.infer<typeof EmailSchema>; // string & z.BRAND<"Email">
|
||||
|
||||
const parsed = EmailSchema.parse(input); // Email
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| ID types | unique symbol brand |
|
||||
| Validated input | smart constructor (parse, don't validate) |
|
||||
| Runtime + compile | Zod `.brand<T>()` |
|
||||
| Numeric units | Tagged number with conversion functions |
|
||||
| One-off | inline `string & { _t: "X" }` |
|
||||
|
||||
**기본값**: unique-symbol brand + smart constructor — 매 boundary 의 cast, 매 internal 은 type-safe propagation.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript-Type-System]]
|
||||
- 변형: [[Branded-Types]]
|
||||
- 응용: [[Parse-Dont-Validate]]
|
||||
- Adjacent: [[API 응답 및 상태 모델링 (State Modeling and API Responses)|Discriminated-Unions]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: ID 매 mix 위험 / unit 매 confusion / validated string 의 boundary protection.
|
||||
**언제 X**: 매 internal helper / one-off — 매 over-engineering.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Brand without smart constructor**: 매 `as` cast 매 어디서나 — type 의 의미 없음.
|
||||
- **Erase brand**: `JSON.stringify` → parse 매 brand 손실 — boundary 에 re-validate.
|
||||
- **Brand on `any`**: 매 base type 의 narrowing 효과 X.
|
||||
- **Excessive branding**: 매 internal 의 모든 string 에 brand — readability 폭락.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TypeScript Handbook, Effective TypeScript Item 37, Zod docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — symbol brand, smart constructor, Zod brand, units 패턴 |
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
---
|
||||
id: wiki-2026-0508-open-closed-principle-ocp
|
||||
title: Open-Closed Principle (OCP)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [OCP, Open Closed Principle, SOLID OCP]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [solid, ocp, design-principles, oop, architecture]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: none
|
||||
---
|
||||
|
||||
# Open-Closed Principle (OCP)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 extension 에 open, modification 에 closed"**. Bertrand Meyer (1988) 매 originally inheritance, Robert Martin (1996) 매 abstraction-based redefinition. 새 behavior 매 *추가* 의 way 로, 매 existing code 의 *수정* 없이 — strategy / plugin / open enum 등 매 광범위 응용.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 두 가지 해석
|
||||
- **Meyer's OCP**: 매 inheritance — class 매 closed for modification (매 published interface 안정), open for extension (매 subclass).
|
||||
- **Polymorphic OCP** (Martin): 매 abstraction (interface) 의존, 매 implementation 추가 만 으로 extend.
|
||||
|
||||
### 매 왜 중요
|
||||
- 매 regression 위험 줄임 (existing 안 건드림).
|
||||
- 매 plugin / extension model 가능.
|
||||
- 매 testability — 매 mock implementation 추가 만.
|
||||
|
||||
### 매 응용
|
||||
1. Strategy pattern — payment processor (Stripe, Toss, PayPal).
|
||||
2. Discriminated union 의 add new variant.
|
||||
3. Plugin architecture (Vite plugin, ESLint rule).
|
||||
4. Visitor pattern.
|
||||
5. Middleware chain (Koa, Express).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Strategy interface (TS)
|
||||
```ts
|
||||
interface PaymentProcessor {
|
||||
charge(amount: number, token: string): Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
async charge(amount: number, token: string) {
|
||||
return { id: `stripe_${Date.now()}` };
|
||||
}
|
||||
}
|
||||
|
||||
class TossProcessor implements PaymentProcessor {
|
||||
async charge(amount: number, token: string) {
|
||||
return { id: `toss_${Date.now()}` };
|
||||
}
|
||||
}
|
||||
|
||||
// New processor → just add new class. CheckoutService unchanged.
|
||||
class CheckoutService {
|
||||
constructor(private processor: PaymentProcessor) {}
|
||||
pay(amt: number, t: string) { return this.processor.charge(amt, t); }
|
||||
}
|
||||
```
|
||||
|
||||
### Discriminated union + exhaustiveness
|
||||
```ts
|
||||
type Shape =
|
||||
| { kind: "circle"; r: number }
|
||||
| { kind: "square"; side: number }
|
||||
| { kind: "triangle"; base: number; height: number };
|
||||
|
||||
function area(s: Shape): number {
|
||||
switch (s.kind) {
|
||||
case "circle": return Math.PI * s.r ** 2;
|
||||
case "square": return s.side ** 2;
|
||||
case "triangle": return (s.base * s.height) / 2;
|
||||
default: {
|
||||
const _exhaust: never = s;
|
||||
throw new Error(_exhaust);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add 'pentagon' → TS error in `area`, no silent miss.
|
||||
```
|
||||
|
||||
### Plugin / hook registry
|
||||
```ts
|
||||
type Hook<T> = (ctx: T) => void | Promise<void>;
|
||||
class HookRegistry<T> {
|
||||
private hooks: Hook<T>[] = [];
|
||||
add(h: Hook<T>) { this.hooks.push(h); }
|
||||
async run(ctx: T) { for (const h of this.hooks) await h(ctx); }
|
||||
}
|
||||
|
||||
const onUserCreated = new HookRegistry<{ userId: string }>();
|
||||
onUserCreated.add(({ userId }) => sendWelcomeEmail(userId));
|
||||
onUserCreated.add(({ userId }) => analytics.track("user_created", userId));
|
||||
// New behavior → add new hook, no service change.
|
||||
```
|
||||
|
||||
### Visitor pattern
|
||||
```ts
|
||||
interface NodeVisitor<R> {
|
||||
visitNumber(n: NumberNode): R;
|
||||
visitBinary(n: BinaryNode): R;
|
||||
}
|
||||
|
||||
abstract class AstNode { abstract accept<R>(v: NodeVisitor<R>): R; }
|
||||
class NumberNode extends AstNode {
|
||||
constructor(public val: number) { super(); }
|
||||
accept<R>(v: NodeVisitor<R>) { return v.visitNumber(this); }
|
||||
}
|
||||
class BinaryNode extends AstNode {
|
||||
constructor(public op: "+"|"-", public l: AstNode, public r: AstNode) { super(); }
|
||||
accept<R>(v: NodeVisitor<R>) { return v.visitBinary(this); }
|
||||
}
|
||||
|
||||
// New traversal (printer, evaluator, optimizer) → new visitor class.
|
||||
```
|
||||
|
||||
### Middleware chain
|
||||
```ts
|
||||
type Middleware<C> = (ctx: C, next: () => Promise<void>) => Promise<void>;
|
||||
|
||||
function compose<C>(mws: Middleware<C>[]): (ctx: C) => Promise<void> {
|
||||
return async (ctx) => {
|
||||
let i = -1;
|
||||
const dispatch = async (idx: number): Promise<void> => {
|
||||
if (idx <= i) throw new Error("next() called multiple times");
|
||||
i = idx;
|
||||
const fn = mws[idx];
|
||||
if (!fn) return;
|
||||
await fn(ctx, () => dispatch(idx + 1));
|
||||
};
|
||||
await dispatch(0);
|
||||
};
|
||||
}
|
||||
|
||||
const app = compose<{ req: Request; res?: Response }>([
|
||||
async (c, next) => { console.log("log"); await next(); },
|
||||
async (c, next) => { c.res = new Response("ok"); await next(); },
|
||||
]);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Multiple swappable algos | Strategy / interface |
|
||||
| Closed set of variants | Discriminated union + exhaustive switch |
|
||||
| Open set of behaviors | Plugin / hook registry |
|
||||
| Tree traversal, multiple ops | Visitor |
|
||||
| Pipeline | Middleware chain |
|
||||
| Just one impl, no plans | Don't pre-abstract (YAGNI) |
|
||||
|
||||
**기본값**: 매 second variation 발견 시 abstract — 매 first time 의 over-abstraction X.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[SOLID-Principles]] · [[Design-Principles]]
|
||||
- 변형: [[Visitor-Pattern]]
|
||||
- 응용: [[Middleware]] · [[API 응답 및 상태 모델링 (State Modeling and API Responses)|Discriminated-Unions]] · [[Dependency_Injection_(DI)|Dependency-Injection]]
|
||||
- Adjacent: [[DIP]] · [[Composition-over-Inheritance]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 add-only domain (payment processor, plugin, parser AST), variant 매 자주 추가.
|
||||
**언제 X**: 매 small / unchanging code — 매 abstraction 의 cost > benefit.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Premature abstraction**: 매 single impl 의 interface — 매 indirection 만 추가, value 0.
|
||||
- **Speculative generality**: 매 "혹시 나중에" — YAGNI.
|
||||
- **Inheritance for code reuse**: 매 LSP 위반 risk. Composition 우선.
|
||||
- **Open everything**: 매 모든 method virtual / hook — 매 cognitive load 폭증.
|
||||
- **Ignore exhaustiveness**: 매 switch default fallback — 매 new variant 의 silent skip.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Meyer "Object-Oriented Software Construction", Martin "Clean Architecture", refactoring.guru).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — strategy, discriminated union, plugin/visitor/middleware 패턴 |
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
id: wiki-2026-0508-pinia
|
||||
title: Pinia
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Vue Store, Pinia Store]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [vue, state-management, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Vue 3 / Pinia 2
|
||||
---
|
||||
|
||||
# Pinia
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Vue 3 매 official state management — 매 Vuex 4 후속, 매 Composition API native + 매 TypeScript-first"**. Eduardo San Martin Morote 매 2019 발표 매 Vue Core 팀 채택, 매 store 매 composable function 매 표현 매 boilerplate 90% 감소, 매 2026 매 Vue 표준 store library.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 vs Vuex
|
||||
- **No mutations**: 매 actions 가 directly state 수정 — 매 mutation indirection 제거.
|
||||
- **Flat stores**: 매 nested module 매 X — 매 cross-store import 매 graph 형성.
|
||||
- **Type inference**: 매 zero manual typing — 매 store usage 매 fully typed.
|
||||
- **Devtools**: 매 timeline + state inspect + time-travel 지원.
|
||||
|
||||
### 매 store kinds
|
||||
- **Options Store**: `state`, `getters`, `actions` — 매 Vuex-like familiar shape.
|
||||
- **Setup Store**: `ref`/`computed`/`function` — 매 Composition API native.
|
||||
|
||||
### 매 응용
|
||||
1. SPA global state (auth, user prefs).
|
||||
2. Server-side rendering (Nuxt 3 매 native integration).
|
||||
3. Cross-component caching (API result, derived state).
|
||||
4. Plugin extension (persistedstate, undo/redo).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Setup store (modern preferred)
|
||||
```ts
|
||||
// stores/counter.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubled = computed(() => count.value * 2)
|
||||
function increment() { count.value++ }
|
||||
function reset() { count.value = 0 }
|
||||
return { count, doubled, increment, reset }
|
||||
})
|
||||
```
|
||||
|
||||
### Options store
|
||||
```ts
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: null as User | null,
|
||||
loading: false,
|
||||
}),
|
||||
getters: {
|
||||
isLoggedIn: (s) => s.user !== null,
|
||||
displayName: (s) => s.user?.name ?? 'Guest',
|
||||
},
|
||||
actions: {
|
||||
async login(creds: Credentials) {
|
||||
this.loading = true
|
||||
try {
|
||||
this.user = await api.login(creds)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
logout() { this.user = null },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Component usage
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCounterStore } from '@/stores/counter'
|
||||
|
||||
const store = useCounterStore()
|
||||
// 매 reactivity 매 유지: storeToRefs 매 ref 변환
|
||||
const { count, doubled } = storeToRefs(store)
|
||||
// actions 매 destructure OK (not reactive)
|
||||
const { increment } = store
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="increment">{{ count }} (×2 = {{ doubled }})</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### App setup (Vue 3)
|
||||
```ts
|
||||
// main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
### Cross-store usage
|
||||
```ts
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
const auth = useAuthStore() // 매 다른 store 매 import + use
|
||||
const items = ref<CartItem[]>([])
|
||||
|
||||
async function checkout() {
|
||||
if (!auth.isLoggedIn) throw new Error('Login required')
|
||||
return api.checkout(auth.user!.id, items.value)
|
||||
}
|
||||
return { items, checkout }
|
||||
})
|
||||
```
|
||||
|
||||
### $patch 매 batch update
|
||||
```ts
|
||||
const store = useUserStore()
|
||||
store.$patch({ loading: false, user: newUser }) // 매 single devtools entry
|
||||
// or function form for complex mutations
|
||||
store.$patch((state) => {
|
||||
state.cart.push(item)
|
||||
state.lastUpdated = Date.now()
|
||||
})
|
||||
```
|
||||
|
||||
### $subscribe 매 store mutation 감지
|
||||
```ts
|
||||
store.$subscribe((mutation, state) => {
|
||||
localStorage.setItem('cart', JSON.stringify(state))
|
||||
}, { detached: true })
|
||||
```
|
||||
|
||||
### Plugin (persistedstate)
|
||||
```ts
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPersist from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPersist)
|
||||
|
||||
// store 정의 시:
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({ token: '' }),
|
||||
persist: true, // localStorage 매 자동 sync
|
||||
})
|
||||
```
|
||||
|
||||
### SSR (Nuxt 3)
|
||||
```ts
|
||||
// composables/useAuth.ts
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null)
|
||||
// 매 Nuxt 3 매 자동 hydration — server 매 set 한 state 매 client 매 transfer
|
||||
return { user }
|
||||
})
|
||||
```
|
||||
|
||||
### Testing
|
||||
```ts
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { beforeEach, expect, test } from 'vitest'
|
||||
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
test('counter increments', () => {
|
||||
const store = useCounterStore()
|
||||
expect(store.count).toBe(0)
|
||||
store.increment()
|
||||
expect(store.count).toBe(1)
|
||||
expect(store.doubled).toBe(2)
|
||||
})
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Vue 3 new project | Pinia (default) |
|
||||
| Vuex 4 migration | Pinia (incremental, alias module) |
|
||||
| Composition API preference | Setup store |
|
||||
| Vuex-familiar team | Options store |
|
||||
| Component-local state | `ref`/`reactive` (no store) |
|
||||
| Server state caching | TanStack Query / 매 store 의 X |
|
||||
| SSR (Nuxt) | Pinia (built-in support) |
|
||||
|
||||
**기본값**: 매 setup store + TypeScript + storeToRefs.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[State-Management]]
|
||||
- 변형: [[Vuex]] (predecessor) · [[프론트엔드_및_UIUX_표준|Redux]] · [[Zustand]] · [[Jotai]]
|
||||
- 응용: [[Nuxt]]
|
||||
- Adjacent: [[Composition-API]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 Vue 3 global state, 매 cross-component sharing, 매 Nuxt 3 SSR state, 매 Vuex migration target.
|
||||
**언제 X**: 매 component-local state (ref 만 충분), 매 server cache (TanStack Query 적합), 매 React project (Zustand/Redux).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **storeToRefs 없이 destructure**: 매 reactivity loss — 매 `const { count } = store` 의 X.
|
||||
- **Store action 매 component logic 침범**: 매 store 매 단순 state holder 매 됨 — 매 business logic 매 store 에 응집.
|
||||
- **Nested store hierarchy 시도**: 매 flat 의 X — 매 cross-import 매 graph 형성.
|
||||
- **Mutation pattern 의 반복**: 매 Vuex habit — 매 action 에서 직접 state 수정 OK.
|
||||
- **매 component 매 store instance 다중 생성**: 매 useStore() 매 singleton — 매 매 호출 동일 instance.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Pinia 2.x docs, Vue.js official).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — setup/options store + cross-store + SSR + plugin patterns |
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
id: wiki-2026-0508-principles
|
||||
title: Principles
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Software Engineering Principles, Design Principles]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [design, principles, engineering, meta]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: meta
|
||||
framework: cross-language
|
||||
---
|
||||
|
||||
# Principles
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 software engineering 매 invariant guidelines — 매 decade-tested heuristics 매 specific tech 매 무관 적용"**. SOLID (Robert C. Martin 2000), DRY/KISS/YAGNI (XP 1999), Composition over Inheritance (GoF 1994) 매 backbone, 매 2026 매 LLM-augmented coding 매 시대 매 여전히 readability/maintainability 매 governing axis.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 SOLID
|
||||
- **S — Single Responsibility**: 매 module 매 매 reason-to-change 매 하나.
|
||||
- **O — Open/Closed**: 매 extension 매 open, 매 modification 매 closed.
|
||||
- **L — Liskov Substitution**: 매 subtype 매 base type 매 자리 매 대체 가능.
|
||||
- **I — Interface Segregation**: 매 client 매 unused method 매 의존 X.
|
||||
- **D — Dependency Inversion**: 매 high-level module 매 low-level 매 의존 X — 매 abstraction 매 의존.
|
||||
|
||||
### 매 핵심 short heuristics
|
||||
- **DRY (Don't Repeat Yourself)**: 매 knowledge 매 single representation.
|
||||
- **KISS (Keep It Simple, Stupid)**: 매 simplest thing 매 works.
|
||||
- **YAGNI (You Aren't Gonna Need It)**: 매 future-need 매 가정 X.
|
||||
- **Composition over Inheritance**: 매 has-a 매 is-a 보다 flexible.
|
||||
- **Tell, Don't Ask**: 매 object 매 데이터 묻지 말고 매 행동 요청.
|
||||
- **Law of Demeter**: 매 friend-of-friend 매 의존 X (`a.b.c.d()` 매 X).
|
||||
- **Principle of Least Astonishment**: 매 행동 매 사용자 expectation 매 일치.
|
||||
- **Fail Fast**: 매 오류 매 early surface — 매 silent corruption 매 X.
|
||||
- **Boy Scout Rule**: 매 떠날 때 매 코드 매 더 깨끗하게.
|
||||
|
||||
### 매 응용
|
||||
1. Code review checklist (SOLID violations, DRY 적용 검토).
|
||||
2. Architecture decision record (principle 매 trade-off rationale).
|
||||
3. Onboarding (junior dev 매 mental model).
|
||||
4. LLM prompt engineering (gen 시 매 principle 매 명시 → quality ↑).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### SRP — split reasons-to-change
|
||||
```ts
|
||||
// 매 BAD — 매 두 reasons (HTTP + persistence)
|
||||
class UserController {
|
||||
async register(req, res) {
|
||||
const user = req.body
|
||||
if (!user.email) return res.status(400).send('email required')
|
||||
await db.query('INSERT INTO users ...', [user])
|
||||
return res.send('ok')
|
||||
}
|
||||
}
|
||||
|
||||
// 매 GOOD
|
||||
class UserController {
|
||||
constructor(private validator: UserValidator, private repo: UserRepo) {}
|
||||
async register(req, res) {
|
||||
const result = this.validator.validate(req.body)
|
||||
if (!result.ok) return res.status(400).send(result.error)
|
||||
await this.repo.save(req.body)
|
||||
return res.send('ok')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OCP — strategy pattern
|
||||
```ts
|
||||
// 매 BAD — 매 새 type 매 추가 시 매 modify
|
||||
function calc(shape: Shape): number {
|
||||
switch (shape.kind) {
|
||||
case 'circle': return Math.PI * shape.r ** 2
|
||||
case 'square': return shape.side ** 2
|
||||
// 매 새 type → 매 여기 수정 필요
|
||||
}
|
||||
}
|
||||
|
||||
// 매 GOOD — 매 extension 매 open
|
||||
interface Shape { area(): number }
|
||||
class Circle implements Shape { constructor(public r: number) {}; area() { return Math.PI * this.r ** 2 } }
|
||||
class Square implements Shape { constructor(public side: number) {}; area() { return this.side ** 2 } }
|
||||
class Triangle implements Shape { /* 매 새 file — 매 기존 코드 unchanged */ }
|
||||
```
|
||||
|
||||
### LSP — substitutability
|
||||
```ts
|
||||
// 매 BAD — Square 매 Rectangle 의 LSP 위반
|
||||
class Rectangle {
|
||||
constructor(public w: number, public h: number) {}
|
||||
setW(w: number) { this.w = w }
|
||||
setH(h: number) { this.h = h }
|
||||
}
|
||||
class Square extends Rectangle {
|
||||
setW(w: number) { this.w = w; this.h = w } // 매 surprise
|
||||
setH(h: number) { this.w = h; this.h = h }
|
||||
}
|
||||
function expand(r: Rectangle) {
|
||||
r.setW(5); r.setH(10)
|
||||
console.assert(r.w === 5 && r.h === 10) // Square 매 fail
|
||||
}
|
||||
|
||||
// 매 GOOD — 매 hierarchy 분리 / composition
|
||||
```
|
||||
|
||||
### ISP — fat interface 매 분할
|
||||
```ts
|
||||
// 매 BAD
|
||||
interface Worker {
|
||||
work(): void
|
||||
eat(): void
|
||||
sleep(): void
|
||||
}
|
||||
class Robot implements Worker {
|
||||
work() {}
|
||||
eat() { throw new Error('robots dont eat') } // 매 ISP 위반
|
||||
sleep() { throw new Error('robots dont sleep') }
|
||||
}
|
||||
|
||||
// 매 GOOD
|
||||
interface Workable { work(): void }
|
||||
interface Eatable { eat(): void }
|
||||
interface Sleepable { sleep(): void }
|
||||
class Robot implements Workable { work() {} }
|
||||
class Human implements Workable, Eatable, Sleepable { /* */ }
|
||||
```
|
||||
|
||||
### DIP — depend on abstractions
|
||||
```ts
|
||||
// 매 BAD — 매 high-level (OrderService) 매 low-level (MysqlOrderRepo) 매 의존
|
||||
class OrderService {
|
||||
private repo = new MysqlOrderRepo()
|
||||
place(order: Order) { this.repo.save(order) }
|
||||
}
|
||||
|
||||
// 매 GOOD
|
||||
interface OrderRepo { save(o: Order): Promise<void> }
|
||||
class OrderService {
|
||||
constructor(private repo: OrderRepo) {}
|
||||
place(o: Order) { return this.repo.save(o) }
|
||||
}
|
||||
class MysqlOrderRepo implements OrderRepo { /* */ }
|
||||
class PostgresOrderRepo implements OrderRepo { /* */ }
|
||||
```
|
||||
|
||||
### Composition over Inheritance
|
||||
```ts
|
||||
// 매 BAD
|
||||
class Duck {
|
||||
fly() { /* */ }
|
||||
quack() { /* */ }
|
||||
}
|
||||
class RubberDuck extends Duck {
|
||||
fly() { throw new Error("can't fly") } // 매 LSP 위반
|
||||
}
|
||||
|
||||
// 매 GOOD — 매 strategy composition
|
||||
interface FlyBehavior { fly(): void }
|
||||
interface QuackBehavior { quack(): void }
|
||||
class Duck {
|
||||
constructor(private flyB: FlyBehavior, private quackB: QuackBehavior) {}
|
||||
performFly() { this.flyB.fly() }
|
||||
performQuack() { this.quackB.quack() }
|
||||
}
|
||||
const rubberDuck = new Duck(new FlyNoWay(), new Squeak())
|
||||
const mallard = new Duck(new FlyWithWings(), new NormalQuack())
|
||||
```
|
||||
|
||||
### Tell Don't Ask
|
||||
```ts
|
||||
// 매 BAD — 매 ask
|
||||
if (account.balance > amount) {
|
||||
account.balance -= amount
|
||||
ledger.record(amount)
|
||||
}
|
||||
|
||||
// 매 GOOD — 매 tell
|
||||
account.withdraw(amount, ledger) // 매 객체 매 캡슐화 + 매 invariant 매 보장
|
||||
```
|
||||
|
||||
### Law of Demeter
|
||||
```ts
|
||||
// 매 BAD — 매 train wreck
|
||||
const street = order.customer.address.street
|
||||
|
||||
// 매 GOOD — 매 method 매 노출
|
||||
const street = order.customerStreet() // 매 Order 매 delegate
|
||||
```
|
||||
|
||||
### Fail Fast
|
||||
```ts
|
||||
// 매 BAD — 매 silent fallback
|
||||
function parse(s: string) {
|
||||
try { return JSON.parse(s) } catch { return {} } // 매 caller 매 깨진 data 매 모름
|
||||
}
|
||||
|
||||
// 매 GOOD — 매 explicit
|
||||
function parse(s: string): Result<unknown, Error> {
|
||||
try { return { ok: true, value: JSON.parse(s) } }
|
||||
catch (e) { return { ok: false, error: e as Error } }
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Principle |
|
||||
|---|---|
|
||||
| God class refactor | SRP — split |
|
||||
| 매 새 변형 매 추가 매 잦은 수정 | OCP — strategy |
|
||||
| Subtype 매 caller surprise | LSP — rethink hierarchy |
|
||||
| Interface 매 unused methods | ISP — split |
|
||||
| Hard-coded dependency | DIP — inject |
|
||||
| Repeated code 3+ places | DRY — extract |
|
||||
| Speculative generality | YAGNI — delete |
|
||||
| Surprise behavior | Least Astonishment — rename/redesign |
|
||||
| Hidden errors | Fail Fast — throw early |
|
||||
|
||||
**기본값**: 매 SOLID + 매 KISS + 매 YAGNI — 매 over-abstraction 매 함정 매 회피.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[SOLID]] · [[Clean-Architecture]] · [[Hexagonal-Architecture]]
|
||||
- 응용: [[Code-Review]] · [[Refactoring_Best_Practices|Refactoring]] · [[Architecture-Decision-Record]]
|
||||
- Adjacent: [[Design-Patterns]] · [[Domain-Driven-Design]] · [[Test-Driven-Development]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 design review, 매 refactor planning, 매 principle violation 매 식별, 매 mentoring/teaching context.
|
||||
**언제 X**: 매 throwaway script (overhead), 매 dogmatic application — 매 principle 매 means 매 X end.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **매 principle 매 dogma**: 매 SRP 매 따라 매 1-line class 매 폭발 — 매 trade-off.
|
||||
- **DRY over-application**: 매 coincidental duplication 매 동일 함수 묶음 — 매 false abstraction.
|
||||
- **YAGNI 매 결여**: 매 "future-proof" 매 매 무한 layer.
|
||||
- **DIP 매 always**: 매 simple CRUD 매 abstract repo — 매 over-engineer.
|
||||
- **OCP 매 rule**: 매 모든 enum 매 polymorphism 변환 — 매 readability ↓.
|
||||
- **Pattern dropping**: 매 Strategy/Factory/Observer 매 nameset — 매 problem 매 first.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Clean Code/Architecture by R.C. Martin, Design Patterns GoF, Pragmatic Programmer).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — SOLID + heuristics + violation/fix patterns matrix |
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
---
|
||||
id: wiki-2026-0508-probabilistic-graphical-models
|
||||
title: Probabilistic Graphical Models
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [PGM, Bayesian Network, Markov Random Field, MRF]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [pgm, bayesian-network, mrf, inference, machine-learning]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: Python
|
||||
framework: pgmpy/pyro/numpyro
|
||||
---
|
||||
|
||||
# Probabilistic Graphical Models
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 graph 로 joint distribution 의 factorization 표현"**. 매 random variable = node, dependency = edge. Bayesian Network (DAG) 와 Markov Random Field (undirected) 의 두 family. 2026 의 매 deep learning 시대에도 medical diagnosis, fault detection, causal inference 에서 핵심.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Two Families
|
||||
- **Bayesian Network (Directed)**: P(X) = ∏ᵢ P(Xᵢ | Pa(Xᵢ)). 매 causal direction 명시.
|
||||
- **Markov Random Field (Undirected)**: P(X) = (1/Z) ∏ φc(Xc). 매 symmetric correlation.
|
||||
- **Factor Graph**: 매 두 family 통합 representation — bipartite (variable + factor nodes).
|
||||
|
||||
### 매 핵심 Operation
|
||||
- **Marginal inference**: P(Xᵢ) 매 sum out 다른 variable.
|
||||
- **MAP inference**: argmax P(X) — most likely assignment.
|
||||
- **Conditional**: P(Xᵢ | E=e) — evidence 주어진 belief update.
|
||||
- **Learning**: 매 parameter (MLE, EM) + structure (score-based, constraint-based).
|
||||
|
||||
### 매 Inference Algorithm
|
||||
- **Exact**: Variable Elimination, Belief Propagation (tree), Junction Tree.
|
||||
- **Approx**: MCMC (Gibbs, Metropolis-Hastings), Variational Inference, Loopy BP.
|
||||
|
||||
### 매 응용
|
||||
1. Medical diagnosis: 매 symptom → disease causal network.
|
||||
2. Fault detection: 매 sensor reading → root cause.
|
||||
3. Computer vision: 매 CRF for image segmentation (DeepLab v3 의 backbone).
|
||||
4. Causal inference: 매 do-calculus, counterfactual.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### pgmpy: Bayesian Network 정의
|
||||
```python
|
||||
from pgmpy.models import DiscreteBayesianNetwork
|
||||
from pgmpy.factors.discrete import TabularCPD
|
||||
|
||||
model = DiscreteBayesianNetwork([('Rain', 'Sprinkler'), ('Rain', 'Wet'), ('Sprinkler', 'Wet')])
|
||||
|
||||
cpd_rain = TabularCPD('Rain', 2, [[0.8], [0.2]])
|
||||
cpd_sprinkler = TabularCPD('Sprinkler', 2, [[0.9, 0.5], [0.1, 0.5]], evidence=['Rain'], evidence_card=[2])
|
||||
cpd_wet = TabularCPD('Wet', 2,
|
||||
[[1.0, 0.2, 0.1, 0.01], [0.0, 0.8, 0.9, 0.99]],
|
||||
evidence=['Rain', 'Sprinkler'], evidence_card=[2, 2])
|
||||
|
||||
model.add_cpds(cpd_rain, cpd_sprinkler, cpd_wet)
|
||||
assert model.check_model()
|
||||
```
|
||||
|
||||
### Variable Elimination inference
|
||||
```python
|
||||
from pgmpy.inference import VariableElimination
|
||||
|
||||
infer = VariableElimination(model)
|
||||
result = infer.query(variables=['Rain'], evidence={'Wet': 1})
|
||||
print(result)
|
||||
```
|
||||
|
||||
### NumPyro: Bayesian regression
|
||||
```python
|
||||
import numpyro
|
||||
import numpyro.distributions as dist
|
||||
from numpyro.infer import MCMC, NUTS
|
||||
import jax.numpy as jnp
|
||||
|
||||
def model(X, y=None):
|
||||
beta = numpyro.sample('beta', dist.Normal(jnp.zeros(X.shape[1]), 1.0))
|
||||
sigma = numpyro.sample('sigma', dist.HalfNormal(1.0))
|
||||
mu = jnp.dot(X, beta)
|
||||
numpyro.sample('y', dist.Normal(mu, sigma), obs=y)
|
||||
|
||||
mcmc = MCMC(NUTS(model), num_warmup=500, num_samples=1000)
|
||||
mcmc.run(jax.random.PRNGKey(0), X, y)
|
||||
```
|
||||
|
||||
### Markov Random Field (CRF for sequence labeling)
|
||||
```python
|
||||
import torch
|
||||
from torchcrf import CRF
|
||||
|
||||
num_tags = 5
|
||||
crf = CRF(num_tags, batch_first=True)
|
||||
|
||||
emissions = torch.randn(2, 10, num_tags) # (batch, seq, tags)
|
||||
tags = torch.randint(0, num_tags, (2, 10))
|
||||
loss = -crf(emissions, tags) # neg log likelihood
|
||||
|
||||
best = crf.decode(emissions) # Viterbi
|
||||
```
|
||||
|
||||
### Pyro: Variational Inference
|
||||
```python
|
||||
import pyro
|
||||
import pyro.distributions as dist
|
||||
from pyro.infer import SVI, Trace_ELBO
|
||||
from pyro.optim import Adam
|
||||
|
||||
def model(data):
|
||||
z = pyro.sample('z', dist.Normal(0, 1))
|
||||
pyro.sample('obs', dist.Normal(z, 1), obs=data)
|
||||
|
||||
def guide(data):
|
||||
mu = pyro.param('mu', torch.tensor(0.))
|
||||
sigma = pyro.param('sigma', torch.tensor(1.), constraint=dist.constraints.positive)
|
||||
pyro.sample('z', dist.Normal(mu, sigma))
|
||||
|
||||
svi = SVI(model, guide, Adam({'lr': 0.01}), Trace_ELBO())
|
||||
for step in range(1000):
|
||||
svi.step(data)
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Discrete, small state space, exact inference | pgmpy + VE |
|
||||
| Continuous, large model | NumPyro + NUTS |
|
||||
| Sequence labeling | CRF |
|
||||
| Image segmentation | CRF + CNN (DeepLab) |
|
||||
| Causal inference | DoWhy + pgmpy |
|
||||
| Real-time, approx OK | Loopy BP / VI |
|
||||
|
||||
**기본값**: 매 small problem → pgmpy. 매 large continuous → NumPyro NUTS.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Probability_Theory]] · [[Graph_Theory]]
|
||||
- 변형: [[Bayesian_Network]] · [[Markov_Random_Field]]
|
||||
- 응용: [[Image_Segmentation]] · [[Causal_Inference]]
|
||||
- Adjacent: [[Hidden_Markov_Model]] · [[Variational_Inference]] · [[MCMC]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 explicit causal structure 필요, interpretability 중요, small-data domain (medicine).
|
||||
**언제 X**: 매 large-scale perception (이미지/음성) — neural network 가 우수.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 변수 fully connected**: 매 parameter explosion — sparsity 활용.
|
||||
- **Exact inference 의 강행 in dense graph**: NP-hard — approximate 사용.
|
||||
- **Causal direction 의 임의 가정**: 매 domain knowledge 없으면 PC algorithm 등으로 학습.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Koller & Friedman 2009, Murphy 2012, pgmpy 0.1.26, NumPyro 0.16).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — pgmpy/NumPyro/Pyro modern stack |
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
---
|
||||
id: wiki-2026-0508-react-native-web-desktop
|
||||
title: React Native Web — Desktop
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [React Native Web Desktop, RNW Desktop, Universal RN]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [react-native, react-native-web, desktop, cross-platform, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React Native Web
|
||||
---
|
||||
|
||||
# React Native Web — Desktop
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 한 codebase 의 mobile + desktop"**. React Native Web (RNW) 매 RN component 를 DOM 으로 render — Expo Web 또는 standalone Vite/Next.js 셋업으로 매 desktop browser target. 2026 기준 Expo Router v4 + RN 0.76 New Architecture 매 stable, Bridgeless 매 web 의 hot reload 빠름. Tamagui / NativeWind 매 styling 의 cross-platform.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 왜 desktop 의 RNW
|
||||
- 매 mobile-first 의 enterprise dashboard / electron 대체.
|
||||
- 매 single team / single component library.
|
||||
- Server-side render (Next.js + RNW 0.19+) 매 SEO 가능.
|
||||
- Tablet / iPad split-view 의 fluid layout.
|
||||
|
||||
### 매 vs alternatives
|
||||
- **Electron**: 매 heavy bundle, native shell, 매 different mental model.
|
||||
- **Tauri**: 매 Rust + WebView — 매 React but 매 RN component X.
|
||||
- **PWA + responsive**: 매 web-first, mobile compromise.
|
||||
- **RNW**: 매 mobile-first, desktop adapt.
|
||||
|
||||
### 매 응용
|
||||
1. Expo Router universal app — iOS + Android + Web (single team).
|
||||
2. Internal tool / admin — RN component library 재사용.
|
||||
3. Linear/Notion-style desktop app — RNW + Tauri shell.
|
||||
4. Marketing site + app — same brand component.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Expo Router universal entry
|
||||
```tsx
|
||||
// app/_layout.tsx
|
||||
import { Stack } from "expo-router";
|
||||
import { useDeviceOrientation } from "@react-native-community/hooks";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
contentStyle: { maxWidth: 1280, alignSelf: "center", width: "100%" },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Platform-specific file (`.web.tsx`)
|
||||
```tsx
|
||||
// FilePicker.tsx (mobile)
|
||||
// FilePicker.web.tsx (desktop)
|
||||
|
||||
// FilePicker.web.tsx
|
||||
import { useRef } from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
|
||||
export function FilePicker({ onPick }: { onPick: (f: File) => void }) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Pressable onPress={() => inputRef.current?.click()}>
|
||||
<Text>Pick file</Text>
|
||||
</Pressable>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => e.target.files?.[0] && onPick(e.target.files[0])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive layout (useWindowDimensions)
|
||||
```tsx
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { width } = useWindowDimensions();
|
||||
const isDesktop = width >= 1024;
|
||||
return (
|
||||
<View style={{ flexDirection: isDesktop ? "row" : "column", flex: 1 }}>
|
||||
{isDesktop && <Sidebar />}
|
||||
<View style={{ flex: 1 }}>{children}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard shortcuts (web only)
|
||||
```tsx
|
||||
import { useEffect } from "react";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export function useShortcut(key: string, handler: () => void) {
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
const fn = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === key) {
|
||||
e.preventDefault();
|
||||
handler();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", fn);
|
||||
return () => window.removeEventListener("keydown", fn);
|
||||
}, [key, handler]);
|
||||
}
|
||||
```
|
||||
|
||||
### Hover state (`Pressable` web-only)
|
||||
```tsx
|
||||
import { Pressable, Text } from "react-native";
|
||||
|
||||
<Pressable
|
||||
style={({ hovered, pressed }: any) => ({
|
||||
backgroundColor: pressed ? "#e0e0e0" : hovered ? "#f0f0f0" : "white",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
})}
|
||||
>
|
||||
{() => <Text>Hover me (web)</Text>}
|
||||
</Pressable>
|
||||
```
|
||||
|
||||
### Tamagui styling (cross-platform)
|
||||
```tsx
|
||||
import { Button, Stack, Text } from "tamagui";
|
||||
|
||||
export function Card() {
|
||||
return (
|
||||
<Stack padding="$4" borderRadius="$4" backgroundColor="$background" hoverStyle={{ backgroundColor: "$backgroundHover" }}>
|
||||
<Text fontSize="$6">Title</Text>
|
||||
<Button theme="active">Action</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js + RNW (SSR setup)
|
||||
```js
|
||||
// next.config.js
|
||||
const { withExpo } = require("@expo/next-adapter");
|
||||
module.exports = withExpo({
|
||||
transpilePackages: ["react-native", "react-native-web", "expo"],
|
||||
experimental: { forceSwcTransforms: true },
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Mobile + desktop, same product | Expo Router universal |
|
||||
| Native desktop window/menu | Electron / Tauri (RNW inside Tauri OK) |
|
||||
| Heavy animations | Reanimated 4 (web target supported) |
|
||||
| Styling | Tamagui / NativeWind |
|
||||
| SSR/SEO | Next.js + RNW |
|
||||
| Mobile only | plain RN (no RNW) |
|
||||
|
||||
**기본값**: Expo Router + Tamagui — 매 mobile-first, desktop 매 responsive breakpoint.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React-Native]]
|
||||
- 변형: [[Expo-Router]]
|
||||
- 응용: [[Universal-App]]
|
||||
- Adjacent: [[Tauri]] · [[Electron]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: mobile-first product 의 desktop site, internal tool with RN library, universal team.
|
||||
**언제 X**: desktop-only 의 native window/menu/system tray — 매 Electron/Tauri.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`document.getElementById` 직접 사용**: 매 mobile crash. Platform.OS gate 필수.
|
||||
- **Mobile-only 라이브러리 import**: 매 web bundle 의 fail. `.web.tsx` split.
|
||||
- **Fixed pixel layout**: 매 desktop wide screen 매 broken. Flex / max-width.
|
||||
- **No keyboard navigation**: 매 desktop a11y 폭망.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Expo SDK 51+ docs, react-native-web 0.19+ docs, Tamagui docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Expo Router, .web.tsx, hover/shortcut, Tamagui, Next adapter 패턴 |
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
id: wiki-2026-0508-readonly-type
|
||||
title: Readonly Type
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [TypeScript Readonly, ReadonlyArray, Readonly Utility Type]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, readonly, immutability, type-system]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: TS 5.7+
|
||||
---
|
||||
|
||||
# Readonly Type
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 TypeScript 의 compile-time immutability"**. 매 `readonly` modifier (property level) + `Readonly<T>` utility type (object) + `ReadonlyArray<T>` (array) + `as const` (literal). 매 runtime 강제 X — 매 dev-time discipline. 2026 TS 5.7+ 매 `satisfies` / `const` type parameter 와 조합으로 강력.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4가지 형태
|
||||
- **`readonly` property**: `interface User { readonly id: number; }` — 단일 property.
|
||||
- **`Readonly<T>`**: 매 mapped type, 매 모든 property readonly.
|
||||
- **`ReadonlyArray<T>` / `readonly T[]`**: 매 array, push/pop 등 mutation method 제거.
|
||||
- **`as const`**: 매 literal value, deeply readonly + literal type 보존.
|
||||
|
||||
### 매 한계
|
||||
- **Compile-time only**: 매 runtime mutation 가능 (cast, JS interop).
|
||||
- **Shallow**: 매 `Readonly<T>` 매 nested object 는 mutable.
|
||||
- **Type erasure**: 매 컴파일 후 일반 JS object — `Object.freeze()` 와 무관.
|
||||
|
||||
### 매 응용
|
||||
1. Function parameter: 매 mutation 의 의도 차단.
|
||||
2. State: 매 Redux store, React state — 매 immutable update 강제.
|
||||
3. Configuration: 매 `as const` 로 literal type 보존.
|
||||
4. API response type: 매 response 의 mutation 차단.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### `readonly` property
|
||||
```ts
|
||||
interface User {
|
||||
readonly id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const u: User = { id: 1, name: 'Alice' };
|
||||
u.name = 'Bob'; // OK
|
||||
u.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
|
||||
```
|
||||
|
||||
### `Readonly<T>` utility
|
||||
```ts
|
||||
interface Config {
|
||||
apiUrl: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
const config: Readonly<Config> = { apiUrl: '/api', timeout: 5000 };
|
||||
config.apiUrl = '/v2'; // Error
|
||||
```
|
||||
|
||||
### `ReadonlyArray<T>` / `readonly T[]`
|
||||
```ts
|
||||
function sum(nums: readonly number[]): number {
|
||||
// nums.push(0); // Error
|
||||
return nums.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
const arr: readonly number[] = [1, 2, 3];
|
||||
arr[0] = 99; // Error
|
||||
```
|
||||
|
||||
### `as const` (deeply readonly literal)
|
||||
```ts
|
||||
const ROUTES = {
|
||||
HOME: '/',
|
||||
USERS: '/users',
|
||||
POSTS: '/posts',
|
||||
} as const;
|
||||
|
||||
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/users' | '/posts'
|
||||
|
||||
const TUPLE = [1, 'two', true] as const;
|
||||
// type: readonly [1, 'two', true] (not (number | string | boolean)[])
|
||||
```
|
||||
|
||||
### Deep Readonly (recursive)
|
||||
```ts
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
|
||||
interface State {
|
||||
user: { name: string; settings: { theme: string } };
|
||||
}
|
||||
const s: DeepReadonly<State> = { user: { name: 'A', settings: { theme: 'dark' } } };
|
||||
s.user.settings.theme = 'light'; // Error
|
||||
```
|
||||
|
||||
### `satisfies` + `as const` (TS 5.7)
|
||||
```ts
|
||||
const config = {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
} as const satisfies { port: number; host: string };
|
||||
|
||||
// config.port type: 3000 (literal), still validated
|
||||
```
|
||||
|
||||
### Function `const` type parameter (TS 5.0+)
|
||||
```ts
|
||||
function tuple<const T extends readonly unknown[]>(arr: T): T {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const t = tuple(['a', 'b', 'c']); // type: readonly ['a', 'b', 'c']
|
||||
```
|
||||
|
||||
### Branded readonly (runtime + type)
|
||||
```ts
|
||||
function freeze<T>(obj: T): Readonly<T> {
|
||||
return Object.freeze(obj) as Readonly<T>;
|
||||
}
|
||||
|
||||
const config = freeze({ apiUrl: '/api' });
|
||||
// config.apiUrl = '/v2'; // TS error + runtime error
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Single property immutable | `readonly` modifier |
|
||||
| Whole object immutable (shallow) | `Readonly<T>` |
|
||||
| Array param (no mutation) | `readonly T[]` |
|
||||
| Literal config | `as const` |
|
||||
| Deeply nested state | `DeepReadonly<T>` 또는 Immer |
|
||||
| Runtime guarantee 필요 | `Object.freeze()` + `Readonly<T>` |
|
||||
| React state | useState (immutable update convention) |
|
||||
|
||||
**기본값**: 매 function parameter 매 `readonly T[]` 항상. 매 literal config 매 `as const`. 매 deep immutability 매 Immer.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]]
|
||||
- 변형: [[const_assertion]]
|
||||
- 응용: [[프론트엔드_및_UIUX_표준|Redux]]
|
||||
- Adjacent: [[satisfies_operator]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: API response type, function param (mutation 의도 차단), config object (`as const`), Redux state.
|
||||
**언제 X**: 매 internal mutation-heavy class — readonly 의 noise.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`Readonly<T>` 의 deep 가정**: shallow 만 — `DeepReadonly<T>` 또는 Immer 사용.
|
||||
- **Runtime immutability 가정**: TS readonly 매 compile-time only — JS cast 시 mutable.
|
||||
- **`Readonly<T>` + class with method**: method 의 type 까지 readonly 처리 — 매 의도와 다른 동작 가능.
|
||||
- **`as const` 남용 in mutable context**: 매 literal type 의 narrowness 가 unintended widening 차단.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TypeScript 5.7 docs, TS handbook).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — `as const` / `satisfies` / `const` type param 추가 |
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
id: wiki-2026-0508-rollup
|
||||
title: Rollup
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [rollup-bundler, rollupjs]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [bundler, build-tool, esm, library-build, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: rollup-4
|
||||
---
|
||||
|
||||
# Rollup
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 ESM-first 의 module bundler 의 library-grade output 의 specialized."**. Rich Harris 의 2015 의 의 originated, 매 tree-shaking 의 pioneer — 매 2026 의 Rollup 4.x 의 매 SWC/Oxc-powered, library 의 publishing (NPM packages, SDKs) 의 의 default choice 이며, Vite 의 production build 의 underlying engine.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 정의
|
||||
- ESM-native bundler — `import`/`export` 의 first-class.
|
||||
- Tree-shaking 의 pioneer (2015) — dead code elimination via static analysis.
|
||||
- Output formats: ESM, CJS, UMD, IIFE, AMD, SystemJS.
|
||||
- Plugin-driven — minimal core, everything via `@rollup/plugin-*`.
|
||||
|
||||
### 매 vs. 다른 bundlers
|
||||
- **Vite (dev)**: Rollup 의 production 의 wraps — dev 의 esbuild + Rollup of build.
|
||||
- **webpack**: 매 application bundling 의 strong, code splitting 의 mature; Rollup 의 library output 의 cleaner.
|
||||
- **esbuild**: 10-100x faster, 그러나 plugin ecosystem 의 narrower.
|
||||
- **Bun.build / tsdown**: 2026 의 emerging, Rollup-compatible plugin API.
|
||||
|
||||
### 매 응용
|
||||
1. NPM library publishing (React component lib, SDK, utils package).
|
||||
2. Multi-format output (ESM + CJS + types).
|
||||
3. Vite 의 production build (transitively).
|
||||
4. Storybook 의 build pipeline.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Library bundle (TS + multi-format)
|
||||
```js
|
||||
// rollup.config.js
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{ file: 'dist/index.cjs', format: 'cjs', sourcemap: true },
|
||||
{ file: 'dist/index.mjs', format: 'esm', sourcemap: true },
|
||||
],
|
||||
plugins: [nodeResolve(), commonjs(), typescript({ tsconfig: './tsconfig.build.json' })],
|
||||
external: ['react', 'react-dom'],
|
||||
},
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [{ file: 'dist/index.d.ts', format: 'es' }],
|
||||
plugins: [dts()],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### `package.json` exports map
|
||||
```json
|
||||
{
|
||||
"name": "@org/lib",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"files": ["dist"]
|
||||
}
|
||||
```
|
||||
|
||||
### Tree-shaking 의 verify
|
||||
```js
|
||||
// rollup.config.js
|
||||
export default {
|
||||
// ...
|
||||
treeshake: {
|
||||
moduleSideEffects: false, // aggressive — assume no side effects
|
||||
propertyReadSideEffects: false,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Plugin: image inlining
|
||||
```js
|
||||
import image from '@rollup/plugin-image';
|
||||
import url from '@rollup/plugin-url';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
url({ limit: 8192 }), // inline <8kb as base64
|
||||
image(),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### React component library
|
||||
```js
|
||||
import { babel } from '@rollup/plugin-babel';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
|
||||
export default {
|
||||
input: 'src/index.tsx',
|
||||
output: { dir: 'dist', format: 'esm', preserveModules: true },
|
||||
external: ['react', 'react-dom', /^@radix-ui/],
|
||||
plugins: [
|
||||
postcss({ modules: true, extract: 'styles.css' }),
|
||||
babel({ babelHelpers: 'bundled', presets: ['@babel/preset-react'] }),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Watch mode + dev server
|
||||
```bash
|
||||
rollup -c -w
|
||||
# or use Vite which wraps Rollup for production
|
||||
vite build
|
||||
```
|
||||
|
||||
### Code splitting (manual chunks)
|
||||
```js
|
||||
export default {
|
||||
input: { main: 'src/main.ts', admin: 'src/admin.ts' },
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
manualChunks: {
|
||||
vendor: ['react', 'react-dom'],
|
||||
utils: ['lodash-es', 'date-fns'],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Library / SDK publishing | **Rollup** (clean output, multi-format) |
|
||||
| Application bundle | **Vite** (Rollup 의 wraps) or webpack |
|
||||
| Speed-critical (CI) | **esbuild** or **tsdown** (Rolldown) |
|
||||
| Storybook / docs build | Rollup-based (Vite mode) |
|
||||
| Monorepo internal package | tsup (esbuild) or Rollup with `preserveModules` |
|
||||
|
||||
**기본값**: library 의 의 Rollup, app 의 의 Vite (Rollup 의 production engine).
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[Vite]] · [[tsup]]
|
||||
- 응용: [[Component-Library-Architecture]]
|
||||
- Adjacent: [[ESM]] · [[esbuild]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: NPM library publishing, multi-format output, clean ESM bundles, Vite production tuning.
|
||||
**언제 X**: large application bundling (Vite/webpack), HMR-heavy dev (Vite), Node.js server (no bundling needed in 2026 ESM).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Bundling peer deps**: `react`/`react-dom` 의 bundle 의 included — `external` 의 declare 의 필요.
|
||||
- **CJS-only output 의 ESM-only consumer**: 2026 의 ESM-first ecosystem — dual ESM+CJS output 의 ship.
|
||||
- **Source map 의 omission**: production debugging 의 impossible — `sourcemap: true` 의 default.
|
||||
- **Plugin order 의 ignorance**: `nodeResolve` before `commonjs` before `typescript` — order matters.
|
||||
- **Vite app 의 직접 의 Rollup config**: Vite 의 abstracts — `vite.config.ts` 의 `build.rollupOptions` 의 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (rollupjs.org, Rollup 4.x docs 2026, Vite documentation).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full bundler reference + 2026 ecosystem (Rolldown, tsup) |
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
id: wiki-2026-0508-scss-sass
|
||||
title: SCSS (Sass)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Sass, SCSS, Dart Sass]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, css, scss, sass, preprocessor]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: SCSS
|
||||
framework: Dart Sass
|
||||
---
|
||||
|
||||
# SCSS (Sass)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CSS 의 superset preprocessor — variable, nesting, mixin, module"**. 2006 Hampton Catlin 출시. 2020 Dart Sass 의 only 공식 implementation (LibSass deprecated). 2026 매 Tailwind v4 / native CSS nesting 의 부상으로 점유율 감소했지만 매 design system / legacy 에서 견고.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 두 syntax
|
||||
- **SCSS**: 매 CSS-superset, `{}` `;` 사용 — 매 mainstream.
|
||||
- **Sass (indented)**: 매 Python-like indentation, `.sass` 확장자 — 매 niche.
|
||||
|
||||
### 매 핵심 기능
|
||||
- Variable: `$primary: #007bff;`
|
||||
- Nesting: 매 selector 중첩 + `&` (parent reference).
|
||||
- Mixin: `@mixin` / `@include` — reusable block.
|
||||
- Function: `@function` — value 반환.
|
||||
- Module system: `@use` / `@forward` (2019+, 매 `@import` deprecated).
|
||||
- Inheritance: `@extend`.
|
||||
- Math/color operations.
|
||||
|
||||
### 매 2026 상태
|
||||
- Native CSS: nesting (Baseline 2024), `var()`, `@layer`, container query — 매 SCSS 의 일부 기능 native.
|
||||
- Tailwind v4: 매 utility-first → SCSS 의 design system 사용 감소.
|
||||
- 여전히 활발: Bootstrap 5, Material Design legacy, Rails 8 default.
|
||||
|
||||
### 매 응용
|
||||
1. Design system: 매 token (color, spacing, typography) 의 SCSS variable.
|
||||
2. Component library: 매 mixin 으로 button/card variant 생성.
|
||||
3. Theme switching: 매 SCSS map + CSS custom property hybrid.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Variable + Nesting
|
||||
```scss
|
||||
$primary: #007bff;
|
||||
$radius: 4px;
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: $radius;
|
||||
|
||||
&__header {
|
||||
color: $primary;
|
||||
|
||||
&:hover {
|
||||
color: darken($primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mixin
|
||||
```scss
|
||||
@mixin flex-center($direction: row) {
|
||||
display: flex;
|
||||
flex-direction: $direction;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
@include flex-center(column);
|
||||
}
|
||||
```
|
||||
|
||||
### `@use` module system (modern, replaces `@import`)
|
||||
```scss
|
||||
// _colors.scss
|
||||
$primary: #007bff;
|
||||
$secondary: #6c757d;
|
||||
|
||||
// _mixins.scss
|
||||
@mixin shadow($level: 1) {
|
||||
box-shadow: 0 #{$level * 2}px #{$level * 4}px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
// app.scss
|
||||
@use 'colors' as c;
|
||||
@use 'mixins' as m;
|
||||
|
||||
.button {
|
||||
background: c.$primary;
|
||||
@include m.shadow(2);
|
||||
}
|
||||
```
|
||||
|
||||
### Map + each loop (theme tokens)
|
||||
```scss
|
||||
$spacing: (
|
||||
xs: 4px,
|
||||
sm: 8px,
|
||||
md: 16px,
|
||||
lg: 24px,
|
||||
);
|
||||
|
||||
@each $name, $value in $spacing {
|
||||
.p-#{$name} { padding: $value; }
|
||||
.m-#{$name} { margin: $value; }
|
||||
}
|
||||
```
|
||||
|
||||
### CSS custom property + SCSS hybrid (theme switch)
|
||||
```scss
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--text: #000;
|
||||
}
|
||||
[data-theme='dark'] {
|
||||
--bg: #111;
|
||||
--text: #eee;
|
||||
}
|
||||
|
||||
@function token($name) {
|
||||
@return var(--#{$name});
|
||||
}
|
||||
|
||||
body {
|
||||
background: token(bg);
|
||||
color: token(text);
|
||||
}
|
||||
```
|
||||
|
||||
### Function
|
||||
```scss
|
||||
@function rem($px) {
|
||||
@return #{$px / 16}rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: rem(24); // 1.5rem
|
||||
}
|
||||
```
|
||||
|
||||
### Vite 7 SCSS 설정
|
||||
```ts
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler', // Dart Sass embedded API
|
||||
additionalData: `@use "@/styles/_globals" as *;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New project (greenfield 2026) | Native CSS + Tailwind v4 |
|
||||
| Existing Bootstrap project | SCSS (built-in) |
|
||||
| Design system with tokens | SCSS map + CSS var hybrid |
|
||||
| Theme switching | CSS custom property (SCSS optional) |
|
||||
| Rails 8 / Phoenix | SCSS (default) |
|
||||
| React app | CSS Modules / Tailwind / vanilla-extract |
|
||||
|
||||
**기본값**: 매 새 project 매 native CSS + Tailwind v4. 매 SCSS 매 design-system / legacy 의 limited use.
|
||||
|
||||
## 🔗 Graph
|
||||
- 응용: [[Material_Design]] · [[Design_System]]
|
||||
- Adjacent: [[Tailwind_CSS]] · [[CSS_Modules]] · [[vanilla-extract]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 large design system, theme variable, legacy Bootstrap project.
|
||||
**언제 X**: 매 utility-first (Tailwind), 매 native CSS nesting/var 으로 충분한 경우.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`@import` 사용**: deprecated (2024) — 매 `@use`/`@forward` 사용.
|
||||
- **Deep nesting (4+ level)**: 매 specificity 폭발, BEM-like flat 으로.
|
||||
- **LibSass 사용**: 매 deprecated 2020 — Dart Sass 만.
|
||||
- **Color function 의 변경 무시**: 매 `darken()` 의 공식 deprecation (2024) → `color.adjust()` 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Sass 1.81+, Dart Sass docs, Vite 7 release).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — `@use` module / Dart Sass / Vite 통합 |
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
---
|
||||
id: wiki-2026-0508-seo-중심의-마케팅-및-블로그-사이트-구축
|
||||
title: SEO 중심의 마케팅 및 블로그 사이트 구축
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [seo-marketing-site, blog-site-construction]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [seo, marketing-site, blog, ssg, frontend, next-js, astro]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: astro-next-js
|
||||
---
|
||||
|
||||
# SEO 중심의 마케팅 및 블로그 사이트 구축
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 organic search 의 의 LCP < 2.5s + crawlability + structured data + content velocity 의 four-pillar 의 site architecture."**. 2026 의 의 SEO-first build 의 의 default stack 의 Astro / Next.js (SSG) + MDX content + Schema.org JSON-LD + Core Web Vitals optimization — 매 GEO (Generative Engine Optimization, AI overview) 의 등장 의 의 traditional SEO 의 augment.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4-pillar
|
||||
1. **Crawlability**: server-rendered HTML, sitemap, robots.txt, canonical URLs, hreflang.
|
||||
2. **Performance**: LCP < 2.5s, INP < 200ms, CLS < 0.1 — Core Web Vitals 의 ranking factor.
|
||||
3. **Content**: depth, freshness, E-E-A-T (Experience, Expertise, Authoritativeness, Trust).
|
||||
4. **Structure**: Schema.org JSON-LD, OpenGraph, semantic HTML, internal linking.
|
||||
|
||||
### 매 stack 선택
|
||||
- **Astro**: content-heavy, Islands architecture, minimal JS — best LCP.
|
||||
- **Next.js (App Router, SSG mode)**: hybrid 가능, MDX support, vast ecosystem.
|
||||
- **Hugo / 11ty**: pure SSG, sub-second build, no JS runtime.
|
||||
- **WordPress (headless)**: editorial workflow + Astro/Next frontend.
|
||||
|
||||
### 매 2026 GEO consideration
|
||||
- AI overview (Google SGE, Perplexity, ChatGPT search) 의 citation 의 desired — clear `<h1>`, summary paragraph, bulleted facts, schema markup.
|
||||
- LLM-friendly: clean semantic HTML, llms.txt 의 publishing.
|
||||
|
||||
### 매 응용
|
||||
1. SaaS marketing site (homepage + features + pricing + blog).
|
||||
2. Tech blog with MDX + code highlighting.
|
||||
3. Documentation site (Mintlify, Nextra, Starlight).
|
||||
4. Local business + multi-region landing pages.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Astro 의 blog setup
|
||||
```astro
|
||||
---
|
||||
// src/pages/blog/[slug].astro
|
||||
import { getCollection } from 'astro:content';
|
||||
import Layout from '@/layouts/Article.astro';
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({ params: { slug: post.slug }, props: { post } }));
|
||||
}
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
<Layout
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
canonical={`https://site.com/blog/${post.slug}`}
|
||||
ogImage={post.data.ogImage}
|
||||
>
|
||||
<article>
|
||||
<h1>{post.data.title}</h1>
|
||||
<Content />
|
||||
</article>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
### JSON-LD (Article schema)
|
||||
```tsx
|
||||
// components/ArticleSchema.tsx
|
||||
export function ArticleSchema({ post }: { post: Post }) {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: post.title,
|
||||
datePublished: post.publishedAt,
|
||||
dateModified: post.updatedAt,
|
||||
author: { '@type': 'Person', name: post.author },
|
||||
image: post.ogImage,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Sitemap (Next.js App Router)
|
||||
```ts
|
||||
// app/sitemap.ts
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { getAllPosts } from '@/lib/posts';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const posts = await getAllPosts();
|
||||
return [
|
||||
{ url: 'https://site.com', lastModified: new Date(), priority: 1 },
|
||||
...posts.map((p) => ({
|
||||
url: `https://site.com/blog/${p.slug}`,
|
||||
lastModified: p.updatedAt,
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.7,
|
||||
})),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Robots + canonical (Next.js metadata)
|
||||
```ts
|
||||
// app/blog/[slug]/page.tsx
|
||||
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||
const post = await getPost(params.slug);
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.summary,
|
||||
alternates: { canonical: `/blog/${post.slug}` },
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.summary,
|
||||
images: [post.ogImage],
|
||||
type: 'article',
|
||||
},
|
||||
twitter: { card: 'summary_large_image' },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### LCP 최적화 (preload hero image)
|
||||
```html
|
||||
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
|
||||
<img src="/hero.avif" alt="..." width="1200" height="630" loading="eager" decoding="async" />
|
||||
```
|
||||
|
||||
### MDX + content collection (Astro)
|
||||
```ts
|
||||
// src/content/config.ts
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
export const collections = {
|
||||
blog: defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().max(160),
|
||||
publishedAt: z.date(),
|
||||
author: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
ogImage: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
### llms.txt (2026 GEO)
|
||||
```txt
|
||||
# https://site.com/llms.txt
|
||||
# Site: Acme Inc. — SaaS for X
|
||||
# License: CC-BY-4.0 for blog content
|
||||
# Index:
|
||||
- /blog/* — engineering articles
|
||||
- /docs/* — product documentation
|
||||
- /pricing — current pricing
|
||||
```
|
||||
|
||||
### CWV 의 monitoring (Vercel Speed Insights / web-vitals)
|
||||
```ts
|
||||
import { onLCP, onINP, onCLS } from 'web-vitals';
|
||||
|
||||
function send(metric: any) {
|
||||
navigator.sendBeacon('/api/vitals', JSON.stringify(metric));
|
||||
}
|
||||
onLCP(send);
|
||||
onINP(send);
|
||||
onCLS(send);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Stack |
|
||||
|---|---|
|
||||
| Pure content / blog | **Astro** (Islands, near-zero JS) |
|
||||
| Marketing + app shell | **Next.js (App Router, SSG/ISR)** |
|
||||
| Editorial team (CMS) | **Headless WP / Sanity + Astro** |
|
||||
| Docs site | **Starlight / Nextra / Mintlify** |
|
||||
| Multi-region SEO | Next.js + i18n routing + hreflang |
|
||||
|
||||
**기본값**: Astro + MDX + Vercel/Netlify deploy + Sanity/Contentlayer (if CMS needed).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[SEO]] · [[프론트엔드_및_UIUX_표준|Frontend-Architecture]]
|
||||
- 응용: [[Astro]] · [[MDX]]
|
||||
- Adjacent: [[Core Web Vitals Optimization (INP, LCP 개선)|Core-Web-Vitals]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: marketing site, blog, docs, public-facing SEO-critical content, GEO optimization.
|
||||
**언제 X**: authenticated app (SPA), real-time UI (SPA/SSR), no SEO requirement.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **SPA 의 marketing site**: crawlable HTML 의 부재 — SSG/SSR 의 mandatory.
|
||||
- **Render-blocking JS / fonts**: LCP 의 destroy — `font-display: swap`, defer non-critical JS.
|
||||
- **Image 의 dimensions 의 unspecified**: CLS regression — `width`/`height` 의 항상 의 specify.
|
||||
- **Duplicate content 의 canonical 의 부재**: SEO penalty — `<link rel="canonical">` 의 필수.
|
||||
- **Schema.org 없음**: rich result + AI overview citation 의 lose — JSON-LD 의 add.
|
||||
- **Sitemap 의 stale**: ISR / on-demand revalidate or build-time regenerate.
|
||||
- **GEO ignore**: AI search traffic 의 2026 의 surge — llms.txt + clear semantic HTML.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Google Search Central, web.dev CWV, Astro/Next.js 2026 docs, llms.txt spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full 2026 SEO playbook with GEO + Astro/Next stack |
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
---
|
||||
id: wiki-2026-0508-spa-single-page-application
|
||||
title: SPA (Single Page Application)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [single-page-app, client-side-routing-app]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, spa, react, vue, routing, architecture]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-vue-svelte
|
||||
---
|
||||
|
||||
# SPA (Single Page Application)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 single HTML shell 의 의 client-side routing + dynamic rendering 의 driven 의 web app 의 architecture."**. 매 2010s 의 의 dominant pattern (AngularJS, Backbone, Ember → React, Vue, Angular), 매 2026 의 RSC / SSR / Islands 의 ascendency 의 의 SPA 의 niche 화 — 매 highly-interactive dashboards, internal tools, complex stateful UIs 의 의 the right tool 이며 marketing/content 의 의 의 SSG/SSR 의 권장.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 정의
|
||||
- 1 HTML document 의 load — subsequent navigation 의 JS 의 driven (no full page reload).
|
||||
- Client-side router 의 URL ↔ component tree 의 mapping.
|
||||
- Data 의 fetch via XHR/Fetch — JSON API or RPC.
|
||||
- State 의 client 의 in-memory (Redux, Zustand, Jotai, TanStack Query).
|
||||
|
||||
### 매 trade-offs
|
||||
- **Pros**: snappy in-app navigation, rich interactivity, app-like UX, shared state across views.
|
||||
- **Cons**: SEO 의 weak (without SSR), initial bundle 의 large, white screen 의 risk, accessibility 의 careful 의 wiring.
|
||||
|
||||
### 매 vs. modern alternatives
|
||||
- **SSR / SSG (Next.js, Remix)**: server 의 HTML 의 generate, hydration 의 client interactivity 의 add.
|
||||
- **RSC (React Server Components, 2026 mainstream)**: server-only components + client islands.
|
||||
- **Islands (Astro, Fresh, 11ty)**: static HTML + targeted hydration 의 islands.
|
||||
- **MPA (Multi-Page App)**: traditional server-rendered pages — Hotwire / Inertia 의 의 modern revival.
|
||||
|
||||
### 매 응용
|
||||
1. Internal admin dashboards (high interactivity, no SEO need).
|
||||
2. Authenticated SaaS apps (Linear, Figma, Notion).
|
||||
3. Real-time collaboration (CRDT-driven, websocket-heavy).
|
||||
4. Browser-based tools (Excalidraw, Tldraw).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React Router (data router, 2026)
|
||||
```tsx
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
Component: Layout,
|
||||
children: [
|
||||
{ index: true, Component: Home },
|
||||
{ path: 'projects/:id', Component: Project, loader: projectLoader },
|
||||
{ path: '*', Component: NotFound },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export const App = () => <RouterProvider router={router} />;
|
||||
```
|
||||
|
||||
### Data fetching (TanStack Query)
|
||||
```tsx
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export function ProjectView({ id }: { id: string }) {
|
||||
const { data, isPending, error } = useQuery({
|
||||
queryKey: ['project', id],
|
||||
queryFn: () => fetch(`/api/projects/${id}`).then((r) => r.json()),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
if (isPending) return <Skeleton />;
|
||||
if (error) return <ErrorBoundary error={error} />;
|
||||
return <ProjectDetail project={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Client state (Zustand)
|
||||
```ts
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export const useUIStore = create(
|
||||
persist<{ sidebar: boolean; toggle: () => void }>(
|
||||
(set) => ({
|
||||
sidebar: true,
|
||||
toggle: () => set((s) => ({ sidebar: !s.sidebar })),
|
||||
}),
|
||||
{ name: 'ui-store' },
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Code splitting (lazy routes)
|
||||
```tsx
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const Settings = lazy(() => import('./routes/Settings'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/settings',
|
||||
element: (
|
||||
<Suspense fallback={<RouteSkeleton />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### History API (without router)
|
||||
```ts
|
||||
window.history.pushState({}, '', '/projects/42');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
window.addEventListener('popstate', () => {
|
||||
render(parseRoute(location.pathname));
|
||||
});
|
||||
```
|
||||
|
||||
### Auth guard (route loader)
|
||||
```tsx
|
||||
async function projectLoader({ params }: LoaderFunctionArgs) {
|
||||
const session = await getSession();
|
||||
if (!session) throw redirect('/login');
|
||||
return fetch(`/api/projects/${params.id}`).then((r) => r.json());
|
||||
}
|
||||
```
|
||||
|
||||
### Vite SPA의 setup
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: { sourcemap: true, target: 'es2022' },
|
||||
server: { historyApiFallback: true },
|
||||
});
|
||||
```
|
||||
|
||||
### SEO 의 fallback (prerender + hydrate)
|
||||
```ts
|
||||
// vite-plugin-prerender
|
||||
import prerender from 'vite-plugin-prerender';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
react(),
|
||||
prerender({ routes: ['/', '/about', '/pricing'] }),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Marketing site, blog | **SSG** (Astro, Next.js static, 11ty) |
|
||||
| Content site with personalization | **SSR** (Next.js, Remix) |
|
||||
| Dashboards, admin tools | **SPA** (React Router + Vite) |
|
||||
| Highly interactive editor | **SPA** with code-splitting |
|
||||
| Public app needing SEO + interactivity | **Next.js (RSC + client islands)** |
|
||||
| MPA-style with sprinkles | **Hotwire / Inertia.js** |
|
||||
|
||||
**기본값**: 2026 의 default 는 **Next.js / Remix (SSR + RSC)** — pure SPA 는 internal tools / authenticated apps 의 으로 의 reserve.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[프론트엔드_및_UIUX_표준|Frontend-Architecture]]
|
||||
- 변형: [[SSR]] · [[SSG]] · [[Islands-Architecture]] · [[React-Server-Components]]
|
||||
- Adjacent: [[Vite]] · [[Code-Splitting]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: highly-interactive authenticated app, no SEO requirement, complex client state, real-time collaboration.
|
||||
**언제 X**: marketing/content site (SSG), public SEO-critical content (SSR/RSC), simple form-driven app (MPA).
|
||||
|
||||
## 안티패턴
|
||||
- **SPA 의 marketing site**: SEO 의 weak, LCP 의 poor — SSG 의 사용.
|
||||
- **Bundle 의 single chunk**: route-based code splitting 의 default.
|
||||
- **Auth state 의 localStorage 의 raw token**: HttpOnly cookie + refresh flow 의 사용.
|
||||
- **Client routing 의 server fallback 없음**: 404 의 deep link 의 — `historyApiFallback` / catch-all rewrite.
|
||||
- **No skeleton / suspense**: white screen on slow data — Suspense + skeletons.
|
||||
- **Global state 의 overuse**: server state 의 TanStack Query, UI state 의 Zustand/Jotai 의 separate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React Router 7 docs, Vue Router 4, MDN SPA architecture, web.dev rendering patterns 2026).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full SPA architecture with 2026 trade-offs vs RSC/Islands |
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
---
|
||||
id: wiki-2026-0508-saas-대시보드-및-이커머스-레이아웃-구축
|
||||
title: SaaS 대시보드 및 이커머스 레이아웃 구축
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [SaaS Dashboard Layout, Ecommerce Layout, Admin Layout]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, layout, saas, dashboard, ecommerce, nextjs, shadcn]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Next.js 16 / Tailwind v4 / shadcn-ui
|
||||
---
|
||||
|
||||
# SaaS 대시보드 및 이커머스 레이아웃 구축
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 SaaS 의 sidebar+content shell + 매 Ecommerce 의 PLP/PDP/Cart"**. 2026 stack 매 Next.js 16 (App Router) + Tailwind v4 + shadcn-ui + Radix Primitive + TanStack Query. 매 Server Component 로 data fetch, Client Component 로 interactivity.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 SaaS Dashboard 패턴
|
||||
- **Shell**: sidebar (collapsible) + topbar (search/user menu) + content area.
|
||||
- **Navigation**: 매 nested section, persistent state, breadcrumb.
|
||||
- **Data widgets**: 매 stat card, chart (Recharts/Tremor v3), table (TanStack Table).
|
||||
- **Multi-tenant**: 매 workspace switcher.
|
||||
|
||||
### 매 Ecommerce 패턴
|
||||
- **PLP (Product List Page)**: 매 grid + filter sidebar + sort + pagination/infinite scroll.
|
||||
- **PDP (Product Detail Page)**: 매 image gallery + variant selector + add-to-cart.
|
||||
- **Cart/Checkout**: 매 cart drawer + multi-step checkout + Stripe Elements.
|
||||
- **Search**: 매 typeahead + facet filtering (Algolia/MeiliSearch).
|
||||
|
||||
### 매 Stack (2026)
|
||||
- Framework: Next.js 16 App Router / Remix 3 / Astro 5.
|
||||
- UI: shadcn-ui v2 + Radix + Tailwind v4 (Oxide engine).
|
||||
- Data: TanStack Query 5 + Server Action / RSC.
|
||||
- Forms: react-hook-form + Zod.
|
||||
- Tables: TanStack Table v8.
|
||||
- Charts: Tremor v3 / Recharts.
|
||||
|
||||
### 매 응용
|
||||
1. SaaS admin: Linear-style sidebar.
|
||||
2. Ecommerce: Shopify Hydrogen / Next.js Commerce 템플릿.
|
||||
3. Internal tool: Retool-like 매 form/table heavy.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### App Router Layout (Next.js 16)
|
||||
```tsx
|
||||
// app/(dashboard)/layout.tsx
|
||||
import { Sidebar } from '@/components/sidebar';
|
||||
import { Topbar } from '@/components/topbar';
|
||||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const user = await getCurrentUser();
|
||||
return (
|
||||
<div className="grid h-screen grid-cols-[260px_1fr]">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<Topbar user={user} />
|
||||
<main className="flex-1 overflow-auto p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### shadcn-ui Sidebar (collapsible)
|
||||
```tsx
|
||||
'use client';
|
||||
import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||
import { Home, Users, Settings } from 'lucide-react';
|
||||
|
||||
const items = [
|
||||
{ title: 'Home', url: '/', icon: Home },
|
||||
{ title: 'Users', url: '/users', icon: Users },
|
||||
{ title: 'Settings', url: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<a href={item.url}>
|
||||
<item.icon className="size-4" />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Stat Card Grid
|
||||
```tsx
|
||||
function StatCards({ stats }: { stats: Stat[] }) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((s) => (
|
||||
<div key={s.id} className="rounded-lg border bg-card p-6">
|
||||
<div className="text-sm text-muted-foreground">{s.label}</div>
|
||||
<div className="mt-2 text-3xl font-semibold">{s.value}</div>
|
||||
<div className="mt-1 text-sm text-emerald-600">{s.delta}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TanStack Table (server pagination)
|
||||
```tsx
|
||||
'use client';
|
||||
import { useReactTable, getCoreRowModel } from '@tanstack/react-table';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export function UsersTable() {
|
||||
const [page, setPage] = useState(0);
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users', page],
|
||||
queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: data?.users ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: data?.totalPages,
|
||||
});
|
||||
|
||||
return <table>...</table>;
|
||||
}
|
||||
```
|
||||
|
||||
### Ecommerce PLP (RSC + filter)
|
||||
```tsx
|
||||
// app/products/page.tsx
|
||||
export default async function ProductsPage({ searchParams }: { searchParams: Promise<{ category?: string; sort?: string }> }) {
|
||||
const params = await searchParams;
|
||||
const products = await getProducts({ category: params.category, sort: params.sort });
|
||||
return (
|
||||
<div className="grid grid-cols-[240px_1fr] gap-6">
|
||||
<FilterSidebar />
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
{products.map(p => <ProductCard key={p.id} product={p} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Cart Drawer (Zustand + shadcn Sheet)
|
||||
```tsx
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
|
||||
const useCart = create<{ items: Item[]; add: (i: Item) => void; remove: (id: string) => void }>((set) => ({
|
||||
items: [],
|
||||
add: (i) => set((s) => ({ items: [...s.items, i] })),
|
||||
remove: (id) => set((s) => ({ items: s.items.filter(x => x.id !== id) })),
|
||||
}));
|
||||
|
||||
export function CartDrawer() {
|
||||
const items = useCart((s) => s.items);
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger>Cart ({items.length})</SheetTrigger>
|
||||
<SheetContent>
|
||||
{items.map(i => <div key={i.id}>{i.name}</div>)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action (Add to cart)
|
||||
```tsx
|
||||
// app/actions/cart.ts
|
||||
'use server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function addToCart(productId: string, qty: number) {
|
||||
const cart = (await cookies()).get('cart');
|
||||
// ... persist
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New SaaS dashboard | Next.js 16 + shadcn + Tailwind v4 |
|
||||
| Ecommerce | Next.js Commerce 또는 Shopify Hydrogen |
|
||||
| Internal admin | Refine.dev / Retool |
|
||||
| Mobile-first ecommerce | Astro + Solid Islands |
|
||||
| Real-time dashboard | Next.js + WebSocket (Pusher/Ably) |
|
||||
| Heavy data viz | Tremor + Recharts |
|
||||
|
||||
**기본값**: 매 SaaS 매 Next.js + shadcn-ui + Tailwind v4. 매 Ecommerce 매 Next.js Commerce 템플릿 fork.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Frontend_Architecture]]
|
||||
- Adjacent: [[Next.js]] · [[Tailwind_v4]] · [[shadcn-ui]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: greenfield SaaS / ecommerce 의 layout shell 구축, design system 적용.
|
||||
**언제 X**: 매 micro-frontend / heavy SSR-disabled 의 SPA — 매 다른 stack (Vite + React Router) 적합.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 component 'use client'**: 매 RSC 의 benefit 상실 — server-first.
|
||||
- **inline style + Tailwind 혼재**: 매 inconsistency — design token 통일.
|
||||
- **Custom UI library scratch**: 매 shadcn-ui copy-paste 가 maintenance 우위.
|
||||
- **Cart in localStorage only**: 매 cross-device sync 불가 — server-side cart + cookie.
|
||||
- **No skeleton during loading**: 매 CLS / UX 저하 — Suspense + skeleton.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Next.js 16 docs, shadcn-ui v2, Tailwind v4, Vercel Commerce).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Next.js 16 / shadcn / Tailwind v4 modern stack |
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
id: wiki-2026-0508-scripts
|
||||
title: Scripts
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [npm scripts, package.json scripts, Build Scripts]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [npm, build, automation, package-json]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Node.js + npm
|
||||
---
|
||||
|
||||
# Scripts
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 scripts는 package.json 의 entry point 인 명령어 alias"**. `npm run <name>` 으로 실행되며, dev/build/test/lint/deploy 등의 lifecycle automation 을 정의. 2026 기준 npm/pnpm/bun/yarn 호환, 매 monorepo 에서는 turbo/nx 가 orchestration.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 lifecycle
|
||||
- **pre/post**: `prebuild` → `build` → `postbuild` 자동 실행
|
||||
- **special names**: `start`, `test`, `install`, `prepare`
|
||||
- **arbitrary**: 매 다른 이름 은 `npm run <name>`
|
||||
|
||||
### 매 cross-platform
|
||||
- `cross-env`, `rimraf`, `mkdirp` 등으로 OS 차이 흡수
|
||||
- 매 modern 대안: zx, execa, tsx-based scripts
|
||||
|
||||
### 매 응용
|
||||
1. Local dev: `dev`, `build`, `test`, `lint`.
|
||||
2. CI: `ci:test`, `ci:build`, `ci:deploy`.
|
||||
3. Tooling: `typecheck`, `format`, `analyze`.
|
||||
4. Release: `release`, `publish:dry`.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Modern package.json scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"format": "biome format --write .",
|
||||
"clean": "rimraf .next dist coverage",
|
||||
"prepare": "husky",
|
||||
"release": "changeset publish"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pre/post hooks
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"prebuild": "npm run typecheck && npm run lint",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"postbuild": "node scripts/copy-assets.mjs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composing with `npm-run-all` or `concurrently`
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "concurrently -n web,api -c blue,green \"npm:dev:web\" \"npm:dev:api\"",
|
||||
"dev:web": "next dev",
|
||||
"dev:api": "tsx watch server/index.ts",
|
||||
"verify": "run-p typecheck lint test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript script with tsx
|
||||
```typescript
|
||||
// scripts/seed.ts
|
||||
import { db } from "../src/lib/db";
|
||||
import { users } from "../src/lib/db/schema";
|
||||
|
||||
await db.insert(users).values([
|
||||
{ email: "alice@example.com" },
|
||||
{ email: "bob@example.com" },
|
||||
]);
|
||||
console.log("매 seed complete");
|
||||
```
|
||||
|
||||
```json
|
||||
{ "scripts": { "db:seed": "tsx scripts/seed.ts" } }
|
||||
```
|
||||
|
||||
### zx automation
|
||||
```javascript
|
||||
#!/usr/bin/env zx
|
||||
import "zx/globals";
|
||||
|
||||
const branches = (await $`git branch --merged main`).stdout
|
||||
.split("\n")
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b && !b.startsWith("*") && b !== "main");
|
||||
|
||||
for (const b of branches) {
|
||||
await $`git branch -d ${b}`;
|
||||
}
|
||||
echo`매 deleted ${branches.length} merged branches`;
|
||||
```
|
||||
|
||||
### Cross-platform env
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"test:debug": "cross-env DEBUG=app:* vitest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Monorepo with pnpm + turbo
|
||||
```json
|
||||
// root package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"test": "turbo run test --filter=...[origin/main]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bun-native scripts (faster startup)
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "bun --hot src/index.ts",
|
||||
"test": "bun test",
|
||||
"build": "bun build src/index.ts --outdir dist --target node"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CI matrix script
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"ci:lint": "biome ci .",
|
||||
"ci:typecheck": "tsc --noEmit",
|
||||
"ci:test": "vitest run --coverage --reporter=verbose",
|
||||
"ci:build": "next build",
|
||||
"ci": "run-s ci:lint ci:typecheck ci:test ci:build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 매 simple alias | npm script directly |
|
||||
| 매 multi-step orchestration | npm-run-all / concurrently |
|
||||
| Complex shell scripting | zx / bash file |
|
||||
| TS logic | tsx / bun + TS file |
|
||||
| Monorepo | turbo / nx |
|
||||
|
||||
**기본값**: npm scripts + tsx for TS + concurrently for parallel. 매 monorepo 면 turbo.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Node.js]]
|
||||
- 변형: [[Task]] · [[Turbo]] · [[Nx]]
|
||||
- 응용: [[Husky Git Hooks]]
|
||||
- Adjacent: [[tsx]] · [[bun]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: package.json 의 standard automation, dev/build/test/lint pipeline.
|
||||
**언제 X**: 매 100+ lines of bash logic — 매 standalone script file 로 추출.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Inline complex shell**: `&& if [[ ... ]]; then ... fi` in JSON → 매 unreadable. zx / bash file.
|
||||
- **No clean script**: 매 stale build artifact debug 어려움. `clean` 필수.
|
||||
- **OS-specific commands**: `rm -rf` Windows 실패 → cross-platform 도구.
|
||||
- **Hidden side effects in postinstall**: 매 supply-chain risk. 신중.
|
||||
- **Duplicate scripts across packages**: 매 monorepo 면 turbo / shared config.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (npm docs, package.json spec).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — npm scripts full content |
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
---
|
||||
id: wiki-2026-0508-server-side-rendering-ssr
|
||||
title: Server Side Rendering (SSR)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [SSR, Server Rendering, isomorphic rendering]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [ssr, react, nextjs, rendering, performance]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Next.js 15 / React 19
|
||||
---
|
||||
|
||||
# Server Side Rendering (SSR)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 SSR 은 server 에서 HTML 을 생성해 first paint 를 빠르게, SEO 를 가능하게"**. 2026 기준 React 19 의 streaming SSR + Suspense + Server Components 가 표준. 매 trade-off 는 server compute cost vs CSR-only 의 blank-screen 제거. 매 modern 변형: SSG (build-time), ISR (revalidate), PPR (Partial Prerender).
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 rendering modes
|
||||
- **CSR**: 매 client only — slow first paint, no SEO without JS
|
||||
- **SSR**: 매 server-render full HTML per request
|
||||
- **SSG**: 매 build-time HTML (static)
|
||||
- **ISR**: 매 SSG + on-demand or time-based revalidation
|
||||
- **PPR**: 매 partial prerender — static shell + dynamic holes (Next.js 15)
|
||||
|
||||
### 매 hydration
|
||||
- Server HTML 송신 → client JS 가 attach (event handler 연결)
|
||||
- Streaming SSR: HTML 을 chunk 로 진행 송신
|
||||
- Selective hydration: visible / interacted 부분 우선
|
||||
|
||||
### 매 응용
|
||||
1. Marketing / blog (SEO + fast paint).
|
||||
2. E-commerce PDP (per-user pricing + SEO).
|
||||
3. Dashboard shells (PPR — static shell + dynamic data).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Next.js 15 RSC + streaming
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import { ProductGrid } from "./product-grid";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Products</h1>
|
||||
<Suspense fallback={<div>매 loading…</div>}>
|
||||
<ProductGrid />
|
||||
</Suspense>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// app/product-grid.tsx (RSC, runs on server)
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function ProductGrid() {
|
||||
const products = await db.product.findMany({ take: 20 });
|
||||
return (
|
||||
<ul>
|
||||
{products.map((p) => <li key={p.id}>{p.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Prerendering (PPR)
|
||||
```tsx
|
||||
// app/dashboard/page.tsx
|
||||
export const experimental_ppr = true;
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { StaticHeader } from "./header";
|
||||
import { LiveMetrics } from "./metrics";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<StaticHeader /> {/* prerendered */}
|
||||
<Suspense fallback={<MetricsSkeleton />}>
|
||||
<LiveMetrics /> {/* dynamic, streamed */}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ISR with revalidate
|
||||
```tsx
|
||||
// app/posts/[slug]/page.tsx
|
||||
export const revalidate = 60; // every 60s
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`,
|
||||
{ next: { revalidate: 60, tags: [`post:${slug}`] } }).then((r) => r.json());
|
||||
return <article>{post.title}</article>;
|
||||
}
|
||||
```
|
||||
|
||||
### On-demand revalidation
|
||||
```typescript
|
||||
// app/api/revalidate/route.ts
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { tag } = await req.json();
|
||||
revalidateTag(tag);
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Pure React 19 streaming SSR
|
||||
```typescript
|
||||
// server.ts
|
||||
import { renderToReadableStream } from "react-dom/server";
|
||||
import App from "./App";
|
||||
|
||||
export default async function handler(req: Request): Promise<Response> {
|
||||
const stream = await renderToReadableStream(<App url={req.url} />, {
|
||||
bootstrapModules: ["/client.js"],
|
||||
onError: (err) => console.error(err),
|
||||
});
|
||||
await stream.allReady; // remove for true streaming
|
||||
return new Response(stream, {
|
||||
headers: { "content-type": "text/html" },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Client hydration entry
|
||||
```typescript
|
||||
// client.ts
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
hydrateRoot(document, <App url={location.href} />);
|
||||
```
|
||||
|
||||
### Cache headers for SSR
|
||||
```typescript
|
||||
// app/api/data/route.ts
|
||||
export async function GET() {
|
||||
return Response.json(
|
||||
{ data: 42 },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server-only utility
|
||||
```typescript
|
||||
// lib/server-only.ts
|
||||
import "server-only"; // 매 import in client component throws
|
||||
|
||||
import { db } from "./db";
|
||||
|
||||
export async function loadSecret() {
|
||||
return db.secret.findFirst();
|
||||
}
|
||||
```
|
||||
|
||||
### Edge runtime SSR
|
||||
```typescript
|
||||
// app/page.tsx
|
||||
export const runtime = "edge";
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch("https://api.example.com/edge").then((r) => r.json());
|
||||
return <pre>{JSON.stringify(data)}</pre>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Static marketing page | SSG (`generateStaticParams`) |
|
||||
| Per-user dynamic | SSR / RSC |
|
||||
| Mostly static + small dynamic | **PPR** |
|
||||
| Data refreshes minutes-scale | ISR with revalidate |
|
||||
| Internal app, no SEO | CSR (Vite SPA) sufficient |
|
||||
| Low latency global | Edge runtime SSR |
|
||||
|
||||
**기본값**: Next.js 15 App Router + RSC + Suspense streaming + PPR where applicable.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Rendering Strategies]] · [[Web Performance]]
|
||||
- 변형: [[CSR]] · [[SSG]] · [[ISR]] · [[Streaming SSR]]
|
||||
- 응용: [[Remix]] · [[SvelteKit]] · [[Nuxt]]
|
||||
- Adjacent: [[React Server Components]] · [[Hydration]] · [[Suspense]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: rendering strategy 결정, SEO + first paint 최적화, RSC + Suspense 설계.
|
||||
**언제 X**: 매 internal admin tool with auth-only access (CSR 충분), 매 매우 dynamic real-time app (WebSocket-driven).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **SSR everything**: 매 unnecessary server compute. 매 marketing page → SSG.
|
||||
- **No streaming**: 매 await all data → blank for 5s. 매 Suspense + streaming.
|
||||
- **Hydration mismatch**: server `Date.now()` vs client → 매 warning. `suppressHydrationWarning` 또는 client-only render.
|
||||
- **Secret in client component**: 매 env var leak. `server-only` import.
|
||||
- **Massive RSC payload**: 매 props 에 huge JSON. 매 boundary 재설계.
|
||||
- **Forgetting cache tags**: ISR 인데 invalidation 못 함.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Next.js 15 docs, React 19 docs, Vercel blog 2026).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — SSR full content |
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
id: wiki-2026-0508-spectre
|
||||
title: Spectre
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Spectre Attack, CVE-2017-5753, CVE-2017-5715, Branch Target Injection]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [security, cpu, side-channel, speculation, microarchitecture]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: c
|
||||
framework: x86-arm
|
||||
---
|
||||
|
||||
# Spectre
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 speculative execution 의 microarchitectural side-effect leak"**. 2018 Kocher et al. discovery — branch predictor / BTB 를 mistrained 시켜 out-of-bounds load 의 cache footprint 로 secret 을 추출. 2026 현재 Variant 1/2/4 + Retbleed/Inception 등 8년차 ongoing mitigation arms race.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Variants
|
||||
- **V1 (Bounds Check Bypass, CVE-2017-5753)**: speculative array access past bounds.
|
||||
- **V2 (Branch Target Injection, CVE-2017-5715)**: BTB poisoning — indirect call gadget hijack.
|
||||
- **V4 (Speculative Store Bypass, CVE-2018-3639)**: store-to-load forwarding misprediction.
|
||||
- **Retbleed (2022)**: return predictor 의 V2 variant.
|
||||
- **Inception (2023)**: AMD Zen recursive speculation.
|
||||
|
||||
### 매 Mechanism
|
||||
- **Speculation**: CPU executes past branch before resolution.
|
||||
- **Transient window**: ~100-200 cycles before rollback.
|
||||
- **Covert channel**: cache (Flush+Reload) / port contention / TLB.
|
||||
- **Architectural state**: rolled back. Microarchitectural: persists.
|
||||
|
||||
### 매 응용
|
||||
1. JS sandbox escape (browser → cross-origin memory read).
|
||||
2. KVM guest → host memory leak.
|
||||
3. Kernel ASLR break + secret exfiltration.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### V1 gadget (classic)
|
||||
```c
|
||||
// Vulnerable: array2 cache state leaks array1[x]
|
||||
uint8_t array1[16];
|
||||
uint8_t array2[256 * 4096];
|
||||
|
||||
void victim(size_t x) {
|
||||
if (x < 16) { // mistrained branch
|
||||
uint8_t v = array1[x]; // x can be OOB during speculation
|
||||
uint8_t leak = array2[v * 4096]; // cache footprint encodes v
|
||||
}
|
||||
}
|
||||
// Attacker: train with valid x, then call with OOB x,
|
||||
// flush array2, victim(), then time array2[i*4096] reads.
|
||||
```
|
||||
|
||||
### Flush+Reload primitive
|
||||
```c
|
||||
#include <x86intrin.h>
|
||||
|
||||
static inline uint64_t rdtsc_serial(void) {
|
||||
_mm_lfence();
|
||||
uint64_t t = __rdtsc();
|
||||
_mm_lfence();
|
||||
return t;
|
||||
}
|
||||
|
||||
int probe(volatile uint8_t *addr) {
|
||||
uint64_t t0 = rdtsc_serial();
|
||||
(void)*addr;
|
||||
uint64_t t1 = rdtsc_serial();
|
||||
_mm_clflush((void *)addr);
|
||||
return (t1 - t0) < CACHE_HIT_THRESHOLD; // ~80 cycles
|
||||
}
|
||||
```
|
||||
|
||||
### V1 mitigation: lfence barrier
|
||||
```c
|
||||
void victim_safe(size_t x) {
|
||||
if (x < 16) {
|
||||
_mm_lfence(); // serialize — block speculation
|
||||
uint8_t v = array1[x];
|
||||
uint8_t leak = array2[v * 4096];
|
||||
}
|
||||
}
|
||||
// Cost: ~30-50% perf hit. 매 array_index_nospec() 의 Linux kernel 사용.
|
||||
```
|
||||
|
||||
### V1 mitigation: index masking
|
||||
```c
|
||||
// Linux kernel array_index_nospec
|
||||
static inline size_t mask_idx(size_t idx, size_t sz) {
|
||||
size_t mask = ~(idx >= sz ? ~0UL : 0);
|
||||
return idx & mask; // 0 if OOB, idx otherwise — branchless
|
||||
}
|
||||
|
||||
void victim_masked(size_t x) {
|
||||
if (x < 16) {
|
||||
x = mask_idx(x, 16);
|
||||
uint8_t v = array1[x];
|
||||
uint8_t leak = array2[v * 4096];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### V2 mitigation: retpoline
|
||||
```asm
|
||||
; Replace `jmp *%rax` with retpoline trampoline
|
||||
retpoline:
|
||||
call set_up_target
|
||||
capture:
|
||||
pause
|
||||
lfence
|
||||
jmp capture ; speculation trap
|
||||
set_up_target:
|
||||
mov %rax, (%rsp) ; overwrite return addr
|
||||
ret ; predictor uses RSB, not BTB
|
||||
```
|
||||
|
||||
### Browser mitigation: timer coarsening
|
||||
```js
|
||||
// performance.now() resolution reduced from 5μs → 100μs (Chrome 2018+).
|
||||
// Cross-origin isolation (COOP+COEP) required for SharedArrayBuffer.
|
||||
performance.now(); // 1234.1 (was 1234.123456)
|
||||
|
||||
// SharedArrayBuffer gated on:
|
||||
// Cross-Origin-Opener-Policy: same-origin
|
||||
// Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### Site Isolation (Chrome)
|
||||
```text
|
||||
- Each origin → separate renderer process.
|
||||
- OS-level memory boundary blocks Spectre cross-origin reads.
|
||||
- Cost: +10-20% memory.
|
||||
- Partial Site Isolation on Android (resource-constrained).
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Kernel hot-path bounds check | array_index_nospec (mask) |
|
||||
| Indirect call (kernel/hypervisor) | Retpoline + IBRS/eIBRS |
|
||||
| JS engine bounds check | index masking + speculation barrier |
|
||||
| Browser cross-origin | Site Isolation + COOP/COEP + timer coarsening |
|
||||
| Embedded / no MMU | accept risk, no speculation typically |
|
||||
|
||||
**기본값**: hardware mitigation (eIBRS, IBPB, BHI_DIS_S) on by default + retpoline + array_index_nospec on hot paths.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Side Channel Attack]]
|
||||
- 변형: [[Spectre|Spectre and Meltdown]]
|
||||
- 응용: [[Speculative Execution]]
|
||||
- Adjacent: [[Cache Timing Attack]] · [[Timing Attack]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: explaining microarchitectural attacks, kernel mitigation review, browser sandbox design, audit speculation gadgets.
|
||||
**언제 X**: high-level web app security (XSS/CSRF) — Spectre 의 ~irrelevant in app layer; OS+browser handle it.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **lfence everywhere**: 30-50% perf hit. Use array_index_nospec mask instead.
|
||||
- **Disable speculation entirely**: 5-10x slowdown. Never deploy.
|
||||
- **Trust performance.now() resolution alone**: SharedArrayBuffer 의 still risk without COOP/COEP.
|
||||
- **Ignore V2 retpoline 의 ROP risk**: RSB stuffing required on context switch.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Kocher et al. 2018 paper, Intel/AMD security advisories, Linux kernel `Documentation/admin-guide/hw-vuln/`).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full canonical (V1/V2/V4 + retpoline/lfence/mask + browser isolation) |
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
---
|
||||
id: wiki-2026-0508-state-management-libraries
|
||||
title: State Management Libraries
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [State Management, React State Libraries, Zustand vs Jotai vs Redux]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [state-management, react, zustand, jotai, valtio, redux]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19
|
||||
---
|
||||
|
||||
# State Management Libraries
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 state management 는 component tree 외부의 reactive store 추상화"**. 2026 기준 React server-side data 는 TanStack Query / RSC, client-only state 는 Zustand (top-down) / Jotai (bottom-up atomic) / Valtio (proxy mutable) 가 mainstream. Redux 는 legacy + 매우 큰 enterprise 만. 매 핵심 결정은 "어떤 abstraction 이 mental model 과 맞는가".
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4 가지 패러다임
|
||||
- **Top-down store** (Zustand, Redux): 단일 store, selector
|
||||
- **Atomic** (Jotai, Recoil): 작은 atom 의 graph
|
||||
- **Proxy mutable** (Valtio, MobX): mutable object, auto-tracking
|
||||
- **Server-state** (TanStack Query, SWR): cache + revalidation
|
||||
|
||||
### 매 server vs client
|
||||
- 2026 의 truth: 매 대부분의 "state" 는 사실 server data → 매 TanStack Query 로
|
||||
- 진짜 client state (modal open, form draft, theme) → 매 Zustand / Jotai
|
||||
|
||||
### 매 응용
|
||||
1. Zustand: 매 일반 SPA, mid-size app 의 default.
|
||||
2. Jotai: 매 fine-grained reactivity, derived value graph.
|
||||
3. Valtio: 매 game/canvas, mutable mental model 선호.
|
||||
4. Redux Toolkit: 매 legacy migration, Redux DevTools 의존.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Zustand store with slice pattern
|
||||
```typescript
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
type AuthSlice = {
|
||||
user: { id: string; name: string } | null;
|
||||
login: (u: { id: string; name: string }) => void;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
type CartSlice = {
|
||||
items: { id: string; qty: number }[];
|
||||
add: (id: string) => void;
|
||||
};
|
||||
|
||||
export const useStore = create<AuthSlice & CartSlice>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
user: null,
|
||||
login: (u) => set((s) => { s.user = u; }),
|
||||
logout: () => set((s) => { s.user = null; }),
|
||||
items: [],
|
||||
add: (id) =>
|
||||
set((s) => {
|
||||
const it = s.items.find((i) => i.id === id);
|
||||
if (it) it.qty += 1;
|
||||
else s.items.push({ id, qty: 1 });
|
||||
}),
|
||||
})),
|
||||
{ name: "app-store" },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// usage with selector to prevent re-render
|
||||
const user = useStore((s) => s.user);
|
||||
```
|
||||
|
||||
### Jotai atomic + derived
|
||||
```typescript
|
||||
import { atom, useAtom, useAtomValue } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
export const themeAtom = atomWithStorage<"light" | "dark">("theme", "light");
|
||||
export const filterAtom = atom("");
|
||||
export const itemsAtom = atom<{ id: string; name: string }[]>([]);
|
||||
|
||||
export const filteredItemsAtom = atom((get) => {
|
||||
const f = get(filterAtom).toLowerCase();
|
||||
return get(itemsAtom).filter((i) => i.name.toLowerCase().includes(f));
|
||||
});
|
||||
|
||||
function Search() {
|
||||
const [filter, setFilter] = useAtom(filterAtom);
|
||||
const filtered = useAtomValue(filteredItemsAtom);
|
||||
return (
|
||||
<>
|
||||
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
||||
<ul>{filtered.map((i) => <li key={i.id}>{i.name}</li>)}</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Valtio proxy
|
||||
```typescript
|
||||
import { proxy, useSnapshot } from "valtio";
|
||||
|
||||
export const game = proxy({
|
||||
player: { x: 0, y: 0, hp: 100 },
|
||||
enemies: [] as { id: string; x: number; y: number }[],
|
||||
damage(amount: number) {
|
||||
game.player.hp = Math.max(0, game.player.hp - amount);
|
||||
},
|
||||
});
|
||||
|
||||
function HUD() {
|
||||
const snap = useSnapshot(game);
|
||||
return <div>HP: {snap.player.hp}</div>;
|
||||
}
|
||||
|
||||
// mutate directly outside React
|
||||
setInterval(() => {
|
||||
game.player.x += 1; // 매 자동으로 컴포넌트 re-render
|
||||
}, 16);
|
||||
```
|
||||
|
||||
### Redux Toolkit (when 필요)
|
||||
```typescript
|
||||
import { createSlice, configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
const counter = createSlice({
|
||||
name: "counter",
|
||||
initialState: { value: 0 },
|
||||
reducers: {
|
||||
inc: (s) => { s.value += 1; },
|
||||
incBy: (s, a: { payload: number }) => { s.value += a.payload; },
|
||||
},
|
||||
});
|
||||
|
||||
export const { inc, incBy } = counter.actions;
|
||||
export const store = configureStore({ reducer: { counter: counter.reducer } });
|
||||
```
|
||||
|
||||
### TanStack Query for server state
|
||||
```typescript
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export function usePosts() {
|
||||
return useQuery({
|
||||
queryKey: ["posts"],
|
||||
queryFn: () => fetch("/api/posts").then((r) => r.json()),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePost() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: { title: string }) =>
|
||||
fetch("/api/posts", { method: "POST", body: JSON.stringify(body) }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["posts"] }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Zustand + RSC bridge (Next.js 15)
|
||||
```typescript
|
||||
// store-provider.tsx
|
||||
"use client";
|
||||
import { createContext, useContext, useRef } from "react";
|
||||
import { useStore as useZ, type StoreApi } from "zustand";
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
type State = { count: number; inc: () => void };
|
||||
|
||||
const StoreCtx = createContext<StoreApi<State> | null>(null);
|
||||
|
||||
export function StoreProvider({ children, initialCount = 0 }: any) {
|
||||
const ref = useRef<StoreApi<State>>();
|
||||
if (!ref.current) {
|
||||
ref.current = createStore<State>((set) => ({
|
||||
count: initialCount,
|
||||
inc: () => set((s) => ({ count: s.count + 1 })),
|
||||
}));
|
||||
}
|
||||
return <StoreCtx.Provider value={ref.current}>{children}</StoreCtx.Provider>;
|
||||
}
|
||||
|
||||
export function useAppStore<T>(sel: (s: State) => T): T {
|
||||
const store = useContext(StoreCtx);
|
||||
if (!store) throw new Error("StoreProvider missing");
|
||||
return useZ(store, sel);
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribing outside React (Zustand)
|
||||
```typescript
|
||||
const unsub = useStore.subscribe(
|
||||
(s) => s.user,
|
||||
(user) => console.log("user changed", user),
|
||||
);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Server data (lists, details) | TanStack Query / SWR |
|
||||
| Mid-size client state, simple | **Zustand** |
|
||||
| Fine-grained derived graphs | **Jotai** |
|
||||
| Mutable mental model, games | Valtio |
|
||||
| Legacy / strict Redux DevTools | Redux Toolkit |
|
||||
| Shared single value (theme, locale) | Context (sufficient) |
|
||||
|
||||
**기본값**: server state → TanStack Query, client state → Zustand. Jotai 는 atom graph 가 도움될 때.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- 변형: [[Zustand]] · [[Jotai]] · [[Valtio]] · [[Redux Toolkit]] · [[MobX]] · [[Recoil]]
|
||||
- Adjacent: [[React Server Components]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: client-only state design, store shape 결정, library 선택. 매 SPA 의 boundary.
|
||||
**언제 X**: server data fetch (TanStack Query), 매 single-component local state (useState).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Redux for everything**: 매 small app 에 boilerplate. 매 Zustand 로 충분.
|
||||
- **Server state in Zustand**: cache invalidation 직접 구현 → 매 TanStack Query 사용.
|
||||
- **Atom 폭발**: Jotai 에서 모든 변수를 atom 으로 → 매 graph navigation 혼란.
|
||||
- **No selector**: `useStore((s) => s)` 전체 구독 → 모든 변경에 re-render.
|
||||
- **Mutating snapshot**: Valtio snap 을 직접 mutate → noop or warning.
|
||||
- **Multiple stores for same domain**: 매 single source of truth 위배.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Zustand/Jotai/Valtio/Redux Toolkit official docs, 2026).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — state management libraries canonical full content |
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
id: wiki-2026-0508-style-registry
|
||||
title: Style Registry
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [SSR Style Registry, useServerInsertedHTML, CSS-in-JS SSR]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, ssr, css-in-js, nextjs, react, app-router]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: nextjs
|
||||
---
|
||||
|
||||
# Style Registry
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 SSR streaming 시 CSS-in-JS 의 styles 를 HTML 에 inject 하는 mechanism"**. Next.js 13 App Router 의 `useServerInsertedHTML` hook 도입 — 매 streaming RSC render 도중 styled-components / emotion / @mui 가 generated CSS 를 `<head>` 의 inject. 2026 zero-runtime CSS (Vanilla Extract / Panda) 의 등장 으로 registry 의 less common, but legacy SC/emotion app 의 still required.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Why needed
|
||||
- **CSS-in-JS = runtime styles**: rules generated when component renders.
|
||||
- **SSR**: server renders HTML; client hydrates. Without registry → FOUC (flash of unstyled content) + hydration mismatch.
|
||||
- **Streaming SSR**: HTML chunks sent progressively. Styles must inject as components render, not at end.
|
||||
- **App Router**: `useServerInsertedHTML` provides hook into Suspense boundary stream.
|
||||
|
||||
### 매 Mechanism
|
||||
1. Server: collect styles into sheet during render (per request).
|
||||
2. Server: insert `<style>` tags into HTML stream via `useServerInsertedHTML`.
|
||||
3. Client: hydrate — runtime takes over, no re-render needed.
|
||||
4. Concurrency: per-request sheet (no global state pollution).
|
||||
|
||||
### 매 응용
|
||||
1. styled-components in Next.js App Router.
|
||||
2. Emotion in Next.js App Router.
|
||||
3. @mui v5+ in Next.js.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### styled-components Registry
|
||||
```typescript
|
||||
// app/lib/registry.tsx
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
|
||||
|
||||
export default function StyledComponentsRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||
styledComponentsStyleSheet.instance.clearTag();
|
||||
return <>{styles}</>;
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') return <>{children}</>;
|
||||
|
||||
return (
|
||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||
{children}
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Apply in root layout
|
||||
```typescript
|
||||
// app/layout.tsx
|
||||
import StyledComponentsRegistry from './lib/registry';
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Emotion Registry
|
||||
```typescript
|
||||
// app/lib/emotion-registry.tsx
|
||||
'use client';
|
||||
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function EmotionRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [{ cache, flush }] = useState(() => {
|
||||
const cache = createCache({ key: 'css' });
|
||||
cache.compat = true;
|
||||
const prevInsert = cache.insert;
|
||||
let inserted: string[] = [];
|
||||
cache.insert = (...args) => {
|
||||
const serialized = args[1];
|
||||
if (cache.inserted[serialized.name] === undefined) {
|
||||
inserted.push(serialized.name);
|
||||
}
|
||||
return prevInsert(...args);
|
||||
};
|
||||
const flush = () => {
|
||||
const prev = inserted;
|
||||
inserted = [];
|
||||
return prev;
|
||||
};
|
||||
return { cache, flush };
|
||||
});
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const names = flush();
|
||||
if (names.length === 0) return null;
|
||||
let styles = '';
|
||||
for (const name of names) {
|
||||
styles += cache.inserted[name];
|
||||
}
|
||||
return (
|
||||
<style
|
||||
data-emotion={`${cache.key} ${names.join(' ')}`}
|
||||
dangerouslySetInnerHTML={{ __html: styles }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return <CacheProvider value={cache}>{children}</CacheProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
### @mui Registry
|
||||
```typescript
|
||||
// app/lib/mui-registry.tsx
|
||||
'use client';
|
||||
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { theme } from './theme';
|
||||
|
||||
export default function MuiRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pages Router (legacy) — _document.tsx
|
||||
```typescript
|
||||
// pages/_document.tsx — Pages Router uses different mechanism
|
||||
import Document, { DocumentContext } from 'next/document';
|
||||
import { ServerStyleSheet } from 'styled-components';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: App => props =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
...initialProps,
|
||||
styles: [initialProps.styles, sheet.getStyleElement()],
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Verifying SSR works
|
||||
```typescript
|
||||
// 1. View source (Cmd+U) — should see <style> tags with rules
|
||||
// 2. Disable JS in DevTools — page should still be styled
|
||||
// 3. Check Network tab — no FOUC during page transition
|
||||
// 4. React DevTools — no hydration mismatch warnings
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Greenfield 2026 Next.js | Tailwind / Vanilla Extract — no registry needed |
|
||||
| Existing styled-components migration | Add StyledComponentsRegistry |
|
||||
| Emotion-based codebase | EmotionRegistry pattern |
|
||||
| @mui v5+ | AppRouterCacheProvider (built-in) |
|
||||
| Pages Router legacy | _document.tsx + sheet collection |
|
||||
|
||||
**기본값**: greenfield → zero-runtime CSS (no registry). Existing CSS-in-JS → use library-recommended registry pattern.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS in JS]] · [[SSR]]
|
||||
- 변형: [[useServerInsertedHTML]]
|
||||
- 응용: [[Styled Components v6]]
|
||||
- Adjacent: [[Streaming SSR]] · [[React Server Components]] · [[Hydration]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: SSR setup for CSS-in-JS, Next.js App Router migration from Pages, debugging FOUC / hydration mismatch.
|
||||
**언제 X**: Tailwind / CSS Modules / Vanilla Extract — these are zero-runtime, no registry needed.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No registry → FOUC**: styled-components SSR without registry shows unstyled HTML on first paint.
|
||||
- **Global sheet (not per-request)**: cross-request style pollution / memory leak.
|
||||
- **`'use client'` on registry but rendered at top level**: marks entire tree as client — kills RSC benefit. Wrap deeply.
|
||||
- **Forgetting `clearTag()`**: duplicate styles inserted on each chunk.
|
||||
- **Mixing registries**: emotion + styled-components → two style systems, double bundle, conflicts.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Next.js docs `app/building-your-application/styling`, styled-components Next.js example, Emotion + Next.js guide).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full canonical (registry mechanism + SC/Emotion/MUI patterns + Pages Router fallback) |
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
---
|
||||
id: wiki-2026-0508-styled-components-v6
|
||||
title: Styled Components v6
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [styled-components, sc-v6, CSS-in-JS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, css-in-js, react, styled-components]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react
|
||||
---
|
||||
|
||||
# Styled Components v6
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 React-runtime CSS-in-JS 의 v6 modernization"**. 2023 release — stylis v4 (5x faster), `transient props` ($-prefix) 의 default, native `transient`, drop legacy babel-plugin in favor of SWC plugin. 2026 현재 RSC-incompatible 의 큰 단점 — Next.js App Router 사용 시 zero-runtime alternatives (Linaria/Vanilla Extract/Panda) 의 dominant.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 v6 변경
|
||||
- **stylis v4**: parser rewrite, 5x faster, smaller bundle.
|
||||
- **`as` polymorphic prop**: typed properly with TS generics.
|
||||
- **`$prop` transient**: forwarded to component but not DOM attr (was opt-in, now norm).
|
||||
- **No babel-plugin needed**: SWC/Vite plugin 의 standard.
|
||||
- **Drop**: `.extend`, primary-theme prop forwarding, IE11.
|
||||
|
||||
### 매 RSC limitation
|
||||
- styled-components 의 React Server Components 와 incompatible — runtime stylesheet injection requires client.
|
||||
- Next.js App Router 의 `'use client'` boundary required everywhere using styled.
|
||||
- Mitigation: `StyleRegistry` + `useServerInsertedHTML` for SSR streaming.
|
||||
|
||||
### 매 응용
|
||||
1. Design system component library (theming via ThemeProvider).
|
||||
2. Conditional styling via props (variant, size).
|
||||
3. Animation via `keyframes` helper.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic styled component
|
||||
```typescript
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Button = styled.button<{ $primary?: boolean; $size?: 'sm' | 'md' | 'lg' }>`
|
||||
padding: ${({ $size }) => ({ sm: '4px 8px', md: '8px 16px', lg: '12px 24px' }[$size ?? 'md'])};
|
||||
background: ${({ $primary, theme }) => $primary ? theme.colors.primary : 'transparent'};
|
||||
color: ${({ $primary, theme }) => $primary ? '#fff' : theme.colors.text};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
&:hover { opacity: 0.9; }
|
||||
`;
|
||||
|
||||
<Button $primary $size="lg">Click</Button>
|
||||
```
|
||||
|
||||
### Theme + TypeScript
|
||||
```typescript
|
||||
// styled.d.ts
|
||||
import 'styled-components';
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme {
|
||||
colors: { primary: string; text: string; border: string; bg: string };
|
||||
space: (n: number) => string;
|
||||
}
|
||||
}
|
||||
|
||||
// theme.ts
|
||||
export const theme: DefaultTheme = {
|
||||
colors: { primary: '#0066cc', text: '#222', border: '#ddd', bg: '#fff' },
|
||||
space: n => `${n * 4}px`,
|
||||
};
|
||||
|
||||
<ThemeProvider theme={theme}><App /></ThemeProvider>
|
||||
```
|
||||
|
||||
### Polymorphic `as`
|
||||
```typescript
|
||||
const Box = styled.div<{ $padded?: boolean }>`
|
||||
padding: ${({ $padded }) => $padded ? '16px' : 0};
|
||||
`;
|
||||
|
||||
// Render as <a> with proper typing
|
||||
<Box as="a" href="/x" $padded>Link</Box>
|
||||
|
||||
// Component composition
|
||||
const Card = styled(Box)`
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
```
|
||||
|
||||
### Keyframes + animation
|
||||
```typescript
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const spin = keyframes`
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
`;
|
||||
|
||||
const Spinner = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #ddd;
|
||||
border-top-color: #0066cc;
|
||||
border-radius: 50%;
|
||||
animation: ${spin} 1s linear infinite;
|
||||
`;
|
||||
```
|
||||
|
||||
### Next.js App Router SSR setup
|
||||
```typescript
|
||||
// app/registry.tsx
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
|
||||
|
||||
export function StyledRegistry({ children }: { children: React.ReactNode }) {
|
||||
const [sheet] = useState(() => new ServerStyleSheet());
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = sheet.getStyleElement();
|
||||
sheet.instance.clearTag();
|
||||
return <>{styles}</>;
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') return <>{children}</>;
|
||||
return <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>;
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html><body>
|
||||
<StyledRegistry>{children}</StyledRegistry>
|
||||
</body></html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Global styles + reset
|
||||
```typescript
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ${({ theme }) => theme.fonts.body};
|
||||
background: ${({ theme }) => theme.colors.bg};
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
### Variants via css helper
|
||||
```typescript
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const variants = {
|
||||
primary: css`background: #0066cc; color: #fff;`,
|
||||
ghost: css`background: transparent; border: 1px solid #0066cc; color: #0066cc;`,
|
||||
danger: css`background: #cc0000; color: #fff;`,
|
||||
};
|
||||
|
||||
const Button = styled.button<{ $variant: keyof typeof variants }>`
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
${({ $variant }) => variants[$variant]}
|
||||
`;
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Existing styled-components codebase | stay on v6 |
|
||||
| Next.js App Router (greenfield) | Vanilla Extract / Panda CSS / Tailwind |
|
||||
| Component library (npm package) | v6 fine OR Stitches/VE (zero-runtime) |
|
||||
| Performance-critical (LCP) | zero-runtime CSS (Linaria/VE) |
|
||||
| Need RSC | NOT styled-components |
|
||||
|
||||
**기본값**: 2026 greenfield React → Tailwind or Vanilla Extract. Existing styled-components codebase → upgrade to v6, plan migration if RSC needed.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS in JS]]
|
||||
- 변형: [[Vanilla Extract]]
|
||||
- 응용: [[Style Registry]] · [[Design Tokens]] · [[Theming]]
|
||||
- Adjacent: [[CSS_Architecture_and_Styling|Tailwind CSS]] · [[Panda CSS]] · [[Modern_Web_Rendering_and_Optimization|Server Components]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: existing SC codebase, design system library with runtime theming, Pages Router Next.js.
|
||||
**언제 X**: RSC-heavy app, performance-critical (LCP), greenfield 2026 — recommend zero-runtime alternative.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Non-transient props leaking to DOM**: `<Button primary>` → `<button primary>` warning. Use `$primary`.
|
||||
- **styled() inside render body**: new component each render → tree thrash + cache miss.
|
||||
- **No ThemeProvider but using theme**: undefined theme errors.
|
||||
- **Forgetting StyleRegistry on SSR**: FOUC + hydration mismatch.
|
||||
- **Mixing styled-components and Tailwind**: specificity hell.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (styled-components v6 changelog, official docs, Next.js styling guide 2025).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full canonical (v6 changes + RSC limit + SSR registry + patterns) |
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
---
|
||||
id: wiki-2026-0508-threejs-webgpu-파티클-예제
|
||||
title: Threejs WebGPU 파티클 예제
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Three.js WebGPU Particles, TSL Particles]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [threejs, webgpu, particles, tsl, gpgpu]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: three.js
|
||||
---
|
||||
|
||||
# Threejs WebGPU 파티클 예제
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 GPU compute shader 의 millions-of-particles 의 60fps"**. Three.js r170+ 의 WebGPURenderer 의 TSL (Three Shader Language) 의 compute node 의 particle position/velocity 의 GPU buffer 의 simulate. 매 2026 standard: WebGPU 의 baseline browser support (Chrome/Edge/Safari 17.4+/Firefox 127+) 의 production 의 reach.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 architecture
|
||||
- **Storage buffer**: 매 particle position/velocity 의 GPU memory 의 persist — 매 CPU readback 의 X.
|
||||
- **Compute pass**: 매 frame 의 start 의 simulation step 의 dispatch.
|
||||
- **Render pass**: 매 same buffer 의 vertex attribute 의 read — instanced point / mesh.
|
||||
- **TSL**: 매 JS-authored shader graph 의 WGSL 의 compile — 매 backend portability (WebGPU + WebGL fallback).
|
||||
|
||||
### 매 핵심 node
|
||||
- `storage()`: 매 mutable GPU buffer.
|
||||
- `Fn()`: 매 reusable shader function.
|
||||
- `instanceIndex`: 매 compute thread id.
|
||||
- `attribute()`: 매 vertex attribute read.
|
||||
- `uniform()`: 매 per-frame CPU-set value.
|
||||
|
||||
### 매 응용
|
||||
1. Galaxy / nebula simulation 의 web demo.
|
||||
2. GPGPU fluid (SPH, FLIP) 의 art piece.
|
||||
3. Real-time crowd / flock (boid) 의 100k+ agent.
|
||||
4. Data viz 의 millions-of-points scatter.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Setup WebGPURenderer
|
||||
```javascript
|
||||
import * as THREE from 'three/webgpu';
|
||||
import { Fn, storage, instanceIndex, uniform, vec3, sin, cos, time } from 'three/tsl';
|
||||
|
||||
const renderer = new THREE.WebGPURenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
await renderer.init();
|
||||
document.body.appendChild(renderer.domElement);
|
||||
```
|
||||
|
||||
### Allocate particle buffers
|
||||
```javascript
|
||||
const COUNT = 500_000;
|
||||
const positionBuffer = storage(new THREE.StorageInstancedBufferAttribute(COUNT, 3), 'vec3', COUNT);
|
||||
const velocityBuffer = storage(new THREE.StorageInstancedBufferAttribute(COUNT, 3), 'vec3', COUNT);
|
||||
```
|
||||
|
||||
### Init compute (one-time)
|
||||
```javascript
|
||||
const initCompute = Fn(() => {
|
||||
const i = instanceIndex;
|
||||
const angle = i.toFloat().mul(0.001);
|
||||
positionBuffer.element(i).assign(vec3(cos(angle).mul(50), 0, sin(angle).mul(50)));
|
||||
velocityBuffer.element(i).assign(vec3(0));
|
||||
})().compute(COUNT);
|
||||
|
||||
await renderer.computeAsync(initCompute);
|
||||
```
|
||||
|
||||
### Per-frame simulation
|
||||
```javascript
|
||||
const dt = uniform(0.016);
|
||||
const simCompute = Fn(() => {
|
||||
const i = instanceIndex;
|
||||
const pos = positionBuffer.element(i);
|
||||
const vel = velocityBuffer.element(i);
|
||||
const gravity = pos.normalize().negate().mul(9.8);
|
||||
vel.addAssign(gravity.mul(dt));
|
||||
pos.addAssign(vel.mul(dt));
|
||||
})().compute(COUNT);
|
||||
```
|
||||
|
||||
### Render as instanced points
|
||||
```javascript
|
||||
const material = new THREE.SpriteNodeMaterial();
|
||||
material.positionNode = positionBuffer.toAttribute();
|
||||
material.colorNode = velocityBuffer.toAttribute().length().mul(0.1);
|
||||
|
||||
const mesh = new THREE.InstancedMesh(new THREE.PlaneGeometry(0.05), material, COUNT);
|
||||
scene.add(mesh);
|
||||
```
|
||||
|
||||
### Animation loop
|
||||
```javascript
|
||||
renderer.setAnimationLoop(async () => {
|
||||
dt.value = clock.getDelta();
|
||||
await renderer.computeAsync(simCompute);
|
||||
await renderer.renderAsync(scene, camera);
|
||||
});
|
||||
```
|
||||
|
||||
### Curl noise flow field
|
||||
```javascript
|
||||
import { mx_noise_vec3 } from 'three/tsl';
|
||||
|
||||
const flowCompute = Fn(() => {
|
||||
const i = instanceIndex;
|
||||
const pos = positionBuffer.element(i);
|
||||
const noise = mx_noise_vec3(pos.mul(0.1).add(time.mul(0.5)));
|
||||
velocityBuffer.element(i).assign(noise.mul(2));
|
||||
pos.addAssign(velocityBuffer.element(i).mul(dt));
|
||||
})().compute(COUNT);
|
||||
```
|
||||
|
||||
### WebGL fallback
|
||||
```javascript
|
||||
const renderer = WebGPU.isAvailable()
|
||||
? new THREE.WebGPURenderer()
|
||||
: new THREE.WebGLRenderer();
|
||||
// 매 TSL 의 same code 의 WebGL backend 의 transpile (compute X 의 limit 의 case 의 GPGPUComputationRenderer 의 fallback)
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| <10k particle, simple motion | CPU 의 BufferGeometry update |
|
||||
| 10k–100k, WebGL2 only | GPGPUComputationRenderer (ping-pong texture) |
|
||||
| 100k–10M, modern browser | WebGPURenderer + TSL compute |
|
||||
| Physics-accurate fluid | WebGPU compute + custom WGSL |
|
||||
| Cross-browser 의 require, IE/legacy | Canvas2D 또는 WebGL1 fallback |
|
||||
|
||||
**기본값**: 매 2026 의 new project 의 WebGPURenderer + TSL — 매 WebGL fallback 의 automatic.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Three.js]] · [[WebGPU]]
|
||||
- 변형: [[GPGPU]] · [[TSL Three Shader Language]]
|
||||
- 응용: [[Particle System]]
|
||||
- Adjacent: [[Compute Shader]] · [[Instanced Rendering]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 100k+ particle 의 60fps 의 require, 매 modern browser 의 target.
|
||||
**언제 X**: 매 static scene, low count, 또는 mobile-Safari-pre-17.4 의 fallback 의 critical.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **CPU position update**: 매 frame 의 millions-of-vertex 의 GPU upload 의 PCIe bottleneck.
|
||||
- **Compute pass 의 매 frame 의 buffer recreate**: 매 GC pressure 의 stutter — 매 reuse.
|
||||
- **`renderer.compute()` sync wait**: 매 main thread 의 block — 매 `computeAsync` 의 use.
|
||||
- **Float32 over-precision**: 매 WebGPU 의 f16 storage 의 bandwidth halve 의 opportunity 의 miss.
|
||||
- **TSL 의 raw WGSL 의 mix 의 unnecessary**: 매 portability 의 break.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Three.js r170+ docs, threejs.org/examples webgpu_compute_particles).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — TSL compute, instanced render, WebGL fallback |
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
id: wiki-2026-0508-timing-attack
|
||||
title: Timing Attack
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Timing Side Channel, Constant Time Comparison]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [security, side-channel, cryptography, constant-time]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: nodejs
|
||||
---
|
||||
|
||||
# Timing Attack
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 execution time 의 secret-dependent variation 의 leak"**. 1996 Kocher RSA timing paper origin — comparison/branch/cache 가 secret bit 에 dependent 면 attacker 가 multiple measurement 의 statistical analysis 로 secret 을 추출. Mitigation 의 핵심: constant-time code.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Vulnerable patterns
|
||||
- **Early-exit string compare**: `==` / `strcmp` returns on first mismatch byte.
|
||||
- **Secret-dependent branch**: `if (key[i] == 0)` 의 cache miss timing differs.
|
||||
- **Secret-dependent table lookup**: AES S-box → cache line timing.
|
||||
- **Modular exponentiation**: square-and-multiply 의 bit-dependent ops.
|
||||
|
||||
### 매 Mitigation
|
||||
- **Constant-time compare**: scan all bytes regardless, XOR-accumulate.
|
||||
- **No secret-dependent branches**: use bitwise mask instead.
|
||||
- **No secret-dependent indices**: scan full table or use bit-slicing.
|
||||
- **Blinding**: randomize input (RSA: blind w/ random r, decrypt, unblind).
|
||||
|
||||
### 매 응용
|
||||
1. HMAC token comparison (auth bypass via timing).
|
||||
2. Password hash compare (after bcrypt — still need constant-time).
|
||||
3. JWT signature verify.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Vulnerable: early-exit compare
|
||||
```typescript
|
||||
// ❌ DON'T — leaks prefix length
|
||||
function badCompare(a: string, b: string): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false; // early exit reveals match length
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Attacker measures: "aaaa" vs "baaa" faster than "aaaa" vs "aaab"
|
||||
```
|
||||
|
||||
### Constant-time compare (Node.js)
|
||||
```typescript
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
|
||||
function safeCompare(a: string, b: string): boolean {
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
if (bufA.length !== bufB.length) {
|
||||
// length leak unavoidable — pad to fixed length OR accept
|
||||
timingSafeEqual(bufA, bufA); // dummy compare to equalize timing
|
||||
return false;
|
||||
}
|
||||
return timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
|
||||
// HMAC token check
|
||||
import { createHmac } from 'node:crypto';
|
||||
function verifyToken(token: string, payload: string, secret: string): boolean {
|
||||
const expected = createHmac('sha256', secret).update(payload).digest('hex');
|
||||
return expected.length === token.length &&
|
||||
timingSafeEqual(Buffer.from(expected), Buffer.from(token));
|
||||
}
|
||||
```
|
||||
|
||||
### Browser: SubtleCrypto + manual constant-time
|
||||
```typescript
|
||||
// Web Crypto has no timingSafeEqual — implement carefully
|
||||
function ctEqualBytes(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
diff |= a[i] ^ b[i]; // XOR-accumulate, no branch
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
async function verifyHmac(msg: string, sig: ArrayBuffer, key: CryptoKey) {
|
||||
const computed = await crypto.subtle.sign(
|
||||
'HMAC', key, new TextEncoder().encode(msg)
|
||||
);
|
||||
return ctEqualBytes(new Uint8Array(computed), new Uint8Array(sig));
|
||||
}
|
||||
```
|
||||
|
||||
### Python constant-time compare
|
||||
```python
|
||||
import hmac
|
||||
|
||||
def verify(token: str, expected: str) -> bool:
|
||||
return hmac.compare_digest(token, expected)
|
||||
|
||||
# bcrypt — already constant-time via library
|
||||
import bcrypt
|
||||
def check_password(plain: bytes, hashed: bytes) -> bool:
|
||||
return bcrypt.checkpw(plain, hashed) # safe internally
|
||||
```
|
||||
|
||||
### Go constant-time
|
||||
```go
|
||||
import "crypto/subtle"
|
||||
|
||||
func verify(a, b []byte) bool {
|
||||
return subtle.ConstantTimeCompare(a, b) == 1
|
||||
}
|
||||
|
||||
// Conditional copy without branch
|
||||
func ctSelect(cond int, a, b []byte) {
|
||||
subtle.ConstantTimeCopy(cond, a, b)
|
||||
}
|
||||
```
|
||||
|
||||
### Branchless conditional (C-style)
|
||||
```typescript
|
||||
// Constant-time conditional select
|
||||
function ctSelect(cond: number, a: number, b: number): number {
|
||||
// cond must be 0 or 1
|
||||
const mask = -cond; // 0 or 0xFFFFFFFF
|
||||
return (a & mask) | (b & ~mask);
|
||||
}
|
||||
|
||||
// Constant-time min/max without branch
|
||||
function ctMin(a: number, b: number): number {
|
||||
const lt = (a - b) >>> 31; // 1 if a<b
|
||||
return ctSelect(lt, a, b);
|
||||
}
|
||||
```
|
||||
|
||||
### Remote timing (network attacks)
|
||||
```text
|
||||
Lucky 13 (TLS 2013): MAC verify timing leaked plaintext.
|
||||
CRIME / BREACH: compression length leaked secrets (different side channel).
|
||||
|
||||
Mitigation:
|
||||
- Always run full computation regardless of input validity.
|
||||
- Add randomized delay (debatable — may not help, can hurt).
|
||||
- Rate-limit + monitoring for anomalous timing-probe traffic.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Password hash check | bcrypt/argon2 lib (already CT) |
|
||||
| HMAC/token compare | timingSafeEqual / hmac.compare_digest |
|
||||
| AES on untrusted host | AES-NI (HW) or bit-sliced soft impl |
|
||||
| Secret-dependent index | bitwise mask or full-table scan |
|
||||
| RSA/ECDSA private op | use vetted lib (BoringSSL, libsodium) — never roll your own |
|
||||
|
||||
**기본값**: never write your own crypto compare. Use language stdlib `timingSafeEqual` / `compare_digest` / `subtle.ConstantTimeCompare`.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Side Channel Attack]] · [[Practical-Cryptography|Cryptography]]
|
||||
- 변형: [[Spectre]] · [[Cache Timing Attack]] · [[Power Analysis]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: auth code review, HMAC/token compare, custom crypto routines, security audit.
|
||||
**언제 X**: high-level app logic where no secrets are compared (UI rendering, business rules).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`a === b` for secrets**: V8/JIT may early-exit, branch, or short-circuit.
|
||||
- **Custom "constant-time" without testing**: compiler can re-introduce branches via optimization. Test with `dudect`.
|
||||
- **Throwing on mismatch**: exception path differs in timing from success path.
|
||||
- **Logging mismatch position**: leaks comparison index.
|
||||
- **Comparing hashes in DB**: even hash compare 의 leaks length prefix → use CT.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Kocher 1996 paper, Node.js crypto docs, Go subtle pkg, OWASP Cryptographic Storage CS).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — full canonical (CT compare + branchless + Node/Go/Python + remote attacks) |
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
---
|
||||
id: wiki-2026-0508-v8-javascript-engine
|
||||
title: V8 JavaScript Engine
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [V8, V8 Engine, Chrome V8]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [v8, javascript, engine, runtime, chrome, node]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: C++
|
||||
framework: V8
|
||||
---
|
||||
|
||||
# V8 JavaScript Engine
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 Ignition interpreter + TurboFan / Maglev / Sparkplug 의 매 multi-tier JIT 으로 매 JS 를 native 수준에 도달시키는 엔진"**. 매 Chrome / Edge / Node.js / Deno 의 핵심. 매 2026 기준 Maglev (mid-tier JIT) 와 매 pointer compression 으로 매 startup + memory 모두 개선됨.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Multi-tier compilation
|
||||
- **Ignition** — bytecode interpreter (fastest startup).
|
||||
- **Sparkplug** — non-optimizing baseline JIT (V8 9.1+).
|
||||
- **Maglev** — mid-tier optimizing JIT (V8 11.5+, default 11.9+).
|
||||
- **TurboFan** — top-tier optimizer (slow compile, fast run).
|
||||
|
||||
### 매 Hidden classes & inline caches
|
||||
- 매 JS object 의 매 shape 추적 (V8 internal "Map").
|
||||
- 매 same-shape access 는 매 monomorphic IC 로 매 native 속도.
|
||||
- 매 shape 변경 (속성 추가 순서 다름) 매 → polymorphic / megamorphic.
|
||||
|
||||
### 매 GC
|
||||
- Generational: young (scavenger, Minor MC) + old (Mark-Compact, Mark-Sweep).
|
||||
- Orinoco — concurrent / parallel / incremental.
|
||||
- Pointer compression (V8 8.0+) — 35-40% heap 절감.
|
||||
|
||||
### 매 응용
|
||||
1. Browser JS — Chrome, Edge.
|
||||
2. Server JS — Node.js, Deno (V8 기반).
|
||||
3. Embedded — Electron, CEF.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Hidden class 친화적 객체
|
||||
```javascript
|
||||
// Good — same shape
|
||||
function User(id, name) { this.id = id; this.name = name; }
|
||||
|
||||
// Bad — shape diverges
|
||||
function User(id, name) { this.id = id; if (name) this.name = name; }
|
||||
```
|
||||
|
||||
### Monomorphic call site
|
||||
```javascript
|
||||
function getX(point) { return point.x; }
|
||||
// pass only one shape consistently → IC stays monomorphic
|
||||
const points = Array.from({ length: 1e6 }, (_, i) => ({ x: i, y: i }));
|
||||
points.forEach(getX);
|
||||
```
|
||||
|
||||
### Avoiding deopt
|
||||
```javascript
|
||||
function add(a, b) { return a + b; }
|
||||
add(1, 2); // optimized for number
|
||||
add('a', 'b'); // deopt — TurboFan must reoptimize
|
||||
```
|
||||
|
||||
### V8 inspector / profiling
|
||||
```bash
|
||||
node --inspect-brk app.js
|
||||
# Chrome devtools → Performance → JS samples
|
||||
```
|
||||
|
||||
### V8 flags for diagnosis
|
||||
```bash
|
||||
node --trace-opt --trace-deopt app.js
|
||||
node --print-bytecode app.js
|
||||
node --allow-natives-syntax # %OptimizeFunctionOnNextCall
|
||||
```
|
||||
|
||||
### Pointer compression check
|
||||
```bash
|
||||
node -p "process.config.variables.v8_enable_pointer_compression"
|
||||
# 1 → enabled
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 관심사 | Approach |
|
||||
|---|---|
|
||||
| Startup 빠름 | Ignition / Sparkplug 충분 |
|
||||
| Hot loop | Maglev / TurboFan 의 monomorphic 유지 |
|
||||
| Memory tight | pointer compression + young GC tuning |
|
||||
| Diagnose perf | `--trace-deopt`, `clinic flame` |
|
||||
|
||||
**기본값**: 매 modern V8 default — 매 micro-opt 보다 매 algorithmic 우선.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[JavaScript]]
|
||||
- 변형: [[JavaScriptCore]] · [[Hermes]]
|
||||
- 응용: [[Node.js]] · [[Deno]] · [[Chrome]] · [[Electron]]
|
||||
- Adjacent: [[JIT Compilation]] · [[Garbage Collection]] · [[Inline Cache]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 perf 분석 가설 세우기, 매 deopt 원인 추정.
|
||||
**언제 X**: 매 V8 internal 변경 빠름 — 매 매 latest blog 확인 필요.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Shape divergence**: 매 conditional property 추가 — 매 polymorphic 유발.
|
||||
- **delete 연산자**: 매 hidden class transition 강제.
|
||||
- **`arguments` 변형**: 매 deopt 유발.
|
||||
- **Try/catch in hot path**: 매 V8 ≤ 7.x 까진 deopt — 매 modern V8 은 ok 이나 매 case-by-case.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (V8 official blog v8.dev, "Maglev" announcement).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Maglev + pointer compression 반영 |
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
id: wiki-2026-0508-webgl-20
|
||||
title: WebGL 2.0
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [WebGL 2.0, WebGL2, GLES3 Web]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [webgl, graphics, frontend, gpu, opengl-es]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript / GLSL ES 3.00
|
||||
framework: WebGL 2.0
|
||||
---
|
||||
|
||||
# WebGL 2.0
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 OpenGL ES 3.0 기반 매 browser 그래픽 API"**. 매 WebGL 1.0 (ES 2.0) 대비 매 transform feedback, 매 UBO, 매 instancing, 매 3D / array textures, 매 GLSL ES 3.00 등 매 modern feature 표준화. 매 2026 기준 매 모든 주요 browser default 지원, 매 WebGPU 으로 매 점진 이행 중이지만 매 fallback baseline.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 ES 2.0 → ES 3.0 차이
|
||||
- **GLSL ES 3.00** — `in`/`out`, layout qualifier, integer texture.
|
||||
- **VAO** — Vertex Array Object 표준 (1.0 은 extension).
|
||||
- **UBO** — Uniform Buffer Object (binding point 기반 sharing).
|
||||
- **Instancing** — `drawArraysInstanced` core (1.0 은 ANGLE_instanced_arrays).
|
||||
- **Transform feedback** — vertex shader 결과 buffer 로 capture.
|
||||
- **MRT** — Multiple Render Targets.
|
||||
- **3D / 2D array textures**, sampler objects.
|
||||
|
||||
### 매 응용
|
||||
1. 매 GPGPU compute (transform feedback).
|
||||
2. 매 Deferred shading (MRT).
|
||||
3. 매 Volumetric rendering (3D texture).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Context 획득
|
||||
```javascript
|
||||
const gl = canvas.getContext('webgl2');
|
||||
if (!gl) throw new Error('WebGL2 unsupported');
|
||||
```
|
||||
|
||||
### VAO + instancing
|
||||
```javascript
|
||||
const vao = gl.createVertexArray();
|
||||
gl.bindVertexArray(vao);
|
||||
// setup attribs...
|
||||
gl.vertexAttribDivisor(instanceAttribLoc, 1); // 1 per instance
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
||||
```
|
||||
|
||||
### UBO
|
||||
```glsl
|
||||
#version 300 es
|
||||
layout(std140) uniform Camera {
|
||||
mat4 view;
|
||||
mat4 proj;
|
||||
};
|
||||
```
|
||||
```javascript
|
||||
const ubo = gl.createBuffer();
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, 128, gl.DYNAMIC_DRAW);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);
|
||||
const idx = gl.getUniformBlockIndex(program, 'Camera');
|
||||
gl.uniformBlockBinding(program, idx, 0);
|
||||
```
|
||||
|
||||
### Transform feedback
|
||||
```javascript
|
||||
gl.transformFeedbackVaryings(program, ['v_position'], gl.SEPARATE_ATTRIBS);
|
||||
gl.linkProgram(program);
|
||||
|
||||
const tfBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, tfBuf);
|
||||
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, byteLen, gl.DYNAMIC_COPY);
|
||||
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, tfBuf);
|
||||
|
||||
gl.enable(gl.RASTERIZER_DISCARD);
|
||||
gl.beginTransformFeedback(gl.POINTS);
|
||||
gl.drawArrays(gl.POINTS, 0, count);
|
||||
gl.endTransformFeedback();
|
||||
gl.disable(gl.RASTERIZER_DISCARD);
|
||||
```
|
||||
|
||||
### MRT
|
||||
```javascript
|
||||
gl.drawBuffers([
|
||||
gl.COLOR_ATTACHMENT0, // diffuse
|
||||
gl.COLOR_ATTACHMENT1, // normal
|
||||
gl.COLOR_ATTACHMENT2, // depth/spec
|
||||
]);
|
||||
```
|
||||
|
||||
### 3D texture
|
||||
```javascript
|
||||
gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, w, h, d, 0, gl.RED, gl.UNSIGNED_BYTE, data);
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | API |
|
||||
|---|---|
|
||||
| 광범위 호환 / 단순 | WebGL 1 + 매 fallback |
|
||||
| Modern feature 필요 | WebGL 2 |
|
||||
| Compute / 매 advanced | WebGPU |
|
||||
| Library 사용 | Three.js / Babylon.js (auto-detect) |
|
||||
|
||||
**기본값**: 매 2026 신규 프로젝트 — WebGPU 가능하면 WebGPU, 아니면 WebGL 2 + WEBGL_multi_draw.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[WebGL]] · [[OpenGL ES]]
|
||||
- 변형: [[WebGPU]]
|
||||
- 응용: [[Three.js]] · [[Babylon.js]] · [[CesiumJS]]
|
||||
- Adjacent: [[WEBGL_multi_draw]] · [[GLSL]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 boilerplate (VAO/UBO setup), 매 GLSL 변환 (ES 2 → ES 3).
|
||||
**언제 X**: 매 driver-specific bug — 매 매 platform test 필요.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`#version 300 es` 누락**: 매 WebGL2 shader 인데 매 ES 2 syntax.
|
||||
- **VAO 미사용**: 매 매 draw 마다 attrib 재바인딩 — 매 perf 손실.
|
||||
- **UBO 정렬 무시**: 매 std140 padding 안 맞춰 매 silent 손상.
|
||||
- **Loss of context 무시**: 매 GPU reset / 매 tab background — 매 `webglcontextlost` 핸들링 필수.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Khronos WebGL 2.0 spec, MDN WebGL2RenderingContext).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — VAO/UBO/TF/MRT 패턴 + WebGPU 비교 |
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
id: wiki-2026-0508-webgl-api
|
||||
title: WebGL API
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [WebGL, WebGL API, WebGL 1.0, WebGL 2.0]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [webgl, graphics, gpu, gles]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/GLSL
|
||||
framework: Web Platform
|
||||
---
|
||||
|
||||
# WebGL API
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 OpenGL ES 의 web — canvas 의 GPU draw"**. WebGL 매 OpenGL ES 2.0 (WebGL1) / ES 3.0 (WebGL2) binding 의 browser. 2026 매 WebGPU rising 매 WebGL 매 still ubiquitous — fallback / wide compatibility 의 사용.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 pipeline
|
||||
- **Vertex shader**: per-vertex transform — clip space.
|
||||
- **Rasterizer**: triangles → fragments.
|
||||
- **Fragment shader**: per-pixel color.
|
||||
- **Framebuffer**: render target — default canvas / FBO offscreen.
|
||||
|
||||
### 매 핵심 objects
|
||||
- **Buffer** (`ARRAY_BUFFER`, `ELEMENT_ARRAY_BUFFER`): vertex / index data.
|
||||
- **Texture** (2D / cube / 3D-WebGL2 / array-WebGL2).
|
||||
- **Program**: vertex + fragment shader linked.
|
||||
- **VAO** (WebGL2): vertex attribute state.
|
||||
- **UBO** (WebGL2): uniform block — efficient bulk uniform.
|
||||
- **Framebuffer / Renderbuffer**: offscreen render targets.
|
||||
|
||||
### 매 응용
|
||||
1. **3D libraries**: Three.js / Babylon.js / PlayCanvas.
|
||||
2. **2D accelerated**: PixiJS / Konva.
|
||||
3. **Data viz**: deck.gl / Mapbox GL / regl.
|
||||
4. **Games / interactive**: itch.io HTML5 / Unity WebGL build.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 매 minimal triangle
|
||||
```js
|
||||
const canvas = document.querySelector('canvas');
|
||||
const gl = canvas.getContext('webgl2');
|
||||
|
||||
const vs = `#version 300 es
|
||||
in vec2 a_pos;
|
||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }`;
|
||||
|
||||
const fs = `#version 300 es
|
||||
precision highp float;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0, 0.5, 0.2, 1.0); }`;
|
||||
|
||||
function compile(type, src) {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, src); gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(s);
|
||||
return s;
|
||||
}
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
|
||||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
|
||||
gl.linkProgram(prog);
|
||||
|
||||
const vao = gl.createVertexArray();
|
||||
gl.bindVertexArray(vao);
|
||||
const vbo = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5]), gl.STATIC_DRAW);
|
||||
const loc = gl.getAttribLocation(prog, 'a_pos');
|
||||
gl.enableVertexAttribArray(loc);
|
||||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.useProgram(prog);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
```
|
||||
|
||||
### Texture upload
|
||||
```js
|
||||
const tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
```
|
||||
|
||||
### Instanced rendering (WebGL2)
|
||||
```js
|
||||
gl.vertexAttribDivisor(instanceLoc, 1); // advance per instance
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, 10000); // 10k instances 1 draw call
|
||||
```
|
||||
|
||||
### Uniform Buffer Object (WebGL2)
|
||||
```js
|
||||
const ubo = gl.createBuffer();
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, mat4Buffer, gl.DYNAMIC_DRAW);
|
||||
const idx = gl.getUniformBlockIndex(prog, 'Camera');
|
||||
gl.uniformBlockBinding(prog, idx, 0);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);
|
||||
```
|
||||
|
||||
### Framebuffer (offscreen / render-to-texture)
|
||||
```js
|
||||
const fbo = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
||||
const tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
||||
// 매 render → tex, then post-process
|
||||
```
|
||||
|
||||
### Transform Feedback (WebGL2 GPU compute-ish)
|
||||
```js
|
||||
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, outBuf);
|
||||
gl.beginTransformFeedback(gl.POINTS);
|
||||
gl.drawArrays(gl.POINTS, 0, particleCount);
|
||||
gl.endTransformFeedback();
|
||||
```
|
||||
|
||||
### Context loss handling
|
||||
```js
|
||||
canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); });
|
||||
canvas.addEventListener('webglcontextrestored', () => { rebuildResources(); });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | WebGL vs WebGPU |
|
||||
|---|---|
|
||||
| 2026 wide compatibility | WebGL2 — Safari iOS / older Android |
|
||||
| Compute shader | WebGPU |
|
||||
| Multi-pass complex | WebGPU 매 cleaner state model |
|
||||
| Existing Three.js / Babylon | WebGL2 fallback + WebGPU primary |
|
||||
| Simple 2D accel | WebGL2 / 2D canvas |
|
||||
| AAA-grade graphics | WebGPU |
|
||||
|
||||
**기본값**: Three.js with WebGPURenderer + WebGL2 fallback.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[OpenGL ES]]
|
||||
- 변형: [[WebGPU]]
|
||||
- 응용: [[Threejs]] · [[Babylon.js]]
|
||||
- Adjacent: [[GLSL]] · [[OffscreenCanvas]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: cross-browser 3D / 2D GPU accel / data viz / WebGPU 의 fallback.
|
||||
**언제 X**: heavy compute (no compute shader in WebGL) / cutting-edge graphics — WebGPU 사용.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **State thrash**: bind/unbind every draw — batch by program/texture.
|
||||
- **Sync readback**: `readPixels` 매 GPU stall — use PBO async (WebGL2).
|
||||
- **No VAO**: re-binding attributes every draw — VAO 매 cache.
|
||||
- **Uniform per draw call**: UBO 의 bulk update / instancing.
|
||||
- **Premultiply confusion**: alpha blending 의 incorrect — `UNPACK_PREMULTIPLY_ALPHA_WEBGL`.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Khronos WebGL 1.0/2.0 spec / MDN WebGL API).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — pipeline, VAO, UBO, FBO, instancing, transform feedback |
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: wiki-2026-0508-wonder
|
||||
title: Wonder
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Wonder Editor, Wonder.js, Wonder ECS]
|
||||
duplicate_of: none
|
||||
source_trust_level: B
|
||||
confidence_score: 0.75
|
||||
verification_status: applied
|
||||
tags: [wonder, ecs, web3d, engine]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript / ReScript
|
||||
framework: Wonder
|
||||
---
|
||||
|
||||
# Wonder
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 ECS-first web 3D — functional core"**. Wonder 매 ReScript/TypeScript 기반 ECS architecture 3D engine — Wonderland 와 별도의 OSS lineage. 2026 매 niche — Three.js / Babylon / Wonderland Engine 의 dominant 매 main alternative.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 design
|
||||
- **ECS (Entity Component System)**: data-oriented — entity (ID) + component (data) + system (logic).
|
||||
- **Functional core**: ReScript 의 immutable / pattern matching 의 사용.
|
||||
- **WebGL2 backend**: GPU draw — WebGPU 매 future direction.
|
||||
- **Editor**: web-based scene editor — separate from runtime.
|
||||
|
||||
### 매 vs Three.js
|
||||
- **Three.js**: scene-graph imperative OOP — vast ecosystem.
|
||||
- **Wonder**: ECS data-oriented — 매 cache-friendly bulk update.
|
||||
- **Tradeoff**: Wonder 매 less plugins / smaller community / steeper learning.
|
||||
|
||||
### 매 응용
|
||||
1. **Data-heavy 3D**: thousands of entities — particle / RTS-like.
|
||||
2. **Functional codebase**: ReScript shop 의 fit.
|
||||
3. **Educational**: ECS pattern 의 reference impl.
|
||||
4. **Custom rendering pipeline**: 직접 control 의 필요한 case.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### ECS basic
|
||||
```typescript
|
||||
import { World, defineComponent, defineSystem } from 'wonder-ecs';
|
||||
|
||||
const Position = defineComponent({ x: 'f32', y: 'f32', z: 'f32' });
|
||||
const Velocity = defineComponent({ x: 'f32', y: 'f32', z: 'f32' });
|
||||
|
||||
const world = new World();
|
||||
const entity = world.createEntity();
|
||||
world.addComponent(entity, Position, { x: 0, y: 0, z: 0 });
|
||||
world.addComponent(entity, Velocity, { x: 1, y: 0, z: 0 });
|
||||
|
||||
const movement = defineSystem([Position, Velocity], (entities, dt) => {
|
||||
for (const e of entities) {
|
||||
const p = world.getComponent(e, Position);
|
||||
const v = world.getComponent(e, Velocity);
|
||||
p.x += v.x * dt; p.y += v.y * dt; p.z += v.z * dt;
|
||||
}
|
||||
});
|
||||
|
||||
function tick(dt) { movement(world.query([Position, Velocity]), dt); }
|
||||
```
|
||||
|
||||
### Mesh + render system
|
||||
```typescript
|
||||
const Mesh = defineComponent({ geometryId: 'u32', materialId: 'u32' });
|
||||
|
||||
const renderSystem = defineSystem([Position, Mesh], (entities) => {
|
||||
for (const e of entities) {
|
||||
const p = world.getComponent(e, Position);
|
||||
const m = world.getComponent(e, Mesh);
|
||||
renderer.draw(m.geometryId, m.materialId, p);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### SoA storage 매 cache-friendly
|
||||
```typescript
|
||||
// 매 Wonder ECS internally 매 SoA — 매 manual 의 example
|
||||
class PositionSoA {
|
||||
x: Float32Array; y: Float32Array; z: Float32Array;
|
||||
constructor(capacity: number) {
|
||||
this.x = new Float32Array(capacity);
|
||||
this.y = new Float32Array(capacity);
|
||||
this.z = new Float32Array(capacity);
|
||||
}
|
||||
}
|
||||
// 매 tight loop 매 cache hit — AoS 보다 2-5x faster
|
||||
```
|
||||
|
||||
### ReScript 매 functional system
|
||||
```rescript
|
||||
// 매 ReScript syntax
|
||||
let movement = (world, dt) => {
|
||||
let entities = World.query(world, [Position.id, Velocity.id])
|
||||
entities->Belt.Array.forEach(e => {
|
||||
let pos = World.getComponent(world, e, Position.id)
|
||||
let vel = World.getComponent(world, e, Velocity.id)
|
||||
World.setComponent(world, e, Position.id, {
|
||||
x: pos.x +. vel.x *. dt,
|
||||
y: pos.y +. vel.y *. dt,
|
||||
z: pos.z +. vel.z *. dt,
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Asset loading
|
||||
```typescript
|
||||
const assets = await Wonder.loadGLTF('scene.gltf');
|
||||
for (const node of assets.nodes) {
|
||||
const e = world.createEntity();
|
||||
world.addComponent(e, Position, node.translation);
|
||||
world.addComponent(e, Mesh, { geometryId: node.meshId, materialId: node.materialId });
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Engine choice |
|
||||
|---|---|
|
||||
| Mainstream web 3D | Three.js |
|
||||
| AAA-style + editor | Babylon.js / Wonderland Engine |
|
||||
| ECS / data-heavy | Wonder / bitECS + Three.js |
|
||||
| ReScript codebase | Wonder |
|
||||
| Massive entity count | ECS + InstancedMesh |
|
||||
|
||||
**기본값**: Three.js + bitECS — Wonder 매 specific ECS-first / ReScript 의 case.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[ECS]]
|
||||
- 변형: [[Threejs]] · [[Babylon.js]] · [[Wonderland Engine]]
|
||||
- 응용: [[bitECS]]
|
||||
- Adjacent: [[WebGL API]] · [[WebGPU]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: ECS-first 3D web project / ReScript 사용 codebase / large entity count + custom systems.
|
||||
**언제 X**: standard 3D scene — Three.js 매 더 ecosystem / artist-friendly editor 필요 — Wonderland / Babylon.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **OOP scene-graph mindset in ECS**: parent-child mutation 매 anti-ECS — entity hierarchy 의 component 의 모델.
|
||||
- **Per-entity allocation in tick**: GC pressure — pool / SoA.
|
||||
- **Wonder + Three.js mix**: rendering 매 conflict — choose one renderer.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Wonder OSS docs / GitHub).
|
||||
- 신뢰도 B (smaller community — verify against latest repo).
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — ECS arch, ReScript, SoA, vs Three.js |
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
id: wiki-2026-0508-wonderland-engine
|
||||
title: Wonderland Engine
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Wonderland, Wonderland Editor]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [wonderland, web3d, webxr, engine, vr, ar]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: JavaScript/TypeScript
|
||||
framework: Wonderland Engine
|
||||
---
|
||||
|
||||
# Wonderland Engine
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 WebXR-first 3D — Unity-like editor + WebAssembly runtime"**. Wonderland Engine 매 commercial web 3D engine 의 Unity-style scene editor + WASM-compiled C++ runtime — 매 WebXR (Quest browser / Vision Pro Safari) 의 first-class. 2026 매 web VR/AR studio 의 main alternative 의 Three.js + WebXR boilerplate.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 architecture
|
||||
- **C++ runtime → WASM**: 매 native-grade perf — Three.js JS 보다 2-3x throughput.
|
||||
- **Editor**: native desktop app (Win/Mac/Linux) — scene / prefab / animation.
|
||||
- **Component system**: TypeScript/JS components — Unity-like MonoBehaviour 패턴.
|
||||
- **Renderer**: WebGL2 + WebGPU (rolling out) — PBR / shadow / postprocess.
|
||||
- **WebXR**: built-in VR/AR session + controllers + hand tracking.
|
||||
|
||||
### 매 vs alternatives
|
||||
- **vs Three.js**: editor + perf + WebXR built-in / Three.js 매 더 flexible + ecosystem.
|
||||
- **vs PlayCanvas**: similar editor model / Wonderland 매 lighter + WebXR-focused.
|
||||
- **vs Unity WebGL build**: Wonderland 매 way smaller bundle (1-3MB vs 20MB+) + faster load.
|
||||
- **vs Babylon.js**: 둘 다 mature — Wonderland editor-driven / Babylon API-driven.
|
||||
|
||||
### 매 응용
|
||||
1. **WebXR VR apps**: Quest / Vision Pro — training / education / social.
|
||||
2. **Mobile AR**: WebXR AR session — product viz / try-on.
|
||||
3. **Marketing 3D**: brand experience — fast load + editor productivity.
|
||||
4. **Web games**: lightweight 3D — itch.io / Crazy Games.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Component (TS) 매 basic
|
||||
```typescript
|
||||
import { Component, Property } from '@wonderlandengine/api';
|
||||
|
||||
export class Rotator extends Component {
|
||||
static TypeName = 'rotator';
|
||||
static Properties = {
|
||||
speed: Property.float(1.0),
|
||||
axis: Property.enum(['x', 'y', 'z'], 'y'),
|
||||
};
|
||||
|
||||
speed!: number;
|
||||
axis!: number;
|
||||
|
||||
update(dt: number) {
|
||||
const rot: [number, number, number] = [0, 0, 0];
|
||||
rot[this.axis] = this.speed * dt;
|
||||
this.object.rotateAxisAngleDegLocal(
|
||||
[this.axis === 0 ? 1 : 0, this.axis === 1 ? 1 : 0, this.axis === 2 ? 1 : 0],
|
||||
this.speed * dt
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WebXR session
|
||||
```typescript
|
||||
import { Component, Property } from '@wonderlandengine/api';
|
||||
import { CursorTarget } from '@wonderlandengine/components';
|
||||
|
||||
export class VRController extends Component {
|
||||
static TypeName = 'vr-controller';
|
||||
|
||||
start() {
|
||||
this.engine.onXRSessionStart.add((session) => {
|
||||
console.log('XR session started:', session.mode);
|
||||
});
|
||||
}
|
||||
|
||||
onActivate() {
|
||||
this.engine.requestXRSession('immersive-vr', ['local-floor', 'hand-tracking']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hand tracking
|
||||
```typescript
|
||||
import { HandTracking } from '@wonderlandengine/components';
|
||||
|
||||
// 매 editor 의 component attach — wrist / fingertip joint 의 transform 매 tracked
|
||||
// runtime 매 joint position / pinch gesture 의 query
|
||||
```
|
||||
|
||||
### Physics (Rapier integration)
|
||||
```typescript
|
||||
import { Component } from '@wonderlandengine/api';
|
||||
import { PhysXComponent } from '@wonderlandengine/components';
|
||||
|
||||
export class Spawner extends Component {
|
||||
static TypeName = 'spawner';
|
||||
|
||||
spawn() {
|
||||
const obj = this.engine.scene.addObject(this.object);
|
||||
obj.addComponent('mesh', { mesh: this.cubeMesh, material: this.cubeMat });
|
||||
obj.addComponent('physx', {
|
||||
shape: 'Box',
|
||||
mass: 1.0,
|
||||
extents: [0.1, 0.1, 0.1],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mesh streaming + dynamic load
|
||||
```typescript
|
||||
import { loadGLTF } from '@wonderlandengine/api';
|
||||
|
||||
async function loadAsset(url: string) {
|
||||
const { meshes, materials } = await loadGLTF(this.engine, url);
|
||||
const obj = this.engine.scene.addObject();
|
||||
obj.addComponent('mesh', { mesh: meshes[0], material: materials[0] });
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
### Animation player
|
||||
```typescript
|
||||
import { AnimationComponent } from '@wonderlandengine/api';
|
||||
|
||||
const anim = this.object.getComponent('animation') as AnimationComponent;
|
||||
anim.play();
|
||||
anim.speed = 0.5;
|
||||
anim.onEvent.add((evt) => console.log('anim event', evt));
|
||||
```
|
||||
|
||||
### Networked multiplayer (Wonderland Cloud / custom)
|
||||
```typescript
|
||||
// 매 Wonderland Cloud — managed multiplayer
|
||||
import { CloudClient } from '@wonderlandengine/cloud';
|
||||
|
||||
const client = new CloudClient({ projectId: 'abc123' });
|
||||
await client.connect();
|
||||
client.onPlayerJoin.add((p) => spawnAvatar(p));
|
||||
client.sendState({ pos: this.object.getPositionWorld() });
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Engine |
|
||||
|---|---|
|
||||
| WebXR VR/AR primary | Wonderland Engine |
|
||||
| 2D UI heavy | Three.js / Babylon (DOM/CSS overlay 매 easier) |
|
||||
| Editor-driven workflow | Wonderland / PlayCanvas |
|
||||
| Code-first, max flexibility | Three.js |
|
||||
| Smallest bundle | Wonderland (WASM-optimized) |
|
||||
| Existing Unity team transitioning | Wonderland (familiar component model) |
|
||||
|
||||
**기본값**: WebXR target 매 Wonderland / general 3D web 매 Three.js.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[WebXR]]
|
||||
- 변형: [[Threejs]] · [[Babylon.js]] · [[Wonder]]
|
||||
- Adjacent: [[WebAssembly]] · [[WebGL API]] · [[WebGPU]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: WebXR-first project / Unity team 의 web migration / lightweight 3D + editor productivity.
|
||||
**언제 X**: code-first / massive Three.js ecosystem dependency / commercial license 매 issue (free tier limits).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Editor scene 의 ignore**: runtime-only entity creation — editor workflow 매 lost.
|
||||
- **Heavy per-frame allocation**: TypedArray reuse / vec3 pool 의 사용.
|
||||
- **Mixing Three.js loaders**: glTF 의 Wonderland loader 의 사용 — material / animation binding.
|
||||
- **No LOD on mobile XR**: Quest 매 vertex / fragment budget 의 strict — LOD + texture compress.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Wonderland Engine official docs / GitHub).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — architecture, components, WebXR, physics, hand tracking |
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
---
|
||||
id: wiki-2026-0508-zero-runtime-css-in-js
|
||||
title: Zero-Runtime CSS-in-JS
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Zero-Runtime CSS-in-JS, Zero Runtime CSS, Static CSS-in-JS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css, css-in-js, build-time, zero-runtime, vanilla-extract, linaria]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: vanilla-extract
|
||||
---
|
||||
|
||||
# Zero-Runtime CSS-in-JS
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 build time 에 CSS 로 추출 — runtime overhead 0"**. 매 styled-components/emotion 의 runtime injection 의 단점 (FCP 손실, hydration cost, RSC 비호환) 매 해결. 매 vanilla-extract (Mark Dalgleish), Linaria, Panda CSS, StyleX (Meta) 매 대표주자. 매 React Server Component 시대의 사실상 표준.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 runtime CSS-in-JS 의 문제
|
||||
- **Bundle size**: 매 emotion ~10KB, styled-components ~12KB minified.
|
||||
- **FCP 손실**: 매 JS 가 parse → execute → CSS inject → paint. 매 3-tier waterfall.
|
||||
- **Hydration cost**: 매 server-rendered CSS 와 client style 의 sync overhead.
|
||||
- **RSC 비호환**: 매 React Server Component 매 runtime context 없음 — 매 styled-components/emotion 매 client-only.
|
||||
- **Re-render cost**: 매 dynamic style prop 의 매 render 마다 hash + inject.
|
||||
|
||||
### 매 zero-runtime 의 해결
|
||||
- **Build-time extraction**: 매 .css.ts → .css 로 매 extract.
|
||||
- **Type safety**: 매 TypeScript 로 token, theme, variants 매 type-check.
|
||||
- **No runtime**: 매 0 KB 추가 — 매 static CSS 와 동일.
|
||||
- **RSC 호환**: 매 build-time output 이라 server/client 무관.
|
||||
- **CSS-only features**: 매 :hover, :focus, media query 매 CSS 자체 사용.
|
||||
|
||||
### 매 응용
|
||||
1. Next.js App Router (RSC) 와 매 vanilla-extract pairing.
|
||||
2. Design system 의 token-based theming (light/dark, brand variant).
|
||||
3. Component library publishing (no runtime dependency).
|
||||
4. Performance-critical apps (e-commerce, content sites).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### vanilla-extract — basic style
|
||||
```ts
|
||||
// button.css.ts
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const button = style({
|
||||
padding: '8px 16px',
|
||||
borderRadius: 4,
|
||||
background: 'blue',
|
||||
color: 'white',
|
||||
':hover': {
|
||||
background: 'darkblue',
|
||||
},
|
||||
});
|
||||
|
||||
// button.tsx
|
||||
import { button } from './button.css';
|
||||
export const Button = (props) => <button className={button} {...props} />;
|
||||
```
|
||||
|
||||
### vanilla-extract — variants (recipes)
|
||||
```ts
|
||||
// button.css.ts
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
|
||||
export const button = recipe({
|
||||
base: { padding: 8, borderRadius: 4 },
|
||||
variants: {
|
||||
intent: {
|
||||
primary: { background: 'blue', color: 'white' },
|
||||
danger: { background: 'red', color: 'white' },
|
||||
},
|
||||
size: {
|
||||
sm: { fontSize: 12 },
|
||||
md: { fontSize: 14 },
|
||||
lg: { fontSize: 18 },
|
||||
},
|
||||
},
|
||||
defaultVariants: { intent: 'primary', size: 'md' },
|
||||
});
|
||||
|
||||
// usage
|
||||
<button className={button({ intent: 'danger', size: 'lg' })}>Delete</button>
|
||||
```
|
||||
|
||||
### vanilla-extract — theme contract
|
||||
```ts
|
||||
// theme.css.ts
|
||||
import { createTheme, createThemeContract } from '@vanilla-extract/css';
|
||||
|
||||
export const vars = createThemeContract({
|
||||
color: { brand: null, text: null, bg: null },
|
||||
space: { sm: null, md: null, lg: null },
|
||||
});
|
||||
|
||||
export const lightTheme = createTheme(vars, {
|
||||
color: { brand: '#0066ff', text: '#000', bg: '#fff' },
|
||||
space: { sm: '4px', md: '8px', lg: '16px' },
|
||||
});
|
||||
|
||||
export const darkTheme = createTheme(vars, {
|
||||
color: { brand: '#3399ff', text: '#fff', bg: '#000' },
|
||||
space: { sm: '4px', md: '8px', lg: '16px' },
|
||||
});
|
||||
|
||||
// component uses vars
|
||||
import { vars } from './theme.css';
|
||||
const card = style({
|
||||
background: vars.color.bg,
|
||||
color: vars.color.text,
|
||||
padding: vars.space.md,
|
||||
});
|
||||
```
|
||||
|
||||
### Linaria — tagged template
|
||||
```ts
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const button = css`
|
||||
padding: 8px 16px;
|
||||
background: ${({ primary }) => primary ? 'blue' : 'gray'};
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
border-radius: 4px;
|
||||
&:hover { opacity: 0.8; }
|
||||
`;
|
||||
// 매 build-time 에 CSS 추출, dynamic prop 매 CSS variable 로 변환.
|
||||
```
|
||||
|
||||
### Panda CSS — atomic + tokens
|
||||
```ts
|
||||
// panda.config.ts
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
tokens: {
|
||||
colors: { brand: { value: '#0066ff' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// component
|
||||
import { css } from 'styled-system/css';
|
||||
<div className={css({
|
||||
bg: 'brand',
|
||||
p: '4',
|
||||
rounded: 'md',
|
||||
_hover: { opacity: 0.8 },
|
||||
})}>
|
||||
Hello
|
||||
</div>
|
||||
// 매 build-time 에 atomic class generation. Tailwind-like.
|
||||
```
|
||||
|
||||
### StyleX (Meta) — atomic
|
||||
```ts
|
||||
import * as stylex from '@stylexjs/stylex';
|
||||
|
||||
const styles = stylex.create({
|
||||
button: {
|
||||
padding: '8px 16px',
|
||||
backgroundColor: { default: 'blue', ':hover': 'darkblue' },
|
||||
},
|
||||
});
|
||||
|
||||
<button {...stylex.props(styles.button)}>Click</button>
|
||||
// 매 Meta Facebook/Instagram 사용. 매 atomic class deduplication.
|
||||
```
|
||||
|
||||
### Migration — emotion → vanilla-extract
|
||||
```ts
|
||||
// Before (emotion)
|
||||
const Button = styled.button`
|
||||
padding: ${props => props.size === 'lg' ? '16px' : '8px'};
|
||||
`;
|
||||
|
||||
// After (vanilla-extract recipes)
|
||||
const button = recipe({
|
||||
base: {},
|
||||
variants: {
|
||||
size: { sm: { padding: 8 }, lg: { padding: 16 } },
|
||||
},
|
||||
});
|
||||
const Button = ({ size, ...rest }) => <button className={button({ size })} {...rest} />;
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Next.js App Router (RSC) | vanilla-extract or Panda CSS. |
|
||||
| TypeScript theme system | vanilla-extract (createThemeContract). |
|
||||
| Tailwind-style atomic | Panda CSS or StyleX. |
|
||||
| Existing styled-components codebase | Linaria (similar API, build-time). |
|
||||
| Component library 배포 | vanilla-extract (zero runtime dep). |
|
||||
| Quick prototype | Tailwind (no build setup) or emotion. |
|
||||
|
||||
**기본값**: vanilla-extract (Next.js + RSC), Panda CSS (atomic), Linaria (migration).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS_Architecture_and_Styling|CSS-in-JS]]
|
||||
- 변형: [[vanilla-extract]] · [[Panda CSS]]
|
||||
- 응용: [[Design System]] · [[React Server Components]]
|
||||
- Adjacent: [[CSS_Architecture_and_Styling|Tailwind CSS]] · [[CSS Modules]] · [[Styled_Components|styled-components]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Next.js App Router (RSC) 의, performance-critical site, design system 의 type-safe theming, component library 배포 의.
|
||||
**언제 X**: 매 highly dynamic runtime style (theme switcher 매 CSS var 로 가능 — 그 외 의), 매 prototype 단계의 (Tailwind 매 빠름).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Runtime variable 을 build-time 으로 expect**: 매 `style({ color: dynamicVar })` — `dynamicVar` 매 build time 의 evaluable 해야. Runtime value 매 CSS variable 로.
|
||||
- **emotion + RSC 혼합**: 매 RSC 에서 emotion `css` prop X. 매 Server component 매 zero-runtime 만.
|
||||
- **css.ts 의 conditional logic**: 매 `if (process.env.NODE_ENV)` 매 build-time 에 evaluated — 매 runtime 변경 X.
|
||||
- **Import side effect 망각**: 매 vanilla-extract `*.css.ts` import 가 CSS 추출 trigger — tree-shake 의 영향.
|
||||
- **Tailwind 와 zero-runtime 의 비교 오류**: 매 Tailwind 도 zero-runtime — 단지 매 CSS-in-JS 가 아닌 utility-first.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (vanilla-extract docs, Linaria docs, Panda CSS docs, StyleX official).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — vanilla-extract, Linaria, Panda CSS, StyleX patterns + RSC compatibility 추가 |
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
id: wiki-2026-0508-starttransition
|
||||
title: startTransition
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [startTransition, useTransition, React transition]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [react, concurrent, startTransition, useTransition, priority]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-19
|
||||
---
|
||||
|
||||
# startTransition
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 React 18+ 의 update priority API — non-urgent state update 매 mark"**. 매 input typing (urgent) 와 search result render (non-urgent) 의 분리. 매 stale UI 표시, interruptible re-render, Suspense fallback skip 매 가능. 매 useDeferredValue 와 함께 React Concurrent Mode 의 핵심.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 priority 분류
|
||||
- **Urgent (synchronous)**: 매 input value, click 의 visual feedback. 매 default.
|
||||
- **Transition (low-priority)**: 매 expensive list filter, route change, data 의 UI update.
|
||||
- 매 urgent update 가 transition 중 들어오면 매 transition interrupt + restart.
|
||||
|
||||
### 매 useTransition vs startTransition
|
||||
- `useTransition()`: 매 component 안 — `[isPending, startTransition]` 반환. 매 pending UI 표시 가능.
|
||||
- `startTransition(fn)`: 매 import 직접 — pending state 추적 불필요한 case.
|
||||
|
||||
### 매 동작
|
||||
- 매 transition 안 setState 매 low-priority 로 mark.
|
||||
- 매 React 매 다른 urgent work 우선 처리, 매 transition 매 idle 시 처리.
|
||||
- 매 concurrent renderer 매 stale tree 유지하면서 new tree 백그라운드 build.
|
||||
- 매 Suspense — 매 transition 의 fallback 표시 skip (existing UI 유지).
|
||||
|
||||
### 매 응용
|
||||
1. Search input — typing (urgent) + result list (transition).
|
||||
2. Tab switch — instant feedback + heavy content (transition).
|
||||
3. Route change — 매 fallback skip 으로 smooth navigation.
|
||||
4. Filter / sort — 매 list 가 매우 클 때.
|
||||
5. Data fetch + render — 매 Suspense + transition 조합.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### useTransition — search
|
||||
```tsx
|
||||
function Search() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<Item[]>([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(e.target.value); // 매 urgent — input 즉시 update
|
||||
startTransition(() => {
|
||||
setResults(filterItems(e.target.value)); // 매 low-priority
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={onChange} />
|
||||
{isPending && <Spinner />}
|
||||
<ResultList items={results} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Tab switch — instant + heavy content
|
||||
```tsx
|
||||
function Tabs() {
|
||||
const [tab, setTab] = useState<'home' | 'reports' | 'settings'>('home');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const select = (next: typeof tab) => {
|
||||
startTransition(() => setTab(next));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabButton onClick={() => select('home')} active={tab === 'home'}>Home</TabButton>
|
||||
<TabButton onClick={() => select('reports')} active={tab === 'reports'}>Reports</TabButton>
|
||||
{isPending && <span>Loading...</span>}
|
||||
{tab === 'home' && <HomeContent />}
|
||||
{tab === 'reports' && <ReportsContent />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// 매 click 의 visual feedback 즉시, content 매 transition.
|
||||
```
|
||||
|
||||
### useDeferredValue — alternative
|
||||
```tsx
|
||||
function Search({ query }: { query: string }) {
|
||||
const deferred = useDeferredValue(query);
|
||||
const isStale = deferred !== query;
|
||||
return (
|
||||
<div style={{ opacity: isStale ? 0.5 : 1 }}>
|
||||
<ResultList query={deferred} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 매 useDeferredValue: 매 prop / state 에서 derive — startTransition 보다 declarative.
|
||||
```
|
||||
|
||||
### Suspense — fallback skip during transition
|
||||
```tsx
|
||||
function App() {
|
||||
const [page, setPage] = useState(0);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<button onClick={() => startTransition(() => setPage(p => p + 1))}>
|
||||
Next
|
||||
</button>
|
||||
{isPending && <Spinner />}
|
||||
<PageData page={page} /> {/* 매 suspends on data fetch */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
// 매 transition 의 page change 시 매 Skeleton X — 기존 page UI 유지 + spinner.
|
||||
```
|
||||
|
||||
### React 19 — useTransition 의 async support
|
||||
```tsx
|
||||
function SaveButton() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const save = () => {
|
||||
startTransition(async () => {
|
||||
// 매 React 19 부터 async function 직접 지원
|
||||
try {
|
||||
await api.save(formData);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={save} disabled={isPending}>
|
||||
{isPending ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
// 매 React 18 매 async 직접 X — startTransition 의 outer wrap 필요.
|
||||
```
|
||||
|
||||
### Route change — Next.js App Router
|
||||
```tsx
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTransition } from 'react';
|
||||
|
||||
function Nav() {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<button onClick={() => startTransition(() => router.push('/dashboard'))}>
|
||||
{isPending ? 'Loading...' : 'Dashboard'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Combined with flushSync (반대 우선순위)
|
||||
```tsx
|
||||
// 매 startTransition: 매 low-priority, batchable, interruptible.
|
||||
startTransition(() => setBigList(newData));
|
||||
|
||||
// 매 flushSync: 매 high-priority, sync, immediate.
|
||||
flushSync(() => setUrgent(true));
|
||||
|
||||
// 매 둘은 정반대 — 매 같이 사용 X.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Heavy filter / sort with input | useTransition. |
|
||||
| Tab / route switch | useTransition. |
|
||||
| Derived value from prop | useDeferredValue. |
|
||||
| Async save / submit (React 19+) | useTransition (async). |
|
||||
| Imperative DOM after state | flushSync (반대). |
|
||||
| Simple fast state | 매 plain setState. |
|
||||
|
||||
**기본값**: 매 user input 분리 의 concurrent UI — useTransition. 매 prop-derived expensive 은 useDeferredValue.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[React]] · [[Concurrent React]]
|
||||
- 변형: [[useTransition]] · [[useDeferredValue]] · [[Suspense]]
|
||||
- 응용: [[Search]]
|
||||
- Adjacent: [[flushSync]] · [[Automatic Batching]] · [[React Server Components]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 input + heavy filter, tab switch, route change with data fetch, async form submit (React 19+) 의.
|
||||
**언제 X**: 매 simple light state update (overhead 무의미), 매 imperative DOM 의 즉시 sync (flushSync), 매 server-only logic.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 setState 에 startTransition**: 매 useless overhead, 매 input lag 등 의 unintended effect.
|
||||
- **isPending 무시**: 매 visual feedback 없으면 user confusion.
|
||||
- **External state (zustand etc.) 의 transition**: 매 useTransition 매 React state 만 affect — external store 매 자체 batching.
|
||||
- **Animation 의 transition**: 매 animation 매 urgent — interrupted 되면 jank. flushSync / RAF 사용.
|
||||
- **React 18 의 async startTransition**: 매 React 18 매 sync function 만 — async 매 outer wrap 또는 React 19 upgrade.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React docs useTransition, React 18 release notes, React 19 changes).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — startTransition / useTransition patterns, useDeferredValue 비교, React 19 async 추가 |
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
id: wiki-2026-0508-vanilla-extract
|
||||
title: vanilla-extract
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [vanilla-extract, vanilla extract, ve, css.ts]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [css-in-js, zero-runtime, vanilla-extract, typescript, build-time]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: vanilla-extract
|
||||
---
|
||||
|
||||
# vanilla-extract
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 TypeScript 로 type-safe 한 zero-runtime CSS-in-JS"**. 매 Mark Dalgleish (Seek) 가 2021 년 출시. 매 `.css.ts` 파일을 build time 에 static CSS 로 추출, 매 token / variant / theme 매 모두 typed. 매 Next.js App Router (RSC) 의 사실상 표준, 매 component library 배포 의 zero-runtime guarantee.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 핵심 API
|
||||
- `style({...})`: 매 single CSS class 생성 → unique class name 반환.
|
||||
- `globalStyle('selector', {...})`: 매 global selector style.
|
||||
- `recipe({base, variants})`: 매 variant-based component (CVA-like, type-safe).
|
||||
- `createTheme(contract, values)`: 매 token 의 ThemeProvider 대안.
|
||||
- `createThemeContract({...})`: 매 token shape 정의 (multi-theme).
|
||||
- `createVar()`: 매 CSS custom property (runtime dynamic).
|
||||
- `style([base, override])`: 매 style composition.
|
||||
|
||||
### 매 build setup
|
||||
- **Webpack**: `@vanilla-extract/webpack-plugin`.
|
||||
- **Vite**: `@vanilla-extract/vite-plugin`.
|
||||
- **Next.js**: `@vanilla-extract/next-plugin` (App Router 호환).
|
||||
- **esbuild / Rollup / Parcel**: 매 dedicated plugins.
|
||||
- 매 결과: `.css.ts` → `.css` 추출 + `.js` 의 class name string export.
|
||||
|
||||
### 매 응용
|
||||
1. Design system 의 token-based theming.
|
||||
2. Next.js App Router 의 RSC-compatible styling.
|
||||
3. Component library 의 zero-runtime publish.
|
||||
4. CVA-style variants via recipes.
|
||||
5. CSS variable scoped to component.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic style
|
||||
```ts
|
||||
// button.css.ts
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const button = style({
|
||||
padding: '8px 16px',
|
||||
borderRadius: 4,
|
||||
background: 'blue',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': { background: 'darkblue' },
|
||||
'&:disabled': { opacity: 0.5, cursor: 'not-allowed' },
|
||||
},
|
||||
});
|
||||
|
||||
// button.tsx
|
||||
import { button } from './button.css';
|
||||
export const Button = (props) => <button className={button} {...props} />;
|
||||
```
|
||||
|
||||
### Theme contract — multi-theme
|
||||
```ts
|
||||
// theme.css.ts
|
||||
import { createTheme, createThemeContract } from '@vanilla-extract/css';
|
||||
|
||||
export const vars = createThemeContract({
|
||||
color: { brand: null, text: null, bg: null, border: null },
|
||||
space: { xs: null, sm: null, md: null, lg: null, xl: null },
|
||||
radius: { sm: null, md: null, lg: null },
|
||||
font: { sans: null, mono: null },
|
||||
});
|
||||
|
||||
export const lightTheme = createTheme(vars, {
|
||||
color: { brand: '#0066ff', text: '#111', bg: '#fff', border: '#e0e0e0' },
|
||||
space: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' },
|
||||
radius: { sm: '2px', md: '4px', lg: '8px' },
|
||||
font: { sans: 'Inter, sans-serif', mono: 'JetBrains Mono, monospace' },
|
||||
});
|
||||
|
||||
export const darkTheme = createTheme(vars, {
|
||||
color: { brand: '#3399ff', text: '#fff', bg: '#0a0a0a', border: '#222' },
|
||||
space: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' },
|
||||
radius: { sm: '2px', md: '4px', lg: '8px' },
|
||||
font: { sans: 'Inter, sans-serif', mono: 'JetBrains Mono, monospace' },
|
||||
});
|
||||
|
||||
// app.tsx
|
||||
<html className={isDark ? darkTheme : lightTheme}>
|
||||
```
|
||||
|
||||
### Component using vars
|
||||
```ts
|
||||
// card.css.ts
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { vars } from './theme.css';
|
||||
|
||||
export const card = style({
|
||||
background: vars.color.bg,
|
||||
color: vars.color.text,
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
padding: vars.space.md,
|
||||
borderRadius: vars.radius.lg,
|
||||
fontFamily: vars.font.sans,
|
||||
});
|
||||
```
|
||||
|
||||
### Recipes — variants (CVA-like)
|
||||
```ts
|
||||
// button.css.ts
|
||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { vars } from './theme.css';
|
||||
|
||||
export const button = recipe({
|
||||
base: {
|
||||
padding: vars.space.sm,
|
||||
borderRadius: vars.radius.md,
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
fontFamily: vars.font.sans,
|
||||
},
|
||||
variants: {
|
||||
intent: {
|
||||
primary: { background: vars.color.brand, color: '#fff' },
|
||||
secondary: { background: 'transparent', color: vars.color.brand,
|
||||
border: `1px solid ${vars.color.brand}` },
|
||||
danger: { background: '#dc2626', color: '#fff' },
|
||||
},
|
||||
size: {
|
||||
sm: { fontSize: 12, padding: vars.space.xs },
|
||||
md: { fontSize: 14, padding: vars.space.sm },
|
||||
lg: { fontSize: 18, padding: vars.space.md },
|
||||
},
|
||||
fullWidth: {
|
||||
true: { width: '100%' },
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{ variants: { intent: 'primary', size: 'lg' },
|
||||
style: { fontWeight: 'bold' } },
|
||||
],
|
||||
defaultVariants: { intent: 'primary', size: 'md' },
|
||||
});
|
||||
|
||||
export type ButtonVariants = RecipeVariants<typeof button>;
|
||||
// type ButtonVariants = { intent?: 'primary' | 'secondary' | 'danger'; size?: ...; fullWidth?: boolean }
|
||||
|
||||
// usage
|
||||
<button className={button({ intent: 'danger', size: 'lg' })}>Delete</button>
|
||||
```
|
||||
|
||||
### Sprinkles — atomic CSS (Tailwind-like)
|
||||
```ts
|
||||
// sprinkles.css.ts
|
||||
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
|
||||
|
||||
const space = defineProperties({
|
||||
properties: {
|
||||
padding: { sm: 8, md: 16, lg: 24 },
|
||||
margin: { sm: 8, md: 16, lg: 24 },
|
||||
},
|
||||
shorthands: { p: ['padding'], m: ['margin'] },
|
||||
});
|
||||
|
||||
export const sprinkles = createSprinkles(space);
|
||||
|
||||
// usage
|
||||
<div className={sprinkles({ p: 'md', m: 'sm' })}>...</div>
|
||||
```
|
||||
|
||||
### Global style + reset
|
||||
```ts
|
||||
// reset.css.ts
|
||||
import { globalStyle } from '@vanilla-extract/css';
|
||||
|
||||
globalStyle('html, body', {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
});
|
||||
|
||||
globalStyle('*', {
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
// 매 import 'one' 으로 reset 적용
|
||||
// app.tsx: import './reset.css';
|
||||
```
|
||||
|
||||
### Dynamic value via createVar
|
||||
```ts
|
||||
// progress.css.ts
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const progressVar = createVar();
|
||||
|
||||
export const bar = style({
|
||||
width: progressVar,
|
||||
background: 'blue',
|
||||
height: 4,
|
||||
transition: 'width 0.3s',
|
||||
});
|
||||
|
||||
// progress.tsx
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
|
||||
<div
|
||||
className={bar}
|
||||
style={assignInlineVars({ [progressVar]: `${percent}%` })}
|
||||
/>
|
||||
// 매 runtime dynamic — CSS variable 로 변환.
|
||||
```
|
||||
|
||||
### Style composition
|
||||
```ts
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const base = style({ padding: 8, fontSize: 14 });
|
||||
const primary = style([base, { background: 'blue', color: 'white' }]);
|
||||
// 매 primary class = base class + extra rules.
|
||||
```
|
||||
|
||||
### Next.js App Router setup
|
||||
```ts
|
||||
// next.config.js
|
||||
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
|
||||
const withVanillaExtract = createVanillaExtractPlugin();
|
||||
module.exports = withVanillaExtract({
|
||||
// ... existing config
|
||||
});
|
||||
|
||||
// app/layout.tsx
|
||||
import { lightTheme } from './theme.css';
|
||||
import './reset.css';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return <html className={lightTheme}>{children}</html>;
|
||||
}
|
||||
// 매 RSC 호환 — server component 에서 매 className 사용 가능.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Next.js App Router (RSC) | vanilla-extract (built-in plugin). |
|
||||
| Type-safe theme tokens | `createThemeContract` + `createTheme`. |
|
||||
| CVA-style variants | `recipe`. |
|
||||
| Atomic / Tailwind-like | `sprinkles`. |
|
||||
| Runtime dynamic value | `createVar` + `assignInlineVars`. |
|
||||
| Component library 배포 | vanilla-extract (zero runtime). |
|
||||
|
||||
**기본값**: `style` (single class), `recipe` (variants), `createThemeContract` (multi-theme), `createVar` (runtime dynamic).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS_Architecture_and_Styling|CSS-in-JS]] · [[Zero-Runtime CSS-in-JS]]
|
||||
- 변형: [[Panda CSS]] · [[CSS Modules]]
|
||||
- 응용: [[Design System]] · [[React Server Components]]
|
||||
- Adjacent: [[CVA]] · [[CSS_Architecture_and_Styling|Tailwind CSS]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Next.js App Router (RSC), type-safe design system, component library 배포, multi-theme (light/dark/brand) 의.
|
||||
**언제 X**: 매 simple prototype (Tailwind 매 빠름), 매 highly-dynamic per-pixel value (CSS-in-JS runtime 가 더 적합), 매 already emotion / styled-components codebase 의 incremental migration 어려움.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Runtime value 직접 사용**: 매 `style({ width: dynamicWidth })` — `dynamicWidth` 매 build time 에 evaluable 해야. 매 runtime 매 `createVar`.
|
||||
- **`.css.ts` 의 conditional logic**: 매 `if (env.X)` 매 build-time evaluated — 매 runtime 분기 X.
|
||||
- **Theme class 의 dynamic apply 미숙**: 매 theme 변경 시 `<html className={isDark ? darkTheme : lightTheme}>` — 매 root 단위.
|
||||
- **plugin setup 누락**: 매 webpack/vite plugin 없으면 .css.ts 매 그냥 TS file 로 처리 — error.
|
||||
- **`recipe` 의 base 의 selector**: 매 `recipe` base 안의 `selectors` 매 매 supported 하나 매 syntax 주의.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (vanilla-extract.style 공식 문서, GitHub seek-oss/vanilla-extract).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — vanilla-extract API 전체 (style, recipe, theme contract, sprinkles, createVar) 추가 |
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
---
|
||||
id: wiki-2026-0508-다크-모드-및-다중-브랜드-테마-동적-전환-시스템
|
||||
title: 다크 모드 및 다중 브랜드 테마 동적 전환 시스템
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Dark Mode, Theme Switching, Multi-brand Theming]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, css, theming, dark-mode, design-system]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript/CSS
|
||||
framework: React/Next.js
|
||||
---
|
||||
|
||||
# 다크 모드 및 다중 브랜드 테마 동적 전환 시스템
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CSS variables × design tokens × runtime swap 의 zero-flash theming"**. 매 다크 모드는 단순 색상 toggle이 아닌, design token system의 multi-channel orchestration. 2026 표준은 `light-dark()` CSS function + `prefers-color-scheme` + system token + brand override 의 4-layer cascade.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4-Layer Token Architecture
|
||||
1. **Primitive tokens**: `--color-blue-500: oklch(60% 0.2 240)`.
|
||||
2. **Semantic tokens**: `--color-bg-primary` (theme-aware).
|
||||
3. **Component tokens**: `--button-bg = --color-action`.
|
||||
4. **Brand override**: `[data-brand="acme"] { --color-action: ... }`.
|
||||
|
||||
### 매 SSR Flash 방지
|
||||
- Inline blocking script (head) 의 `localStorage` read → `<html data-theme="dark">` 의 set before paint.
|
||||
- Next.js: `next-themes` library 의 표준.
|
||||
- Cookie-based 의 SSR-aware (Next 15 RSC).
|
||||
|
||||
### 매 응용
|
||||
1. GitHub 의 `light/light-high-contrast/dark/dark-dimmed/dark-high-contrast` 의 5 modes.
|
||||
2. Stripe Dashboard 의 brand switcher (Atlas, Climate 등).
|
||||
3. Linear 의 theme + accent color picker.
|
||||
4. VS Code 의 theme marketplace (color-theme JSON).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. CSS `light-dark()` (2026 baseline)
|
||||
```css
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: light-dark(white, #0a0a0a);
|
||||
--fg: light-dark(#0a0a0a, white);
|
||||
--accent: light-dark(oklch(55% 0.2 250), oklch(70% 0.18 250));
|
||||
}
|
||||
|
||||
[data-theme="light"] { color-scheme: light; }
|
||||
[data-theme="dark"] { color-scheme: dark; }
|
||||
```
|
||||
|
||||
### 2. Multi-brand token override
|
||||
```css
|
||||
:root[data-brand="default"] { --brand-primary: oklch(55% 0.2 250); }
|
||||
:root[data-brand="acme"] { --brand-primary: oklch(60% 0.22 30); }
|
||||
:root[data-brand="globex"] { --brand-primary: oklch(58% 0.25 140); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brand-primary);
|
||||
color: light-dark(white, black);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. No-flash inline script (Next.js layout.tsx)
|
||||
```tsx
|
||||
const themeScript = `
|
||||
(function() {
|
||||
try {
|
||||
const saved = localStorage.getItem('theme');
|
||||
const sys = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
const theme = saved || sys;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
} catch (e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ko" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. React Theme Provider (next-themes 스타일)
|
||||
```tsx
|
||||
'use client';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
const ThemeCtx = createContext<{ theme: Theme; setTheme: (t: Theme) => void }>(null!);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>('system');
|
||||
|
||||
useEffect(() => {
|
||||
const saved = (localStorage.getItem('theme') as Theme) || 'system';
|
||||
setThemeState(saved);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const resolved = theme === 'system'
|
||||
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
: theme;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
document.documentElement.style.colorScheme = resolved;
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = (t: Theme) => {
|
||||
localStorage.setItem('theme', t);
|
||||
setThemeState(t);
|
||||
};
|
||||
|
||||
return <ThemeCtx.Provider value={{ theme, setTheme }}>{children}</ThemeCtx.Provider>;
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeCtx);
|
||||
```
|
||||
|
||||
### 5. Brand context + dynamic token injection
|
||||
```tsx
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function BrandProvider({ brand, children }: { brand: 'acme' | 'globex'; children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.brand = brand;
|
||||
}, [brand]);
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Tailwind v4 + design tokens
|
||||
```css
|
||||
/* app/globals.css */
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg: light-dark(white, #0a0a0a);
|
||||
--color-fg: light-dark(#0a0a0a, white);
|
||||
--color-brand: var(--brand-primary, oklch(55% 0.2 250));
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<button className="bg-brand text-bg">Click</button>
|
||||
```
|
||||
|
||||
### 7. Image / asset variants (theme-aware)
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
|
||||
export function Logo() {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<picture>
|
||||
<source srcSet="/logo-dark.svg" media="(prefers-color-scheme: dark)" />
|
||||
<img src="/logo-light.svg" alt="Logo" />
|
||||
</picture>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8. View Transitions API (smooth theme swap)
|
||||
```ts
|
||||
async function toggleTheme(next: 'light' | 'dark') {
|
||||
if (!document.startViewTransition) {
|
||||
document.documentElement.dataset.theme = next;
|
||||
return;
|
||||
}
|
||||
await document.startViewTransition(() => {
|
||||
document.documentElement.dataset.theme = next;
|
||||
}).ready;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 200ms;
|
||||
}
|
||||
```
|
||||
|
||||
### 9. SSR cookie-based theme (Next 15 RSC)
|
||||
```tsx
|
||||
// app/layout.tsx (server)
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export default async function Layout({ children }) {
|
||||
const theme = (await cookies()).get('theme')?.value ?? 'light';
|
||||
return <html data-theme={theme}>{children}</html>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 단일 브랜드 + dark mode | CSS `light-dark()` + `prefers-color-scheme` |
|
||||
| 다중 브랜드 SaaS | `data-brand` attribute + token override |
|
||||
| SSR Next.js | next-themes OR cookie-based RSC |
|
||||
| Tailwind v4 | `@theme` + CSS vars |
|
||||
| Smooth transition | View Transitions API |
|
||||
| Legacy browser | Class-based (`.dark`) + localStorage |
|
||||
|
||||
**기본값**: CSS `light-dark()` + `data-theme` + `data-brand` + cookie SSR + next-themes.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Design_Systems]] · [[CSS_Variables]]
|
||||
- 변형: [[Design_Tokens]]
|
||||
- 응용: [[Tailwind_v4]] · [[Styled_Components_v6]] · [[CSS_Modules]]
|
||||
- Adjacent: [[View_Transitions_API]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: token naming convention 제안, contrast ratio audit, brand palette 생성 (OKLCH 기반).
|
||||
**언제 X**: 최종 brand color decision (디자이너), accessibility AA/AAA 의 자동 보장은 X.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No-flash 미구현**: hydration 후 theme 적용 → FOUC.
|
||||
- **Hardcoded hex**: design token 의 bypass — multi-brand 의 X.
|
||||
- **JS-only theming**: CSS-only 의 fallback 없으면 SSR flash.
|
||||
- **Inverted contrast**: 다크 모드의 단순 색상 invert — semantic hierarchy 의 break.
|
||||
- **`!important` overuse**: token cascade 의 collapse.
|
||||
- **`localStorage` SSR access**: hydration mismatch — `useEffect`로 lazy.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev color-scheme guide, MDN `light-dark()`, next-themes docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — dark mode + multi-brand theming full content |
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
---
|
||||
id: wiki-2026-0508-단일-진실-공급원-single-source-of-truth
|
||||
title: 단일 진실 공급원(Single Source of Truth)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [SSOT, Single Source of Truth, 진실의 단일 공급원]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [architecture, state-management, ssot, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-19
|
||||
---
|
||||
|
||||
# 단일 진실 공급원(Single Source of Truth)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 모든 derived state 의 single canonical origin"**. 매 1970s database normalization 에서 시작 — 매 modern frontend (Redux, TanStack Query, RSC) 의 핵심 원리. 매 duplication 의 elimination → 매 inconsistency bug 의 소멸.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 정의
|
||||
- 매 한 데이터 의 **하나의** authoritative location.
|
||||
- 매 다른 모든 view/component 의 derive (read-only projection).
|
||||
- 매 update 의 single entry point — 매 race / drift 의 prevention.
|
||||
|
||||
### 매 frontend 적용 layer
|
||||
- **Server state SSOT**: 매 backend DB — 매 client 의 cache (TanStack Query, SWR).
|
||||
- **URL state SSOT**: 매 query params / path — 매 shareable, refreshable.
|
||||
- **Form state SSOT**: 매 RHF / Formik 의 form object — 매 controlled input 의 derive.
|
||||
- **Global UI state SSOT**: 매 Zustand / Jotai store — 매 cross-component shared.
|
||||
|
||||
### 매 응용
|
||||
1. TanStack Query — server SSOT mirror.
|
||||
2. RSC (React Server Components) — server 의 SSOT 그대로 stream.
|
||||
3. URL-driven state (nuqs, TanStack Router) — 매 shareable SSOT.
|
||||
4. CRDT (Yjs, Automerge) — 매 distributed SSOT.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Server SSOT via TanStack Query
|
||||
```typescript
|
||||
// ❌ duplication — local state mirrors server
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
useEffect(() => { fetchUser().then(setUser); }, []);
|
||||
|
||||
// ✅ SSOT — server is truth, query is cached projection
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
```
|
||||
|
||||
### URL as SSOT (nuqs)
|
||||
```typescript
|
||||
import { useQueryState, parseAsInteger } from 'nuqs';
|
||||
|
||||
function ProductList() {
|
||||
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
|
||||
const [sort, setSort] = useQueryState('sort');
|
||||
// URL ?page=2&sort=price is the truth — refresh-safe, shareable
|
||||
return <List page={page} sort={sort} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Derived state (no duplicate store)
|
||||
```typescript
|
||||
// ❌ duplicating filtered list in state
|
||||
const [items, setItems] = useState(allItems);
|
||||
const [filter, setFilter] = useState('');
|
||||
useEffect(() => { setItems(allItems.filter(i => i.name.includes(filter))); }, [filter]);
|
||||
|
||||
// ✅ derive on render — items isn't state
|
||||
const [filter, setFilter] = useState('');
|
||||
const visible = useMemo(() => allItems.filter(i => i.name.includes(filter)), [filter]);
|
||||
```
|
||||
|
||||
### Form SSOT via RHF
|
||||
```typescript
|
||||
const { register, watch, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: { email: '', plan: 'free' },
|
||||
});
|
||||
const plan = watch('plan'); // derived from form SSOT
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<input {...register('email')} />
|
||||
{plan === 'pro' && <input {...register('teamSize')} />}
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
### Zustand store SSOT
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
type CartStore = {
|
||||
items: CartItem[];
|
||||
add: (item: CartItem) => void;
|
||||
remove: (id: string) => void;
|
||||
};
|
||||
|
||||
export const useCart = create<CartStore>((set) => ({
|
||||
items: [],
|
||||
add: (item) => set((s) => ({ items: [...s.items, item] })),
|
||||
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
|
||||
}));
|
||||
|
||||
// Total derived — not stored
|
||||
const useCartTotal = () => useCart((s) => s.items.reduce((a, i) => a + i.price, 0));
|
||||
```
|
||||
|
||||
### CRDT SSOT (collaborative)
|
||||
```typescript
|
||||
import * as Y from 'yjs';
|
||||
import { WebrtcProvider } from 'y-webrtc';
|
||||
|
||||
const ydoc = new Y.Doc();
|
||||
new WebrtcProvider('room-id', ydoc);
|
||||
const ytext = ydoc.getText('content');
|
||||
|
||||
ytext.observe(() => {
|
||||
// single source — all peers converge
|
||||
render(ytext.toString());
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 데이터 종류 | SSOT location |
|
||||
|---|---|
|
||||
| 영속 entity (user, order) | Backend DB → TanStack Query cache |
|
||||
| Shareable view state | URL params (nuqs) |
|
||||
| Form draft | RHF / Formik form object |
|
||||
| Cross-component UI | Zustand / Jotai |
|
||||
| Component-local | useState (그 component 자체 가 SSOT) |
|
||||
| Collaborative | CRDT (Yjs) |
|
||||
|
||||
**기본값**: 매 server 의 SSOT, 매 client 의 cache projection.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[State Management]]
|
||||
- 변형: [[Event Sourcing]] · [[CQRS]]
|
||||
- 응용: [[React Server Components]] · [[CRDT]]
|
||||
- Adjacent: [[단일 진실 공급원(Single Source of Truth) 구축]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 state architecture design / data flow audit / bug-prone "two stores out of sync" 발견 시.
|
||||
**언제 X**: 매 truly independent UI ephemeral state (hover, focus) — 매 local 이 자체 SSOT.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Mirror state**: 매 server 의 data 를 useState 로 복제. → 매 stale + 두 source 의 drift.
|
||||
- **Multiple stores 의 same field**: Redux + Context + URL 모두 `selectedId` 보유. 매 update 의 race.
|
||||
- **Premature derivation cache**: 매 derived value 를 별도 state 로 저장. 매 useMemo 충분.
|
||||
- **localStorage 의 silent SSOT**: 매 sync 없는 cross-tab — 매 BroadcastChannel 또는 storage event 필요.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Redux docs — "Three Principles", Dan Abramov; Martin Fowler — SSOT).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — SSOT FULL writeup |
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
---
|
||||
id: wiki-2026-0508-단일-페이지-애플리케이션-spa
|
||||
title: 단일 페이지 애플리케이션 (SPA)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [SPA, Single Page Application, 클라이언트 사이드 라우팅]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.93
|
||||
verification_status: applied
|
||||
tags: [frontend, architecture, spa, routing, react]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: React 19 / Vite 6
|
||||
---
|
||||
|
||||
# 단일 페이지 애플리케이션 (SPA)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 single HTML shell 의 매 load 후 매 client-side 의 매 view transition 의 매 처리"**. 매 2010s 의 AngularJS / Backbone 의 매 시작, 매 React / Vue 의 매 mainstream 화, 매 2026 의 SPA 는 매 React 19 / 매 TanStack Router / 매 Vite 6 의 매 stack 의 매 표준. 매 SSR / 매 RSC hybrid 의 매 부상에도 매 dashboard / 매 internal tool / 매 high-interactivity app 의 매 SPA 의 매 적합.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 동작 모델
|
||||
- 매 initial request: 매 single `index.html` + 매 JS bundle.
|
||||
- 매 client router: 매 `history.pushState` 의 매 URL 변경, 매 component 의 매 swap.
|
||||
- 매 data: 매 fetch / 매 GraphQL / 매 RPC 의 매 client 직접 호출.
|
||||
|
||||
### 매 장점 / 매 단점
|
||||
- 장점: 매 빠른 navigation (no full reload), 매 rich interaction, 매 offline 가능.
|
||||
- 단점: 매 initial load 무거움, 매 SEO 취약 (매 SSR / 매 prerender 의 보완 필요), 매 JS 의존.
|
||||
|
||||
### 매 응용
|
||||
1. Admin dashboard.
|
||||
2. SaaS internal tool (Linear, Figma).
|
||||
3. Realtime collaborative app.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Vite 6 + React 19 SPA shell
|
||||
```tsx
|
||||
// main.tsx
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { router } from "./router";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
```
|
||||
|
||||
### TanStack Router file-based routing
|
||||
```tsx
|
||||
// routes/__root.tsx
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
export const Route = createRootRoute({ component: () => <Outlet /> });
|
||||
|
||||
// routes/users/$id.tsx
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
export const Route = createFileRoute("/users/$id")({
|
||||
loader: ({ params }) => fetch(`/api/users/${params.id}`).then(r => r.json()),
|
||||
component: UserDetail,
|
||||
});
|
||||
|
||||
function UserDetail() {
|
||||
const user = Route.useLoaderData();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### Code splitting per route
|
||||
```tsx
|
||||
import { lazy } from "react";
|
||||
const Settings = lazy(() => import("./pages/Settings"));
|
||||
// Vite 자동 chunk 분리 + prefetch
|
||||
```
|
||||
|
||||
### Data fetching (TanStack Query)
|
||||
```tsx
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
function Users() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: () => fetch("/api/users").then(r => r.json()),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
if (isLoading) return <Spinner />;
|
||||
return <UserList users={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Client-side auth guard
|
||||
```tsx
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/_authed")({
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.auth.isAuthenticated) {
|
||||
throw redirect({ to: "/login" });
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Prerender for SEO (vite-plugin-ssr or similar)
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from "vite";
|
||||
import ssr from "vite-plugin-ssr/plugin";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [ssr({ prerender: true })],
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Public marketing | SSR / SSG (Next.js, Astro) — SPA 부적합 |
|
||||
| Admin / dashboard | SPA 적합 |
|
||||
| E-commerce | SSR + island hydration (hybrid) |
|
||||
| Realtime collab | SPA + WebSocket |
|
||||
|
||||
**기본값**: 매 internal high-interactivity tool 은 매 SPA, 매 public 페이지는 매 SSR / 매 hybrid.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- 변형: [[SSR]] · [[SSG]] · [[Islands Architecture]] · [[React Server Components]]
|
||||
- 응용: [[단일 페이지 애플리케이션(SPA) 렌더링 설계]] · [[단일 페이지 애플리케이션(SPA) 아키텍처 설계]]
|
||||
- Adjacent: [[Code Splitting]] · [[Client-Side Routing]] · [[State Management]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 highly interactive / 매 authenticated dashboard / 매 internal tool 의 매 설계 의 권장.
|
||||
**언제 X**: 매 SEO 핵심 페이지 / 매 first-paint 매 critical / 매 low-end device target — 매 SSR 또는 매 hybrid 의 권장.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Public 마케팅 SPA**: SEO / 매 first paint 의 매 부담.
|
||||
- **하나의 거대 bundle**: 매 code splitting 의 X — 매 initial load 의 매 폭증.
|
||||
- **Client-only auth**: 매 sensitive route 의 매 server-side guard 의 매 부재.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 19 / Vite 6 / TanStack Router v1 docs, MDN — History API).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Vite 6 / React 19 / TanStack Router 의 modern SPA stack 정리 |
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: wiki-2026-0508-단일-페이지-애플리케이션-spa-렌더링-설계
|
||||
title: 단일 페이지 애플리케이션(SPA) 렌더링 설계
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: spa-rendering-architecture
|
||||
duplicate_of: "[[SPA Rendering Architecture]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, spa, rendering]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# 단일 페이지 애플리케이션(SPA) 렌더링 설계
|
||||
|
||||
> **이 문서는 [[SPA Rendering Architecture]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약 (specialization aspects)
|
||||
- 매 SPA = 단일 HTML shell + client-side routing + dynamic DOM updates.
|
||||
- 매 CSR vs SSR vs SSG vs ISR vs Streaming SSR — 매 trade-off matrix.
|
||||
- 매 2026 modern: React Server Components, Next.js 15 App Router, Remix v3, SolidStart, TanStack Router.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[React Server Components]]
|
||||
- Adjacent: [[Critical Rendering Path (CRP)|Critical Rendering Path]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
---
|
||||
id: wiki-2026-0508-대수의-법칙-law-of-large-numbers
|
||||
title: 대수의 법칙(Law of Large Numbers)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [LLN, Law of Large Numbers, 큰 수의 법칙]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [statistics, probability, frontend-analytics, ab-testing]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: analytics
|
||||
---
|
||||
|
||||
# 대수의 법칙(Law of Large Numbers)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 sample 수가 커질수록 sample mean 의 expected value 로의 수렴"**. 매 Bernoulli (1713) 의 weak LLN, Kolmogorov (1930) 의 strong LLN. 매 frontend analytics / A/B testing / RUM (Real User Monitoring) 의 통계적 정당성 — 매 sample 적으면 의미 X.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 두 형태
|
||||
- **Weak LLN**: $\bar{X}_n \xrightarrow{P} \mu$ — 매 probability convergence.
|
||||
- **Strong LLN**: $\bar{X}_n \xrightarrow{a.s.} \mu$ — 매 almost sure convergence.
|
||||
- 매 둘 다 finite mean μ 가정.
|
||||
|
||||
### 매 frontend 함의
|
||||
- **A/B test sample size**: 매 N=100 의 noise 지배 — 매 N=10,000+ 필요 (effect size 의 함수).
|
||||
- **Core Web Vitals p75**: 매 RUM 의 "75th percentile" — 매 N>1,000 sessions 권장 (Google).
|
||||
- **Conversion rate stabilization**: 매 daily flux → weekly average 의 수렴.
|
||||
- **Error rate monitoring**: 매 small traffic page 의 false alert.
|
||||
|
||||
### 매 응용
|
||||
1. A/B test power analysis (sample size calculator).
|
||||
2. Web Vitals percentile reliability.
|
||||
3. Recommendation system click-through rate.
|
||||
4. Survival analysis of user retention.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Sample size for A/B test
|
||||
```typescript
|
||||
// Two-proportion z-test, 80% power, α=0.05
|
||||
function abTestSampleSize(
|
||||
baselineRate: number,
|
||||
minDetectableEffect: number,
|
||||
): number {
|
||||
const p1 = baselineRate;
|
||||
const p2 = baselineRate + minDetectableEffect;
|
||||
const pBar = (p1 + p2) / 2;
|
||||
const z_alpha = 1.96; // two-sided 0.05
|
||||
const z_beta = 0.84; // power 0.80
|
||||
const numerator =
|
||||
Math.pow(z_alpha * Math.sqrt(2 * pBar * (1 - pBar)) +
|
||||
z_beta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)), 2);
|
||||
return Math.ceil(numerator / Math.pow(p2 - p1, 2));
|
||||
}
|
||||
|
||||
// Baseline 5% conversion, want to detect +1 percentage point lift
|
||||
console.log(abTestSampleSize(0.05, 0.01)); // ~3,000 per arm
|
||||
```
|
||||
|
||||
### Running mean (LLN visualizer)
|
||||
```typescript
|
||||
function* runningMean(samples: Iterable<number>) {
|
||||
let n = 0;
|
||||
let mean = 0;
|
||||
for (const x of samples) {
|
||||
n += 1;
|
||||
mean += (x - mean) / n; // Welford
|
||||
yield { n, mean };
|
||||
}
|
||||
}
|
||||
|
||||
// Coin flip (true mean = 0.5)
|
||||
const flips = Array.from({ length: 10000 }, () => (Math.random() < 0.5 ? 1 : 0));
|
||||
for (const { n, mean } of runningMean(flips)) {
|
||||
if (n % 1000 === 0) console.log(`n=${n}, mean=${mean.toFixed(4)}`);
|
||||
}
|
||||
// n=1000 mean ≈ 0.49
|
||||
// n=10000 mean ≈ 0.50 (LLN convergence)
|
||||
```
|
||||
|
||||
### Web Vitals percentile reliability check
|
||||
```typescript
|
||||
import { onLCP } from 'web-vitals';
|
||||
|
||||
const lcpSamples: number[] = [];
|
||||
onLCP((metric) => {
|
||||
lcpSamples.push(metric.value);
|
||||
if (lcpSamples.length >= 1000) {
|
||||
const sorted = [...lcpSamples].sort((a, b) => a - b);
|
||||
const p75 = sorted[Math.floor(sorted.length * 0.75)];
|
||||
sendBeacon({ p75, n: lcpSamples.length });
|
||||
}
|
||||
});
|
||||
// p75 trustworthy only after N>1,000 (Google CrUX guidance)
|
||||
```
|
||||
|
||||
### Bayesian early-stopping (avoid LLN trap)
|
||||
```typescript
|
||||
// Don't peek at A/B test before sample size reached!
|
||||
function shouldStop(arm: { successes: number; trials: number }, target: number) {
|
||||
if (arm.trials < target) return false;
|
||||
// proceed to analysis
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Bootstrap confidence interval
|
||||
```typescript
|
||||
function bootstrapCI(samples: number[], B = 10000, alpha = 0.05) {
|
||||
const means: number[] = [];
|
||||
for (let b = 0; b < B; b++) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
sum += samples[Math.floor(Math.random() * samples.length)];
|
||||
}
|
||||
means.push(sum / samples.length);
|
||||
}
|
||||
means.sort((a, b) => a - b);
|
||||
return [
|
||||
means[Math.floor(B * (alpha / 2))],
|
||||
means[Math.floor(B * (1 - alpha / 2))],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Sample size guideline |
|
||||
|---|---|
|
||||
| Web Vitals p75 (Google CrUX) | N > 1,000 sessions per page |
|
||||
| A/B test (5% baseline, 1pp lift) | ~3,000 per arm |
|
||||
| Click-through rate stabilization | N > 10,000 impressions |
|
||||
| Error rate monitoring (rare events) | Apply Poisson, not LLN naively |
|
||||
|
||||
**기본값**: 매 결과 보고 전 N≥1,000 — 매 LLN safety zone.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Probability Theory]] · [[Statistical Inference]]
|
||||
- 응용: [[Core Web Vitals Optimization (INP, LCP 개선)|Core Web Vitals]]
|
||||
- Adjacent: [[Monte Carlo Methods]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 sample size 결정 / 매 metric 의 reliability 의 statistical 정당화 / 매 small-N false-positive 의 진단.
|
||||
**언제 X**: 매 비-i.i.d. data (autocorrelated time series) — 매 LLN naive 적용 X. 매 stationarity 확인.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Peeking at A/B test**: 매 N=50 에서 "winner" 선언 — 매 LLN 미달 + multiple testing.
|
||||
- **Rare event LLN**: 매 0.01% conversion → 매 N=1000 의 평균 0 가능. 매 Poisson 필요.
|
||||
- **Heavy-tail distribution**: 매 Cauchy (no finite mean) — 매 LLN 미적용.
|
||||
- **Selection bias**: 매 sample 이 random 이 X — 매 N 무관 의 biased estimate.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Kolmogorov, "Foundations of Probability"; Google web.dev — Web Vitals reporting).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — LLN with frontend analytics applications |
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
---
|
||||
id: wiki-2026-0508-디지털-트윈-및-데이터-시뮬레이션
|
||||
title: 디지털 트윈 및 데이터 시뮬레이션
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Digital Twin, IoT Visualization, Real-time Simulation]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [frontend, digital-twin, threejs, websocket, visualization]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-three-fiber
|
||||
---
|
||||
|
||||
# 디지털 트윈 및 데이터 시뮬레이션
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 물리 자산의 live mirror"**. 공장 / 빌딩 / 차량을 3D로 render + 실시간 sensor stream 으로 동기화. 2026 stack: React Three Fiber + WebSocket / WebTransport + WebGPU compute + Cesium (geo).
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 디지털 트윈이란
|
||||
- 물리 system 의 software replica — sensor 로 state sync, simulation 으로 future predict.
|
||||
- 4 단계: Descriptive (현재 시각화) → Diagnostic (anomaly detect) → Predictive (forecast) → Prescriptive (optimize).
|
||||
- Use case: smart factory, BIM, fleet management, energy grid.
|
||||
|
||||
### 매 frontend 책임
|
||||
- **3D rendering**: 자산 model (glTF/USD) load + scene graph.
|
||||
- **Real-time stream**: WebSocket / SSE / WebTransport 으로 sensor data.
|
||||
- **Time-series viz**: 매 chart + 3D overlay (heatmap, particles).
|
||||
- **Interaction**: select asset → 매 detail panel + control commands.
|
||||
|
||||
### 매 응용
|
||||
1. 공장 라인 모니터링 (machine health).
|
||||
2. 빌딩 BIM + HVAC sensor.
|
||||
3. Fleet tracking (차량 GPS + telematics).
|
||||
4. Energy grid load visualization.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### React Three Fiber + glTF asset
|
||||
```tsx
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { useGLTF, OrbitControls } from '@react-three/drei';
|
||||
|
||||
function Factory() {
|
||||
const { scene } = useGLTF('/models/factory.glb');
|
||||
return <primitive object={scene} />;
|
||||
}
|
||||
|
||||
export function Twin() {
|
||||
return (
|
||||
<Canvas camera={{ position: [10, 10, 10] }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 10, 5]} />
|
||||
<Factory />
|
||||
<SensorOverlay />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket sensor stream → state
|
||||
```tsx
|
||||
import { create } from 'zustand';
|
||||
|
||||
const useSensors = create<{ sensors: Record<string, Sensor> }>(() => ({
|
||||
sensors: {},
|
||||
}));
|
||||
|
||||
function useSensorStream(url: string) {
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket(url);
|
||||
ws.onmessage = e => {
|
||||
const { id, value, timestamp } = JSON.parse(e.data);
|
||||
useSensors.setState(s => ({
|
||||
sensors: { ...s.sensors, [id]: { value, timestamp } },
|
||||
}));
|
||||
};
|
||||
return () => ws.close();
|
||||
}, [url]);
|
||||
}
|
||||
```
|
||||
|
||||
### Sensor heatmap on 3D mesh
|
||||
```tsx
|
||||
function SensorOverlay() {
|
||||
const sensors = useSensors(s => s.sensors);
|
||||
return (
|
||||
<>
|
||||
{Object.entries(sensors).map(([id, sensor]) => (
|
||||
<mesh key={id} position={sensor.position}>
|
||||
<sphereGeometry args={[0.2, 16, 16]} />
|
||||
<meshStandardMaterial
|
||||
color={tempToColor(sensor.value)}
|
||||
emissive={tempToColor(sensor.value)}
|
||||
emissiveIntensity={0.5}
|
||||
/>
|
||||
</mesh>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function tempToColor(t: number) {
|
||||
// 매 cold blue → hot red
|
||||
const h = (1 - Math.min(t / 100, 1)) * 240;
|
||||
return `hsl(${h}, 100%, 50%)`;
|
||||
}
|
||||
```
|
||||
|
||||
### Time-series chart + 3D selection sync
|
||||
```tsx
|
||||
const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
|
||||
|
||||
<Asset onClick={() => setSelectedAsset('pump-3')} />
|
||||
{selectedAsset && (
|
||||
<DetailPanel>
|
||||
<TimeSeriesChart sensorId={selectedAsset} />
|
||||
</DetailPanel>
|
||||
)}
|
||||
```
|
||||
|
||||
### WebGPU compute for particle simulation
|
||||
```ts
|
||||
// 매 air flow / thermal simulation 100k particles.
|
||||
const computeShader = `
|
||||
@group(0) @binding(0) var<storage, read_write> particles: array<vec4f>;
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) id: vec3u) {
|
||||
let i = id.x;
|
||||
particles[i].xyz += particles[i].xyz * 0.01; // 매 velocity update
|
||||
}`;
|
||||
```
|
||||
|
||||
### Cesium for geo-scale twin
|
||||
```tsx
|
||||
import { Viewer, Entity } from 'resium';
|
||||
|
||||
<Viewer full>
|
||||
{fleet.map(vehicle => (
|
||||
<Entity
|
||||
key={vehicle.id}
|
||||
position={Cartesian3.fromDegrees(vehicle.lon, vehicle.lat, vehicle.alt)}
|
||||
model={{ uri: '/models/truck.glb', scale: 1.0 }}
|
||||
/>
|
||||
))}
|
||||
</Viewer>
|
||||
```
|
||||
|
||||
### Anomaly detection (client-side)
|
||||
```ts
|
||||
function detectAnomaly(values: number[]): boolean {
|
||||
const mean = values.reduce((a, b) => a + b) / values.length;
|
||||
const std = Math.sqrt(values.reduce((s, v) => s + (v - mean) ** 2, 0) / values.length);
|
||||
const last = values[values.length - 1];
|
||||
return Math.abs(last - mean) > 3 * std; // 매 3-sigma
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Indoor (factory/building) | React Three Fiber + glTF. |
|
||||
| Outdoor / geo-scale | Cesium / MapLibre 3D. |
|
||||
| < 1k sensors | WebSocket + zustand. |
|
||||
| 100k+ data points | WebGPU compute + instanced mesh. |
|
||||
| Forecasting | server-side (TimeGPT / Prophet) → push results. |
|
||||
| Safety-critical | unidirectional viz only — control via separate verified channel. |
|
||||
|
||||
**기본값**: R3F + WebSocket + zustand + recharts. WebGPU 는 particle/heatmap 만.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[클라우드_인프라_및_IaC_운영_표준|IoT]]
|
||||
- 변형: [[GIS]]
|
||||
- Adjacent: [[WebGPU]] · [[Three.js]] · [[Cesium]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 물리 system live mirror, 감독자 dashboard, what-if simulation.
|
||||
**언제 X**: static infographic, 매 control loop (latency-critical 은 PLC/edge).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **High-poly raw model**: 100M tri 의 CAD model 그대로 load → 매 GPU 죽음. Decimate → 100k tri.
|
||||
- **Per-sensor mesh**: 10k sensor 의 sphere 개별 mesh → 매 instanced mesh 사용.
|
||||
- **Polling**: 매 1초마다 REST GET → WebSocket / SSE.
|
||||
- **Control via twin UI**: viz 와 control 분리. 매 safety-critical 명령은 verified channel.
|
||||
- **No level of detail**: 멀리 있는 asset 도 풀 detail — LOD 필수.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (NIST digital twin definition, R3F docs, Cesium docs, WebGPU spec).
|
||||
- 신뢰도 A-.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — R3F + WebSocket + WebGPU stack |
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
---
|
||||
id: wiki-2026-0508-렌더링-블로킹-방지를-위한-css-분할-및-로딩-최적화
|
||||
title: 렌더링 블로킹 방지를 위한 CSS 분할 및 로딩 최적화
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Critical CSS, CSS Code Splitting, Render-blocking CSS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.95
|
||||
verification_status: applied
|
||||
tags: [frontend, css, performance, critical-css, lcp]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: css
|
||||
framework: vite
|
||||
---
|
||||
|
||||
# 렌더링 블로킹 방지를 위한 CSS 분할 및 로딩 최적화
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CSS는 default 로 render-blocking"**. `<link rel="stylesheet">` 가 도착할 때까지 브라우저는 paint 안 함. Critical CSS inline + non-critical async load + route-based split → LCP 50%+ 개선. 2026 표준은 Vite/Rollup chunk + `media="print"` trick.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 왜 CSS 가 blocking 인가
|
||||
- HTML parser 가 `<link rel="stylesheet">` 만나면 CSSOM 만들 때까지 paint X.
|
||||
- CSSOM 없이는 element 의 final style 알 수 없음 → FOUC 방지 위해 block.
|
||||
- 1 round-trip (HTTP) + parse + match = 100-500ms LCP cost.
|
||||
|
||||
### 매 분할 전략
|
||||
- **Critical CSS**: above-the-fold 의 minimal CSS — `<style>` 으로 inline.
|
||||
- **Async load**: 나머지 CSS 는 non-blocking — `media="print"` swap trick / `rel="preload"`.
|
||||
- **Route-based split**: 각 route 마다 별도 CSS chunk — 안 쓰는 CSS 안 download.
|
||||
- **Component-scoped**: CSS-in-JS / CSS Modules → 자동 split.
|
||||
|
||||
### 매 응용
|
||||
1. Marketing landing (LCP 핵심).
|
||||
2. SSR app (FOUC 방지 critical inline).
|
||||
3. SPA (route chunk).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Critical CSS inline (manual)
|
||||
```html
|
||||
<head>
|
||||
<style>
|
||||
/* 매 above-the-fold only — header, hero, font-face */
|
||||
body { font-family: system-ui; margin: 0; }
|
||||
.hero { height: 80vh; background: #111; color: #fff; }
|
||||
</style>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/css/main.css"
|
||||
as="style"
|
||||
onload="this.onload=null;this.rel='stylesheet'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="/css/main.css" /></noscript>
|
||||
</head>
|
||||
```
|
||||
|
||||
### Critical CSS extraction (Vite plugin)
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
import critical from 'rollup-plugin-critical';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
critical({
|
||||
criticalUrl: 'https://example.com/',
|
||||
criticalBase: './dist/',
|
||||
criticalPages: [{ uri: '', template: 'index' }],
|
||||
criticalConfig: { inline: true, dimensions: [{ width: 375, height: 812 }, { width: 1920, height: 1080 }] },
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### print media swap trick (no JS)
|
||||
```html
|
||||
<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'" />
|
||||
```
|
||||
브라우저가 `media="print"` 를 non-blocking 으로 download → onload 시 `all` 로 swap.
|
||||
|
||||
### Route-based split (Next.js)
|
||||
```tsx
|
||||
// app/products/page.tsx — 매 자동으로 별도 CSS chunk.
|
||||
import './products.css'; // 매 only loaded on /products.
|
||||
```
|
||||
|
||||
### CSS Modules (component-scoped)
|
||||
```tsx
|
||||
import styles from './Button.module.css';
|
||||
<button className={styles.primary}>Click</button>;
|
||||
// 매 build 시 hash → only used styles bundled.
|
||||
```
|
||||
|
||||
### Tailwind v4 (oxide engine, 2026)
|
||||
```css
|
||||
/* main.css */
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: oklch(0.7 0.2 250);
|
||||
}
|
||||
```
|
||||
PurgeCSS 자동 — 매 unused class 제거. 최종 bundle 5-10KB.
|
||||
|
||||
### Preload key fonts + CSS together
|
||||
```html
|
||||
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="/css/main.css" as="style" />
|
||||
```
|
||||
|
||||
### CSS containment for paint isolation
|
||||
```css
|
||||
.card {
|
||||
contain: content; /* 매 layout + paint + style 격리 */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 200px;
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP/2 push 대체 — 103 Early Hints
|
||||
```http
|
||||
HTTP/1.1 103 Early Hints
|
||||
Link: </css/main.css>; rel=preload; as=style
|
||||
Link: </js/app.js>; rel=preload; as=script
|
||||
```
|
||||
Cloudflare/Vercel 자동 지원 — TTFB 안 기다리고 preload 시작.
|
||||
|
||||
### Detect unused CSS (Chrome DevTools)
|
||||
```bash
|
||||
# 매 Coverage tab → Record → reload → unused % 확인.
|
||||
# 매 80%+ unused = split 필요.
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Marketing landing | Critical inline + async rest. |
|
||||
| SPA | Route-based split + CSS Modules. |
|
||||
| SSR (Next/Remix) | 매 자동 split — Tailwind v4 면 충분. |
|
||||
| Component lib | CSS-in-JS 또는 CSS Modules — tree-shake. |
|
||||
| Legacy multi-page | print media trick + critical extraction. |
|
||||
|
||||
**기본값**: Tailwind v4 + Vite/Next 자동 chunk + `<link rel="preload">` for above-fold.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]] · [[Core Web Vitals Optimization (INP, LCP 개선)|Core Web Vitals]]
|
||||
- 변형: [[Critical CSS]] · [[CSS_Architecture_and_Styling|CSS-in-JS]] · [[CSS Modules]]
|
||||
- 응용: [[Tailwind v4]] · [[Vite]] · [[Next.js]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: LCP > 2.5s, CSS bundle > 100KB, FOUC 발생.
|
||||
**언제 X**: 매 internal tool (LCP 무관), 매 tiny site (single CSS file 충분).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Single mega CSS**: 매 1MB CSS load — purge + split.
|
||||
- **`@import` chain**: serial load — bundle 로 합치거나 `<link>` 직접.
|
||||
- **Inline ALL CSS**: HTML 비대 → critical only.
|
||||
- **No font-display**: FOIT (invisible text) → `font-display: swap`.
|
||||
- **CSS in `<body>`**: legacy hack 매 — `<head>` 에서 preload.
|
||||
- **Unused frameworks**: Bootstrap full 250KB 인데 button 만 사용.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev/extract-critical-css, MDN preload, Vite docs, Tailwind v4 docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — critical CSS + Tailwind v4 + Early Hints |
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
---
|
||||
id: wiki-2026-0508-렌더링-차단-리소스-render-blocking-resou
|
||||
title: 렌더링 차단 리소스 (Render-blocking Resources)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Render Blocking, Critical Resources, Critical Rendering Path]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [performance, web-vitals, rendering, frontend]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: html
|
||||
framework: browser
|
||||
---
|
||||
|
||||
# 렌더링 차단 리소스 (Render-blocking Resources)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 render-blocking = First Paint 까지 매 다운로드/parse 필수 리소스"**. 매 head 안 sync `<script>` + 매 default `<link rel="stylesheet">` — 매 둘 다 차단. 매 2026 modern = 매 critical CSS inline + `defer/async` script + `media` query split + 매 LCP 최적화.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 차단 리소스
|
||||
- `<script src>` (no `defer`/`async`) — 매 HTML parse 차단.
|
||||
- `<link rel="stylesheet">` — 매 render 차단 (parse 차단 의 X).
|
||||
- `@import` in CSS — 매 추가 차단.
|
||||
- Web fonts — 매 FOIT/FOUT 매 paint 지연.
|
||||
|
||||
### 매 비차단 만들기
|
||||
- `<script defer>` — DOMContentLoaded 직전 실행, parse 차단 X.
|
||||
- `<script async>` — 다운로드 즉시 실행, parse 차단 가능.
|
||||
- `<link rel="preload">` — early discovery.
|
||||
- `media` attribute — 매 print/non-matching media — 매 비차단.
|
||||
- Critical CSS inline + 매 rest async load.
|
||||
|
||||
### 매 영향 metric
|
||||
- **FCP** (First Contentful Paint).
|
||||
- **LCP** (Largest Contentful Paint).
|
||||
- **TBT** (Total Blocking Time).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Defer vs async
|
||||
```html
|
||||
<!-- 매 GOOD — 매 page-essential, parse 비차단 -->
|
||||
<script defer src="/app.js"></script>
|
||||
|
||||
<!-- 매 GOOD — 매 independent (analytics) -->
|
||||
<script async src="https://analytics.example.com/track.js"></script>
|
||||
|
||||
<!-- 매 BAD — 매 parse 차단 -->
|
||||
<script src="/app.js"></script>
|
||||
```
|
||||
|
||||
### Critical CSS inline
|
||||
```html
|
||||
<head>
|
||||
<style>
|
||||
/* 매 above-the-fold 만 — 매 14KB 이하 — 매 1RTT */
|
||||
body { margin: 0; font-family: system-ui; }
|
||||
.hero { min-height: 60vh; background: #111; color: #fff; }
|
||||
</style>
|
||||
<link rel="preload" href="/main.css" as="style"
|
||||
onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="/main.css"></noscript>
|
||||
</head>
|
||||
```
|
||||
|
||||
### media query split
|
||||
```html
|
||||
<!-- 매 print — 매 즉시 비차단 -->
|
||||
<link rel="stylesheet" href="print.css" media="print">
|
||||
|
||||
<!-- 매 wide screen 만 — small 매 비차단 -->
|
||||
<link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">
|
||||
```
|
||||
|
||||
### Preload critical font
|
||||
```html
|
||||
<link rel="preload" href="/fonts/Inter.var.woff2" as="font"
|
||||
type="font/woff2" crossorigin>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/Inter.var.woff2') format('woff2-variations');
|
||||
font-display: swap; /* 매 FOUT — 매 paint 차단 X */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Modulepreload (ES modules)
|
||||
```html
|
||||
<link rel="modulepreload" href="/app.js">
|
||||
<script type="module" src="/app.js"></script>
|
||||
```
|
||||
|
||||
### Resource hints
|
||||
```html
|
||||
<link rel="preconnect" href="https://api.example.com">
|
||||
<link rel="dns-prefetch" href="https://cdn.example.com">
|
||||
```
|
||||
|
||||
### Speculation Rules (Chrome 121+)
|
||||
```html
|
||||
<script type="speculationrules">
|
||||
{
|
||||
"prerender": [{ "where": { "href_matches": "/article/*" }, "eagerness": "moderate" }]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Fetch priority
|
||||
```html
|
||||
<img src="hero.webp" fetchpriority="high" alt="...">
|
||||
<script src="non-critical.js" defer fetchpriority="low"></script>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 리소스 | 처리 |
|
||||
|---|---|
|
||||
| Critical CSS (above fold) | inline `<style>` |
|
||||
| Non-critical CSS | preload + onload swap, OR media query split |
|
||||
| App JS (DOM-dependent) | `defer` |
|
||||
| 3rd-party analytics | `async` |
|
||||
| Web font (critical) | preload + `font-display: swap` |
|
||||
| LCP image | `fetchpriority="high"` |
|
||||
| Below-fold image | `loading="lazy"` |
|
||||
|
||||
**기본값**: Critical CSS inline (≤14KB), 매 script `defer`, font preload + swap, LCP image high priority.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]] · [[Critical Rendering Path (CRP)|Critical Rendering Path]]
|
||||
- 변형: [[Critical CSS]]
|
||||
- 응용: [[Core Web Vitals Optimization (INP, LCP 개선)|Core Web Vitals]]
|
||||
- Adjacent: [[Speculation Rules]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: Lighthouse audit 분석, "render blocking" 경고 해결, critical CSS 추출.
|
||||
**언제 X**: 매 specific framework SSR config — 매 framework docs 매 더 정확.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 CSS head sync**: 매 critical 만 inline.
|
||||
- **`<script>` head sync**: 매 `defer` 의 사용.
|
||||
- **`@import` chain**: 매 sequential 차단 — 매 `<link>` 의 사용.
|
||||
- **font-display: block** (default): 매 FOIT 매 3s — 매 `swap`.
|
||||
- **preload 남발**: 매 5개+ — 매 priority 무의미 — 매 critical 만.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (web.dev, MDN Performance, Lighthouse audits, Chrome DevRel).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — defer/async + critical CSS + preload |
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
---
|
||||
id: wiki-2026-0508-모바일-퍼스트-mobile-first
|
||||
title: 모바일 퍼스트(Mobile-First)
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Mobile-First, 모바일 우선, Mobile First Design, 모바일 우선주의]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, responsive, css, mobile, design]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: css
|
||||
framework: tailwind
|
||||
---
|
||||
|
||||
# 모바일 퍼스트(Mobile-First)
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 작은 화면부터 design — 큰 화면 의 progressive enhancement"**. Luke Wroblewski (2009)가 제창, smallest viewport 기준 baseline CSS, larger breakpoint 의 `min-width` media query 의 progressive enhancement. 매 2026 의 mobile traffic 60%+ 환경의 default approach.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 vs Desktop-first
|
||||
| | Mobile-first | Desktop-first |
|
||||
|---|---|---|
|
||||
| Baseline | smallest viewport | largest viewport |
|
||||
| Media query | `min-width` (up) | `max-width` (down) |
|
||||
| Mindset | progressive enhancement | graceful degradation |
|
||||
| Default 2026 | ✓ | (legacy) |
|
||||
|
||||
### 매 3원칙 (Wroblewski)
|
||||
1. **Constraints force focus** — small screen 매 essential content prioritize.
|
||||
2. **Capabilities expand** — touch, GPS, camera 의 leverage.
|
||||
3. **Progressive enhancement** — base 의 small + add 의 large.
|
||||
|
||||
### 매 응용
|
||||
1. **Performance budget** — mobile network slow → minimal payload first.
|
||||
2. **Touch-first UX** — 44×44px tap target (Apple HIG), 48×48dp (Material).
|
||||
3. **Content hierarchy** — most important top, fold concept abandoned.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. CSS mobile-first media query
|
||||
```css
|
||||
/* base — mobile (< 640px implicit) */
|
||||
.container {
|
||||
padding: 1rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* tablet ≥ 640px */
|
||||
@media (min-width: 640px) {
|
||||
.container { padding: 2rem; font-size: 16px; }
|
||||
.grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* desktop ≥ 1024px */
|
||||
@media (min-width: 1024px) {
|
||||
.grid { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Tailwind mobile-first utilities
|
||||
```tsx
|
||||
// base 의 mobile, sm:/md:/lg: 의 enhancement
|
||||
export function Card() {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<h2 className="text-xl sm:text-2xl lg:text-3xl">Title</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Item />
|
||||
<Item />
|
||||
<Item />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Container queries (modern 2026)
|
||||
```css
|
||||
.card-container {
|
||||
container-type: inline-size;
|
||||
container-name: card;
|
||||
}
|
||||
|
||||
.card { display: flex; flex-direction: column; }
|
||||
|
||||
@container card (min-width: 400px) {
|
||||
.card { flex-direction: row; }
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Responsive image (srcset)
|
||||
```html
|
||||
<img
|
||||
src="hero-mobile.webp"
|
||||
srcset="hero-mobile.webp 480w, hero-tablet.webp 1024w, hero-desktop.webp 1920w"
|
||||
sizes="(min-width: 1024px) 1920px, (min-width: 640px) 1024px, 100vw"
|
||||
alt="Hero"
|
||||
loading="lazy"
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. Viewport meta + safe area
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
```
|
||||
```css
|
||||
.app {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Touch-friendly tap targets
|
||||
```css
|
||||
button, a {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0.75rem 1rem;
|
||||
/* Apple HIG: 44×44pt minimum */
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Performance: critical CSS inline
|
||||
```html
|
||||
<head>
|
||||
<style>/* critical above-fold CSS, < 14KB */</style>
|
||||
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'">
|
||||
</head>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New project 2026 | mobile-first default. |
|
||||
| Legacy desktop site | gradual migration via container queries. |
|
||||
| Internal tool (desktop only) | desktop-first 도 OK 매 — but mobile-first 매 future-proof. |
|
||||
| Component library | container queries > viewport media queries (component reusability). |
|
||||
|
||||
**기본값**: Tailwind mobile-first utilities + container queries 매 component-level.
|
||||
|
||||
## 🔗 Graph
|
||||
- 변형: [[컨테이너 쿼리 (Container Queries)]]
|
||||
- 응용: [[CSS_Architecture_and_Styling|Tailwind CSS]]
|
||||
- Adjacent: [[Core Web Vitals Optimization (INP, LCP 개선)|Core Web Vitals]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 새 web project, e-commerce, content site, public-facing app, PWA.
|
||||
**언제 X**: desktop-only enterprise tool 의 absolute desktop-first 가 효율적인 case (rare).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`max-width` 만 의 의존**: 매 reverse mobile-first defeats purpose.
|
||||
- **고정 px 의 layout**: rem/em/% 의 use.
|
||||
- **Hover-only interactions**: touch 의 hover 없음 — tap fallback 필수.
|
||||
- **Fold obsession**: 매 mobile 의 scrolling natural — fold 없음.
|
||||
- **Tap target < 44px**: thumb miss-tap.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Wroblewski "Mobile First" 2011, Google Web.dev 2026, MDN responsive design).
|
||||
- 신뢰도 A.
|
||||
- Canonical 매 [[모바일 퍼스트(Mobile-First)]] — 매 [[모바일 우선주의 (Mobile-First) 디자인]], [[모바일 퍼스트 및 다양한 디바이스 환경을 위한 반응형 레이아웃 구축]] redirect 매 here.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — canonical full content + 7 patterns (CSS, Tailwind, container queries) |
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: wiki-2026-0508-비동기-데이터-관리
|
||||
title: 비동기 데이터 관리
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Async Data Management, Server State, Data Fetching]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.92
|
||||
verification_status: applied
|
||||
tags: [frontend, async, react-query, swr, server-state]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-query
|
||||
---
|
||||
|
||||
# 비동기 데이터 관리
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 server state 의 client cache 의 separation"**. 매 fetch / cache / sync / invalidate / retry / dedupe 의 7 concerns 의 library 의 delegation — 매 2026 의 TanStack Query (v5) / SWR / RTK Query 의 standard. Custom `useEffect + fetch` 의 anti-pattern.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 server vs client state
|
||||
- **client state**: form input, modal open/close, theme — local, ephemeral. Zustand/useState.
|
||||
- **server state**: 매 remote source of truth — async, stale, shared, cached. TanStack Query.
|
||||
|
||||
### 매 7 concerns
|
||||
1. **Fetch**: HTTP request + abort.
|
||||
2. **Cache**: key-based store.
|
||||
3. **Dedupe**: 매 simultaneous request 의 share.
|
||||
4. **Stale**: time-based freshness.
|
||||
5. **Background refetch**: window focus, reconnect.
|
||||
6. **Retry**: exponential backoff.
|
||||
7. **Invalidate**: mutation 후 refetch.
|
||||
|
||||
### 매 query lifecycle
|
||||
```
|
||||
fetching → fresh (staleTime) → stale → refetch → fresh
|
||||
↓
|
||||
gcTime → garbage collect
|
||||
```
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### TanStack Query v5 (React)
|
||||
```tsx
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
function UserProfile({ id }: { id: string }) {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['user', id],
|
||||
queryFn: ({ signal }) => fetch(`/api/users/${id}`, { signal }).then(r => r.json()),
|
||||
staleTime: 60_000, // 1min fresh
|
||||
gcTime: 5 * 60_000, // 5min cache
|
||||
});
|
||||
|
||||
if (isLoading) return <Skeleton />;
|
||||
if (error) return <Error error={error} />;
|
||||
return <Profile user={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Mutation + invalidation
|
||||
```tsx
|
||||
function useUpdateUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (user: User) =>
|
||||
fetch(`/api/users/${user.id}`, {
|
||||
method: 'PUT', body: JSON.stringify(user),
|
||||
}).then(r => r.json()),
|
||||
onSuccess: (_, user) => {
|
||||
qc.invalidateQueries({ queryKey: ['user', user.id] });
|
||||
qc.invalidateQueries({ queryKey: ['users'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Optimistic update
|
||||
```tsx
|
||||
useMutation({
|
||||
mutationFn: toggleTodo,
|
||||
onMutate: async (id) => {
|
||||
await qc.cancelQueries({ queryKey: ['todos'] });
|
||||
const prev = qc.getQueryData(['todos']);
|
||||
qc.setQueryData(['todos'], (old: Todo[]) =>
|
||||
old.map(t => t.id === id ? { ...t, done: !t.done } : t));
|
||||
return { prev };
|
||||
},
|
||||
onError: (_, __, ctx) => qc.setQueryData(['todos'], ctx?.prev),
|
||||
onSettled: () => qc.invalidateQueries({ queryKey: ['todos'] }),
|
||||
});
|
||||
```
|
||||
|
||||
### Infinite scroll
|
||||
```tsx
|
||||
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||
queryKey: ['posts'],
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
|
||||
getNextPageParam: (last) => last.nextCursor,
|
||||
initialPageParam: 0,
|
||||
});
|
||||
```
|
||||
|
||||
### Suspense mode (React 19)
|
||||
```tsx
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', id],
|
||||
queryFn: fetchUser,
|
||||
});
|
||||
// data 의 always defined — Suspense boundary 의 loading 의 handle
|
||||
```
|
||||
|
||||
### SWR (lightweight alternative)
|
||||
```tsx
|
||||
import useSWR from 'swr';
|
||||
|
||||
const { data, error, mutate } = useSWR(
|
||||
`/api/users/${id}`,
|
||||
(url) => fetch(url).then(r => r.json()),
|
||||
{ revalidateOnFocus: true, dedupingInterval: 2000 }
|
||||
);
|
||||
```
|
||||
|
||||
### Server Components data (Next.js 15 / RSC)
|
||||
```tsx
|
||||
// app/users/[id]/page.tsx — runs on server
|
||||
async function UserPage({ params }: { params: { id: string } }) {
|
||||
const user = await fetch(`https://api/users/${params.id}`, {
|
||||
next: { revalidate: 60, tags: [`user-${params.id}`] }
|
||||
}).then(r => r.json());
|
||||
return <Profile user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| React app | TanStack Query v5 |
|
||||
| Next.js App Router | RSC fetch + Server Actions + tag invalidation |
|
||||
| Redux app | RTK Query (Redux 의 통합) |
|
||||
| 매 minimal bundle | SWR (~5KB) |
|
||||
| 매 GraphQL | Apollo Client / urql |
|
||||
| 매 simple form fetch | native `fetch` + `useState` 의 OK |
|
||||
|
||||
**기본값**: 매 React + REST → TanStack Query, 매 Next.js → RSC fetch.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[State Management]]
|
||||
- 응용: [[Optimistic UI]] · [[Infinite Scroll]] · [[React Server Components]]
|
||||
- Adjacent: [[AbortController]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: cache key design 의 review, invalidation strategy 의 plan, race condition 의 debug.
|
||||
**언제 X**: real-time data (WebSocket/SSE 의 substitute).
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`useEffect + fetch`**: 매 race condition, 매 no dedup, 매 no cache — 매 library 의 use.
|
||||
- **Global Redux 에 server state**: 매 manual cache management — 매 RTK Query 의 use.
|
||||
- **Polling 의 abuse**: 매 SSE/WebSocket 의 substitute.
|
||||
- **`enabled: !!id` 누락**: 매 undefined 의 fetch — false positive.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (TanStack Query v5 docs, Vercel SWR docs, Next.js 15 data fetching).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — TanStack Query v5 + RSC + optimistic update 의 정리 |
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: wiki-2026-0508-유지보수-가능한-대규모-프론트엔드-css-설계
|
||||
title: 유지보수 가능한 대규모 프론트엔드 CSS 설계
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Scalable CSS Architecture, CSS at Scale, Design System CSS]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, css, architecture, design-system]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: css
|
||||
framework: tailwind-css
|
||||
---
|
||||
|
||||
# 유지보수 가능한 대규모 프론트엔드 CSS 설계
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 CSS 는 매 N 명 의 개발자 가 매 6개월 후 의 코드 base 에서 매 두려움 없이 수정할 수 있어야 한다"**. 매 대규모 CSS 의 적은 매 specificity 폭주, 매 dead code, 매 inconsistent spacing. 매 2026 의 정답 — utility-first (Tailwind 4) + design tokens + CSS Layers + container queries.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 4 개 의 추상 레벨
|
||||
- **Tokens**: color, spacing, typography 의 매 raw value (CSS custom property).
|
||||
- **Primitives**: Box, Stack, Grid 의 매 layout primitive.
|
||||
- **Components**: Button, Card, Modal — 매 design system 의 unit.
|
||||
- **Patterns**: Page-level 조합.
|
||||
|
||||
### 매 방법론 비교
|
||||
- **BEM**: `.block__element--modifier`. 매 명시적이지만 매 verbose.
|
||||
- **CSS Modules**: 매 file-scoped, 매 collision 없음.
|
||||
- **CSS-in-JS**: styled-components, Emotion — 매 runtime cost.
|
||||
- **Utility-first (Tailwind)**: 매 2026 의 default — 매 zero runtime, JIT.
|
||||
- **Vanilla Extract / Panda**: 매 typed CSS, build-time.
|
||||
|
||||
### 매 응용
|
||||
1. Design System 구축 (Material, Ant, Chakra).
|
||||
2. Multi-brand whitelabel (token swap).
|
||||
3. Dark mode / theming.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Design tokens (CSS Custom Properties)
|
||||
```css
|
||||
:root {
|
||||
--color-primary-500: oklch(60% 0.2 270);
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--radius-md: 0.5rem;
|
||||
--font-body: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-primary-500: oklch(75% 0.2 270);
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Layers (cascade 제어)
|
||||
```css
|
||||
@layer reset, base, components, utilities;
|
||||
|
||||
@layer reset { *, *::before, *::after { box-sizing: border-box; } }
|
||||
@layer base { body { font-family: var(--font-body); } }
|
||||
@layer components { .btn { padding: var(--space-2); } }
|
||||
@layer utilities { .mt-4 { margin-top: var(--space-4); } }
|
||||
```
|
||||
|
||||
### Tailwind 4 (CSS-first config)
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand-500: oklch(60% 0.2 270);
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
```
|
||||
```html
|
||||
<button class="bg-brand-500 px-4 py-2 rounded-md text-white">매 Click</button>
|
||||
```
|
||||
|
||||
### Container queries (매 layout-aware component)
|
||||
```css
|
||||
.card-container { container-type: inline-size; }
|
||||
|
||||
.card { display: grid; grid-template-columns: 1fr; }
|
||||
|
||||
@container (min-width: 30rem) {
|
||||
.card { grid-template-columns: 200px 1fr; }
|
||||
}
|
||||
```
|
||||
|
||||
### 컴포넌트 + variants (CVA)
|
||||
```typescript
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const button = cva("rounded-md font-medium", {
|
||||
variants: {
|
||||
intent: {
|
||||
primary: "bg-brand-500 text-white",
|
||||
ghost: "bg-transparent text-brand-500",
|
||||
},
|
||||
size: { sm: "px-2 py-1", md: "px-4 py-2", lg: "px-6 py-3" },
|
||||
},
|
||||
defaultVariants: { intent: "primary", size: "md" },
|
||||
});
|
||||
```
|
||||
|
||||
### Logical properties (i18n-ready)
|
||||
```css
|
||||
.card {
|
||||
padding-inline: var(--space-4); /* LTR / RTL 모두 동작 */
|
||||
margin-block-end: var(--space-2);
|
||||
border-inline-start: 2px solid var(--color-accent);
|
||||
}
|
||||
```
|
||||
|
||||
### Style scoping (CSS Modules)
|
||||
```typescript
|
||||
import styles from "./Card.module.css";
|
||||
export const Card = ({ children }) => <div className={styles.card}>{children}</div>;
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| New project (2026) | Tailwind 4 + CVA + design tokens |
|
||||
| Existing BEM codebase | 매 점진적 migration + Layers |
|
||||
| Component library 출시 | CSS Modules or Vanilla Extract |
|
||||
| Multi-brand SaaS | CSS custom properties + theme swap |
|
||||
|
||||
**기본값**: **Tailwind 4 + CVA + CSS custom property tokens + Container queries**.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[CSS_Architecture_and_Styling|CSS Architecture]] · [[Design System]]
|
||||
- 변형: [[BEM]] · [[CSS Modules]] · [[CSS_Architecture_and_Styling|CSS-in-JS]]
|
||||
- 응용: [[CSS_Architecture_and_Styling|Tailwind CSS]] · [[Vanilla Extract]] · [[Theming]]
|
||||
- Adjacent: [[컨테이너 쿼리 (Container Queries)|Container Queries]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 design system 구축, 매 large team, 매 multi-brand product.
|
||||
**언제 X**: 매 single-page landing, 매 prototype.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Specificity 전쟁**: `!important` 남발 → 매 cascade 붕괴.
|
||||
- **Magic numbers**: `padding: 13px` — 매 token 미사용.
|
||||
- **Global selector overuse**: `div > p > span` — 매 brittle.
|
||||
- **Dead CSS**: 매 unused selector 누적 → 매 bundle bloat.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Tailwind 4 docs, MDN CSS Layers, CSS Tricks).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — scalable CSS 7 patterns |
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
---
|
||||
id: wiki-2026-0508-컴포넌트-기반-웹-프레임워크-아키텍처-설계
|
||||
title: 컴포넌트 기반 웹 프레임워크 아키텍처 설계
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Component Framework Architecture, Web Framework Design]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, architecture, framework-design, react, vue, svelte]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: react-19/vue-3.5/svelte-5
|
||||
---
|
||||
|
||||
# 컴포넌트 기반 웹 프레임워크 아키텍처 설계
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 reactive component tree + diffing/scheduling 의 framework design"**. 매 React/Vue/Svelte 모두 (1) component model, (2) reactivity primitive, (3) rendering scheduler, (4) state management, (5) routing/data layer 의 5-layer stack. 매 2026 의 trend — fine-grained reactivity (Svelte 5 runes, Vue 3.5 Vapor, Solid signals) 의 dominant.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 5-layer stack
|
||||
1. **Component model**: function (React/Solid) vs SFC (Vue/Svelte) vs class.
|
||||
2. **Reactivity primitive**: VDOM diff vs signals vs compile-time reactivity.
|
||||
3. **Scheduler**: sync vs concurrent (React 19) vs microtask-batched.
|
||||
4. **State**: local state, context, external store (Zustand, Pinia, Redux Toolkit).
|
||||
5. **Data/routing**: Next.js App Router, Nuxt, SvelteKit, Remix.
|
||||
|
||||
### 매 reactivity spectrum (2026)
|
||||
- **VDOM diff** (React, Preact): re-run component → diff → patch.
|
||||
- **Fine-grained signals** (Solid, Svelte 5 runes, Vue 3.5 Vapor): track reads/writes, surgical DOM update.
|
||||
- **Compile-time** (Svelte, Marko): compile component to imperative DOM ops.
|
||||
|
||||
### 매 응용
|
||||
1. Internal framework / DSL design.
|
||||
2. Framework-agnostic component library (Lit, Web Components).
|
||||
3. Custom renderer (React Native, react-three-fiber, Ink).
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### 1. Minimal signal-based reactivity (~Solid)
|
||||
```typescript
|
||||
type Signal<T> = [() => T, (v: T) => void];
|
||||
let currentSub: (() => void) | null = null;
|
||||
|
||||
function signal<T>(initial: T): Signal<T> {
|
||||
let value = initial;
|
||||
const subs = new Set<() => void>();
|
||||
const get = () => {
|
||||
if (currentSub) subs.add(currentSub);
|
||||
return value;
|
||||
};
|
||||
const set = (v: T) => { value = v; subs.forEach(s => s()); };
|
||||
return [get, set];
|
||||
}
|
||||
|
||||
function effect(fn: () => void) {
|
||||
const run = () => { currentSub = run; fn(); currentSub = null; };
|
||||
run();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. VDOM diff core (~Preact)
|
||||
```typescript
|
||||
interface VNode { type: string | Function; props: any; children: VNode[]; }
|
||||
|
||||
function diff(oldV: VNode | null, newV: VNode, parent: HTMLElement) {
|
||||
if (!oldV) parent.appendChild(create(newV));
|
||||
else if (oldV.type !== newV.type) parent.replaceChild(create(newV), parent.firstChild!);
|
||||
else updateProps(parent.firstChild as HTMLElement, oldV.props, newV.props);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Component as function (React-style)
|
||||
```typescript
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return h('button', { onClick: () => setCount(count + 1) }, `Count: ${count}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Compile-time reactivity (~Svelte 5 runes)
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
$effect(() => { console.log(count); });
|
||||
</script>
|
||||
<button onclick={() => count++}>{count} / {doubled}</button>
|
||||
```
|
||||
|
||||
### 5. Scheduler (concurrent React-like)
|
||||
```typescript
|
||||
const queue: Array<() => void> = [];
|
||||
let scheduled = false;
|
||||
function schedule(work: () => void) {
|
||||
queue.push(work);
|
||||
if (!scheduled) {
|
||||
scheduled = true;
|
||||
queueMicrotask(() => {
|
||||
while (queue.length) queue.shift()!();
|
||||
scheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Custom renderer registry
|
||||
```typescript
|
||||
interface Renderer<HostNode> {
|
||||
createElement(type: string): HostNode;
|
||||
appendChild(parent: HostNode, child: HostNode): void;
|
||||
setProp(node: HostNode, key: string, value: unknown): void;
|
||||
}
|
||||
// React reconciler / Vue createRenderer 의 패턴.
|
||||
```
|
||||
|
||||
### 7. Compound component pattern
|
||||
```tsx
|
||||
<Tabs>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="a">A</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="a">...</Tabs.Content>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Choice |
|
||||
|---|---|
|
||||
| Mass-market app | React 19 + Next.js 15 (ecosystem) |
|
||||
| Performance-critical | Solid / Svelte 5 (signals) |
|
||||
| Progressive enhancement | Astro + island arch |
|
||||
| Web Components / portable | Lit |
|
||||
| Embedded UI / DSL | Custom renderer atop React reconciler |
|
||||
|
||||
**기본값**: 매 React 19 + Server Components + Suspense (mass-market). 매 perf-bound 의 Solid/Svelte 5.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Component-Composition|Component-Based Architecture]]
|
||||
- 변형: [[Virtual_DOM과_Reconciliation|Virtual DOM]] · [[Fine-Grained Reactivity]]
|
||||
- 응용: [[React]] · [[Solid]]
|
||||
- Adjacent: [[State Management]] · [[Modern_Web_Rendering_and_Optimization|Server Components]] · [[Hydration]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 새 framework / DSL 설계 시. 매 framework choice trade-off discussion 시.
|
||||
**언제 X**: 매 단순 app 개발 — 매 ecosystem (Next.js, Nuxt) defaults 에 의존.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **재발명 반복**: 매 production framework 와 경쟁 의도 — 매 절대 X.
|
||||
- **VDOM 의 abuse**: 매 fine-grained 가 더 적합한 경우에도 VDOM 강행.
|
||||
- **Scheduler omission**: 매 sync only — 매 large tree 의 long task 발생.
|
||||
- **Tight coupling renderer ↔ reactivity**: 매 portability 상실.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (React 19, Vue 3.5 Vapor, Svelte 5 runes, Solid 1.x docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — 5-layer stack + 7 patterns + 2026 reactivity landscape |
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
---
|
||||
id: wiki-2026-0508-콘텐츠-기반의-이커머스-및-뉴스-웹사이트-성능-튜닝
|
||||
title: 콘텐츠 기반의 이커머스 및 뉴스 웹사이트 성능 튜닝
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Content Site Performance, E-commerce Tuning, News Site Tuning]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [frontend, performance, ecommerce, news, isr, edge-caching]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: typescript
|
||||
framework: nextjs
|
||||
---
|
||||
|
||||
# 콘텐츠 기반의 이커머스 및 뉴스 웹사이트 성능 튜닝
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 이커머스 와 뉴스 는 매 LCP 와 매 traffic spike 가 매 매출 / engagement 에 매 직결"**. 매 1 초 LCP 개선 = Amazon $1B/yr (고전), Pinterest +15% conversion. 매 2026 stack — Next.js 15 + ISR + Edge / CDN + Image optim — 가 매 default playbook.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 두 도메인 의 차이
|
||||
- **이커머스**: 매 product page 의 매 LCP (image), 매 PDP 의 매 INP (variant select), 매 cart 의 매 freshness.
|
||||
- **뉴스**: 매 article 의 매 LCP (cover image / headline), 매 publishing speed (ISR ≤ 60s), 매 ad / tracker 부하.
|
||||
|
||||
### 매 핵심 lever
|
||||
- **ISR (Incremental Static Regeneration)**: build-time SSG + runtime revalidate.
|
||||
- **Edge caching**: Vercel / Cloudflare / Fastly 의 매 stale-while-revalidate.
|
||||
- **Image CDN**: Cloudinary / imgix / Next Image — 매 AVIF + responsive.
|
||||
- **Streaming SSR**: RSC + Suspense → 매 TTFB 개선.
|
||||
- **Third-party tax 최소화**: GTM / pixel — 매 Partytown 또는 server-side tracking.
|
||||
|
||||
### 매 응용
|
||||
1. Shopify Hydrogen / Next Commerce.
|
||||
2. NYT, WaPo, Bloomberg 식 article.
|
||||
3. Medium / Substack 식 long-tail.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### ISR (Next 15)
|
||||
```tsx
|
||||
// app/products/[id]/page.tsx
|
||||
export const revalidate = 60; // 매 60초 ISR
|
||||
|
||||
export default async function Product({ params }) {
|
||||
const product = await getProduct(params.id);
|
||||
return <ProductView product={product} />;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const top = await getTopProducts(1000); // 매 top 1000 prebuild
|
||||
return top.map(p => ({ id: p.id }));
|
||||
}
|
||||
```
|
||||
|
||||
### On-demand revalidation
|
||||
```tsx
|
||||
// app/api/revalidate/route.ts
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { path, secret } = await req.json();
|
||||
if (secret !== process.env.REVALIDATE_SECRET) return new Response("nope", { status: 401 });
|
||||
revalidatePath(path);
|
||||
return Response.json({ revalidated: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming SSR (RSC + Suspense)
|
||||
```tsx
|
||||
export default function ArticlePage({ params }) {
|
||||
return (
|
||||
<>
|
||||
<ArticleHeader id={params.id} /> {/* 매 즉시 stream */}
|
||||
<Suspense fallback={<CommentsSkeleton />}>
|
||||
<Comments id={params.id} /> {/* 매 below-fold */}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Edge runtime (latency 최소)
|
||||
```tsx
|
||||
export const runtime = "edge";
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch("https://api...", { next: { revalidate: 30 } });
|
||||
return <View data={await data.json()} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Image responsive + priority
|
||||
```tsx
|
||||
<Image
|
||||
src={product.cover}
|
||||
alt={product.name}
|
||||
width={800} height={600}
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
priority={isAboveFold}
|
||||
quality={80}
|
||||
/>
|
||||
```
|
||||
|
||||
### Partytown (3rd-party off main thread)
|
||||
```tsx
|
||||
import Script from "next/script";
|
||||
|
||||
<Script src="https://www.googletagmanager.com/gtm.js?id=GTM-XXX"
|
||||
strategy="worker" />
|
||||
```
|
||||
|
||||
### Cache header (CDN-friendly)
|
||||
```tsx
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=600",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 페이지 타입 | 전략 |
|
||||
|---|---|
|
||||
| Product Detail (top seller) | SSG + ISR(60s) + Edge |
|
||||
| Product Detail (long tail) | SSR streaming |
|
||||
| Cart / Checkout | SSR (no cache, fresh) |
|
||||
| News article | SSG + ISR(30s) + on-demand |
|
||||
| Live ticker | RSC + WebSocket |
|
||||
|
||||
**기본값**: **Next 15 App Router + ISR + Edge runtime + Next/Image + Partytown**.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Web Performance]] · [[Large_Frontend_Projects|Frontend Architecture]]
|
||||
- 변형: [[ISR]] · [[Edge Computing]] · [[Streaming SSR]]
|
||||
- 응용: [[Headless Commerce]]
|
||||
- Adjacent: [[CDN]] · [[Core Web Vitals Optimization (INP, LCP 개선)|Core Web Vitals]] · [[SEO]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 traffic-heavy content site, 매 PLP/PDP, 매 article-driven SEO.
|
||||
**언제 X**: 매 internal admin, 매 auth-only SaaS.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Cart 까지 ISR**: 매 stale price → 매 trust 붕괴.
|
||||
- **3rd-party 30+ scripts on main thread**: 매 INP 폭주.
|
||||
- **Image 원본 그대로**: 매 5MB JPG → LCP 5초.
|
||||
- **ISR revalidate 너무 짧음 (1s)**: 매 origin 부하.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Vercel docs, web.dev case studies, Cloudflare Workers).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — content site tuning playbook |
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
id: wiki-2026-0508-프론트엔드-컴포넌트-설계
|
||||
title: 프론트엔드 컴포넌트 설계
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: frontend-component-design
|
||||
duplicate_of: "[[Frontend Component Design]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, component, design, architecture]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# 프론트엔드 컴포넌트 설계
|
||||
|
||||
> **이 문서는 [[Frontend Component Design]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약
|
||||
- 매 single-responsibility · 매 composition over inheritance · 매 controlled vs uncontrolled.
|
||||
- 매 modern pattern: 매 headless UI (Radix/Ark) · 매 compound components · 매 slot prop · 매 Server/Client split.
|
||||
- 매 design system 통합: 매 tokens · 매 Storybook · 매 visual regression (Chromatic).
|
||||
|
||||
## 🔗 Graph
|
||||
- Adjacent: [[Design Systems]] · [[React Server Components]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: wiki-2026-0508-하이드레이션-hydration
|
||||
title: 하이드레이션 (Hydration)
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: hydration
|
||||
duplicate_of: "[[Hydration]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, ssr, hydration]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# 하이드레이션 (Hydration)
|
||||
|
||||
> **이 문서는 [[Hydration]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약
|
||||
- 매 SSR/SSG로 전송된 정적 HTML에 client-side JS event handler / state 를 attach 하는 process.
|
||||
- 매 React 18+ `hydrateRoot` · 매 selective hydration · 매 streaming SSR 의 partial hydration.
|
||||
- 매 hydration mismatch 의 SSR/CSR markup 일치 의 강제.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Hydration]] (canonical)
|
||||
- Adjacent: [[Server-Side Rendering (SSR)]] · [[React Server Components]] · [[Streaming SSR]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: wiki-2026-0508-확장-가능한-프론트엔드-아키텍처-scalable-front
|
||||
title: 확장 가능한 프론트엔드 아키텍처(Scalable Frontend Architecture)
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: scalable-frontend-architecture
|
||||
duplicate_of: "[[Scalable Frontend Architecture]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, architecture, micro-frontend]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# 확장 가능한 프론트엔드 아키텍처(Scalable Frontend Architecture)
|
||||
|
||||
> **이 문서는 [[Scalable Frontend Architecture]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약
|
||||
- 매 monorepo (Turborepo/Nx) · 매 module federation · 매 micro-frontend (Module Federation 2.0).
|
||||
- 매 layered: 매 design system · 매 feature module · 매 app shell · 매 BFF (Backend-for-Frontend).
|
||||
- 매 state 분리: 매 server state (TanStack Query) · 매 URL state · 매 client state (Zustand/Jotai).
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Scalable Frontend Architecture]] (canonical)
|
||||
- Adjacent: [[Micro-Frontends]] · [[Module Federation]] · [[Monorepo (Turborepo / Nx)]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
id: wiki-2026-0508-힙-스냅샷-heap-snapshots
|
||||
title: 힙 스냅샷 (Heap Snapshots)
|
||||
category: 10_Wiki/Topics
|
||||
status: duplicate
|
||||
canonical_id: heap-snapshots
|
||||
duplicate_of: "[[Heap Snapshots]]"
|
||||
aliases: []
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: redirected
|
||||
tags: [duplicate, frontend, debugging, memory, devtools]
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
---
|
||||
|
||||
# 힙 스냅샷 (Heap Snapshots)
|
||||
|
||||
> **이 문서는 [[Heap Snapshots]] 의 중복본입니다.** Canonical 문서로 redirect.
|
||||
|
||||
## 핵심 요약
|
||||
- 매 V8 heap 의 dump — 매 Chrome DevTools Memory tab · 매 retainer chain 의 inspect.
|
||||
- 매 leak detection: 매 3-snapshot technique · 매 detached DOM · 매 closure-captured listener.
|
||||
- 매 modern alternative: 매 `performance.measureUserAgentSpecificMemory()` · 매 Lighthouse memory audits.
|
||||
|
||||
## 🔗 Graph
|
||||
- Adjacent: [[Memory Leak Detection]] · [[Chrome DevTools 메모리 프로파일링|Chrome DevTools]]
|
||||
|
||||
## 🕓 변경 이력
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | 중복 처리 — canonical 문서로 redirect |
|
||||
Reference in New Issue
Block a user