docs(10_Wiki): Topic_Business/General/Graphic/Programming을 Topics/ 하위로 이동
최상위 10_Wiki/Topic_*였던 4개 카테고리 폴더를 10_Wiki/Topics/Topic_* 로 재배치. 콘텐츠 변경 없음(순수 폴더 이동) — Topics/ 하위 나머지 폴더는 이미 지난 커밋에서 전부 정리된 상태(잔존 항목은 에이전트 운영 상태 및 사용자가 보존을 요청한 업데이트0615/무제 3.canvas 뿐).
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
---
|
||||
id: wiki-2026-0508-effect-ts-및-ts-brand-라이브러리-활용
|
||||
title: Effect TS 및 ts-brand 라이브러리 활용
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [Effect-TS, ts-brand, Branded Types, Effect.gen]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [typescript, functional, effect-system, branded-types]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript
|
||||
framework: Effect
|
||||
---
|
||||
|
||||
# Effect TS 및 ts-brand 라이브러리 활용
|
||||
|
||||
## 매 한 줄
|
||||
> **"매 typed effect system + nominal type 의 — TypeScript 의 의 ZIO/Cats 의 imported"**. Effect 의 의 async/error/dependency 의 의 single 의 `Effect<A, E, R>` 의 의 unify, ts-brand 의 의 structural type 의 의 nominal flavor 의 의 add. 2026 의 매 enterprise TS codebase 의 의 fast 의 emerging.
|
||||
|
||||
## 매 핵심
|
||||
|
||||
### 매 Effect 의 type
|
||||
- `Effect<A, E, R>` — 의 success `A`, 의 failure `E`, 의 requirement `R`.
|
||||
- 매 lazy — 매 `Effect.runPromise` / `Effect.runSync` 의 의 actual 의 execute.
|
||||
- 의 composable — `pipe`, `Effect.gen` 의 의 chain.
|
||||
|
||||
### 매 ts-brand 의 nominal type
|
||||
- 의 TypeScript structural — `string === string` 의 의 distinguishable X.
|
||||
- Brand 의 의 `string & { __brand: "UserId" }` 의 의 phantom tag.
|
||||
- 의 runtime cost zero — type-level only.
|
||||
|
||||
### 매 응용
|
||||
1. Domain ID (UserId, OrderId 의 의 mix-up 의 prevent).
|
||||
2. Validated value (Email, NonEmptyString).
|
||||
3. Async pipeline 의 typed error.
|
||||
4. Dependency injection (Effect Layer).
|
||||
5. Retry/timeout/concurrency 의 declarative.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### ts-brand 기본
|
||||
```ts
|
||||
import type { Brand } from "ts-brand";
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type OrderId = Brand<string, "OrderId">;
|
||||
|
||||
const makeUserId = (s: string): UserId => s as UserId;
|
||||
|
||||
function getUser(id: UserId) { /* ... */ }
|
||||
|
||||
const uid = makeUserId("u_123");
|
||||
const oid = "o_456" as OrderId;
|
||||
|
||||
getUser(uid);
|
||||
// getUser(oid); // 매 compile error — UserId 의 X
|
||||
// getUser("u_123"); // 매 compile error — raw string
|
||||
```
|
||||
|
||||
### Validated brand (smart constructor)
|
||||
```ts
|
||||
type Email = Brand<string, "Email">;
|
||||
|
||||
function parseEmail(s: string): Email | null {
|
||||
return /^[^@]+@[^@]+\.[^@]+$/.test(s) ? (s as Email) : null;
|
||||
}
|
||||
|
||||
function sendMail(to: Email, subject: string) { /* ... */ }
|
||||
|
||||
const e = parseEmail(input);
|
||||
if (e) sendMail(e, "hi"); // 매 type-narrowed 의 valid 의 only
|
||||
```
|
||||
|
||||
### Effect 기본
|
||||
```ts
|
||||
import { Effect, pipe } from "effect";
|
||||
|
||||
const fetchUser = (id: UserId): Effect.Effect<User, NetworkError | NotFoundError> =>
|
||||
Effect.tryPromise({
|
||||
try: () => fetch(`/api/users/${id}`).then((r) => {
|
||||
if (r.status === 404) throw new NotFoundError(id);
|
||||
return r.json();
|
||||
}),
|
||||
catch: (e) => e instanceof NotFoundError ? e : new NetworkError(e),
|
||||
});
|
||||
|
||||
const program = pipe(
|
||||
fetchUser(uid),
|
||||
Effect.map((u) => u.email),
|
||||
Effect.tap((email) => Effect.log(`Got: ${email}`)),
|
||||
);
|
||||
|
||||
Effect.runPromise(program).then(console.log);
|
||||
```
|
||||
|
||||
### Effect.gen (do-notation)
|
||||
```ts
|
||||
import { Effect } from "effect";
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const user = yield* fetchUser(uid);
|
||||
const orders = yield* fetchOrders(user.id);
|
||||
const valid = orders.filter((o) => o.status === "paid");
|
||||
return { user, orderCount: valid.length };
|
||||
});
|
||||
```
|
||||
|
||||
### Typed retry / timeout
|
||||
```ts
|
||||
import { Effect, Schedule, Duration } from "effect";
|
||||
|
||||
const robust = pipe(
|
||||
fetchUser(uid),
|
||||
Effect.retry(Schedule.exponential(Duration.millis(100)).pipe(Schedule.compose(Schedule.recurs(3)))),
|
||||
Effect.timeout(Duration.seconds(5)),
|
||||
);
|
||||
```
|
||||
|
||||
### Layer / dependency injection
|
||||
```ts
|
||||
import { Context, Effect, Layer } from "effect";
|
||||
|
||||
class Database extends Context.Tag("Database")<Database, {
|
||||
query: (sql: string) => Effect.Effect<unknown[], DBError>;
|
||||
}>() {}
|
||||
|
||||
const DatabaseLive = Layer.succeed(Database, {
|
||||
query: (sql) => Effect.tryPromise({ try: () => pool.query(sql), catch: (e) => new DBError(e) }),
|
||||
});
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const db = yield* Database;
|
||||
const rows = yield* db.query("SELECT * FROM users");
|
||||
return rows;
|
||||
});
|
||||
|
||||
Effect.runPromise(program.pipe(Effect.provide(DatabaseLive)));
|
||||
```
|
||||
|
||||
### Schema (Effect's Zod-equivalent)
|
||||
```ts
|
||||
import { Schema } from "effect";
|
||||
|
||||
const User = Schema.Struct({
|
||||
id: Schema.String.pipe(Schema.brand("UserId")),
|
||||
email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+$/), Schema.brand("Email")),
|
||||
age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
|
||||
});
|
||||
|
||||
type User = Schema.Schema.Type<typeof User>;
|
||||
const decoded = Schema.decodeUnknownSync(User)(input);
|
||||
```
|
||||
|
||||
### 의 Concurrency
|
||||
```ts
|
||||
import { Effect } from "effect";
|
||||
|
||||
const fetchAll = Effect.all(
|
||||
[fetchUser(u1), fetchUser(u2), fetchUser(u3)],
|
||||
{ concurrency: 2 },
|
||||
);
|
||||
```
|
||||
|
||||
### 의 Either (sync error)
|
||||
```ts
|
||||
import { Either } from "effect";
|
||||
|
||||
const safeParse = (s: string): Either.Either<number, "not_a_number"> => {
|
||||
const n = Number(s);
|
||||
return Number.isNaN(n) ? Either.left("not_a_number") : Either.right(n);
|
||||
};
|
||||
```
|
||||
|
||||
### Effect 의 React (effect-rx)
|
||||
```tsx
|
||||
import { useRxSuspense } from "@effect-rx/rx-react";
|
||||
|
||||
const userRx = Rx.make((get) => fetchUser(get(userIdRx)));
|
||||
|
||||
function UserProfile() {
|
||||
const user = useRxSuspense(userRx);
|
||||
return <div>{user.email}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| Domain ID 의 의 mix-up 의 prevent | ts-brand 의만 |
|
||||
| Validated value (email, URL) | ts-brand 의 + smart constructor |
|
||||
| Complex async pipeline | Effect TS |
|
||||
| Typed error + retry + timeout | Effect TS |
|
||||
| DI 의 의 typed | Effect Layer |
|
||||
| Simple fetch + try/catch | 매 plain async — Effect 의 X |
|
||||
|
||||
**기본값**: ts-brand 의 의 always, Effect 의 의 의 complex pipeline 의만.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[TypeScript]] · [[Functional Programming]]
|
||||
- 변형: [[fp-ts]]
|
||||
- 응용: [[Discriminated Unions for Error Handling]] · [[Dependency Injection]]
|
||||
- Adjacent: [[Zod]] · [[Branded Types]] · [[Schema Validation]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: typed pipeline 의 design, domain ID safety, layer-based DI, schema-driven decode.
|
||||
**언제 X**: 의 매 simple CRUD — 매 Effect 의 learning curve 의 의 not worth.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **`as UserId` 의 의 raw string 의 의 cast**: 의 brand 의 의 bypass — 의 smart constructor 의 의 always.
|
||||
- **Effect 의 의 entire codebase 의 의 force**: 의 team 의 의 buy-in 의 X 의 시 의 — 매 friction.
|
||||
- **`Effect.runSync` 의 의 async 의**: 의 throw 의 의 — runPromise 의 의 사용.
|
||||
- **Mutable state 의 의 Effect 의**: 의 referential transparency 의 의 violation — Ref/Layer 의 의 사용.
|
||||
- **Brand 의 의 nested 의 reuse**: `Brand<Brand<string, "A">, "B">` 의 의 confusing — 매 single brand 의 keep.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (Effect-TS docs effect.website, ts-brand npm, ZIO inspiration, Effect Schema docs).
|
||||
- 신뢰도 A.
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — Effect.gen + Layer + Schema + ts-brand smart constructor 추가 |
|
||||
Reference in New Issue
Block a user