[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
---
|
||||
id: ts-effect-fp-patterns
|
||||
title: Effect / fp-ts — 함수형 에러 / 의존성 / 동시성
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [typescript, effect, fp-ts, functional, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend", "Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [Effect, fp-ts, Either, Option, Result, Layer, dependency injection, retry]
|
||||
---
|
||||
|
||||
# Effect / fp-ts
|
||||
|
||||
> Promise + try/catch 의 한계 = 명시적 에러 type X, 의존성 묶임. **Effect (modern) / fp-ts (전통)** = 타입 안전 에러 + DI + 재시도 + 동시성 일급. 학습 곡선 큼 — 팀 합의 필요.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Effect<A, E, R>: 성공 A / 에러 E / 의존성 R.
|
||||
- pipe: 작은 함수 합성.
|
||||
- Layer: 의존성 주입.
|
||||
- Schedule: retry / repeat 정책.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Effect 기본
|
||||
```ts
|
||||
import { Effect } from 'effect';
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const user = yield* fetchUser('u1'); // Effect<User, ApiError, never>
|
||||
const orders = yield* fetchOrders(user.id);
|
||||
return { user, orders };
|
||||
});
|
||||
|
||||
const result = await Effect.runPromise(program); // throws on failure
|
||||
// 또는
|
||||
const result = await Effect.runPromiseExit(program); // Exit<A, E>
|
||||
```
|
||||
|
||||
### Either / 명시적 에러
|
||||
```ts
|
||||
import { Effect } from 'effect';
|
||||
|
||||
class UserNotFound { readonly _tag = 'UserNotFound'; }
|
||||
class NetworkError { readonly _tag = 'NetworkError'; }
|
||||
|
||||
function fetchUser(id: string): Effect.Effect<User, UserNotFound | NetworkError> {
|
||||
return Effect.tryPromise({
|
||||
try: () => api.get(id),
|
||||
catch: (e) => e instanceof TypeError ? new NetworkError() : new UserNotFound(),
|
||||
});
|
||||
}
|
||||
|
||||
// Match
|
||||
const handled = program.pipe(
|
||||
Effect.catchTag('UserNotFound', () => Effect.succeed(defaultUser)),
|
||||
Effect.catchTag('NetworkError', () => Effect.retry(...)),
|
||||
);
|
||||
```
|
||||
|
||||
### Layer (DI)
|
||||
```ts
|
||||
class Database extends Effect.Service<Database>()('Database', {
|
||||
effect: Effect.gen(function* () {
|
||||
const url = yield* Config.string('DATABASE_URL');
|
||||
return { query: (sql: string) => Effect.tryPromise(() => pg.query(sql)) };
|
||||
}),
|
||||
}) {}
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const db = yield* Database;
|
||||
const r = yield* db.query('SELECT 1');
|
||||
return r;
|
||||
});
|
||||
|
||||
// 실행 시 layer 제공
|
||||
Effect.runPromise(program.pipe(Effect.provide(Database.Default)));
|
||||
```
|
||||
|
||||
### Schedule (retry + backoff)
|
||||
```ts
|
||||
import { Schedule, Effect } from 'effect';
|
||||
|
||||
const policy = Schedule.exponential('100 millis').pipe(
|
||||
Schedule.compose(Schedule.recurs(5)),
|
||||
Schedule.jittered,
|
||||
);
|
||||
|
||||
const robust = program.pipe(Effect.retry(policy));
|
||||
```
|
||||
|
||||
### Concurrency
|
||||
```ts
|
||||
const all = Effect.all([fetchA, fetchB, fetchC], { concurrency: 'unbounded' });
|
||||
const limited = Effect.all(items.map(process), { concurrency: 10 });
|
||||
|
||||
// Race
|
||||
const fast = Effect.race(slowEndpoint, fastEndpoint);
|
||||
```
|
||||
|
||||
### Stream
|
||||
```ts
|
||||
import { Stream } from 'effect';
|
||||
|
||||
const numbers = Stream.range(1, 1000);
|
||||
const processed = numbers.pipe(
|
||||
Stream.map(n => n * 2),
|
||||
Stream.filter(n => n % 3 === 0),
|
||||
Stream.take(10),
|
||||
Stream.runCollect,
|
||||
);
|
||||
```
|
||||
|
||||
### Schema (built-in 검증, like zod)
|
||||
```ts
|
||||
import { Schema } from 'effect';
|
||||
|
||||
const User = Schema.Struct({
|
||||
id: Schema.String.pipe(Schema.uuid()),
|
||||
email: Schema.String.pipe(Schema.email()),
|
||||
age: Schema.Number.pipe(Schema.greaterThan(0)),
|
||||
});
|
||||
|
||||
type User = Schema.Schema.Type<typeof User>;
|
||||
|
||||
const decoded = Schema.decodeUnknownEither(User)(input);
|
||||
```
|
||||
|
||||
### Resource (자동 cleanup)
|
||||
```ts
|
||||
const file = Effect.acquireRelease(
|
||||
Effect.sync(() => fs.openSync(path, 'r')),
|
||||
(fd) => Effect.sync(() => fs.closeSync(fd)),
|
||||
);
|
||||
|
||||
const program = Effect.scoped(Effect.gen(function* () {
|
||||
const fd = yield* file;
|
||||
// 사용
|
||||
// 자동 close
|
||||
}));
|
||||
```
|
||||
|
||||
### fp-ts (전통 — Effect 가 후속)
|
||||
```ts
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
|
||||
const program = pipe(
|
||||
fetchUser('u1'),
|
||||
TE.flatMap(user => fetchOrders(user.id)),
|
||||
TE.map(orders => orders.length),
|
||||
);
|
||||
|
||||
const result = await program(); // Either<Error, number>
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 작은 / 일반 팀 | Promise + try/catch + zod |
|
||||
| 큰 백엔드 + FP 팀 | Effect |
|
||||
| Monad / FP 학습 | fp-ts |
|
||||
| Error type 안전 강 | Effect |
|
||||
| 큰 동시성 / 재시도 정책 복잡 | Effect |
|
||||
| Front + Back 공유 | Effect (range 광범위) |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Promise 와 Effect 혼용**: 변환 비용. 한 모델만.
|
||||
- **Effect 도입 — 팀 학습 X**: 유지보수 못 함.
|
||||
- **catchAll 모든 에러 swallow**: type 안전 의미 없음.
|
||||
- **runSync prod**: error 던짐. runPromise / runFork.
|
||||
- **Layer 없이 직접 의존**: testability 낮음.
|
||||
- **Stream 으로 작은 데이터**: overkill. 일반 array.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 도입 결정 = 팀 합의 필수.
|
||||
- Effect 가 modern (2024+), fp-ts 는 legacy (저자가 Effect 로 이동).
|
||||
- Schema + DI + retry 가 가장 ROI.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[TS_tsconfig_Strategy]]
|
||||
- [[Schema_Validation_Zod_Patterns]]
|
||||
- [[Error_Handling_Result_vs_Throw]]
|
||||
Reference in New Issue
Block a user