Brass TS — TypeScript에서 Effect Runtime 구축하기 (파트 4)
Source: Dev.to
TL;DR
brass-http은 fetch 래퍼가 아닙니다.
이는 효과를 가진 HTTP 클라이언트로, 파이버, 구조화된 동시성, 그리고 취소를 지원하는 런타임 위에 구축되었습니다.
이전 파트에서는 ZIO에서 영감을 받은 효과 런타임의 기초를 만들었습니다:
- 파이버와 스케줄링
- 구조화된 동시성
- JavaScript 생태계와의 통합
- 실제 개발자 경험(DX)을 제공하는 ZIO‑스타일 HTTP 클라이언트
이번 네 번째 파트에서는 HTTP가 효과로 모델링될 때 어떤 변화가 일어나는지와 재시도, 취소, 스트리밍, 파이프라인 같은 기능들이 해킹이 아니라 런타임 속성이 되는 이유를 보여줍니다.
📦 Repository:
중요한 사고 전환
HTTP는 비동기 함수가 아니다.
HTTP는 작업에 대한 설명이다.
Brass에서는:
- 해석될 때까지 아무것도 실행되지 않는다
- 재시도는 스케줄링되며, 반복되지 않는다
- 취소는 구조적이며, 최선 노력에 의한 것이 아니다
주석:
효과는 실행을 지연시킨다. 프라미스는 그렇지 않다.
효과 vs 약속
Promise‑based HTTP
fetch()
└─▶ starts immediately
└─▶ cannot be stopped
└─▶ side‑effects escape
Effect‑based HTTP (Brass)
HTTP Effect
│
│ (pure description)
▼
Interpreter (toPromise / Fiber / Stream)
│
▼
Runtime Scheduler
│
▼
Execution
효과적인 HTTP 클라이언트 만들기
const http = httpClient({
baseUrl: "https://jsonplaceholder.typicode.com",
}).withRetry({
maxRetries: 3,
baseDelayMs: 200,
maxDelayMs: 2000,
});
🚫 아직 요청이 실행되지 않았습니다.
✅ 당신은 단지 설명했습니다:
- 요청이 어떻게 구성되는지
- 재시도가 어떻게 동작하는지
- 오류가 어떻게 분류되는지
- 모든 것이 런타임 스케줄러와 어떻게 통합되는지
실행 효과: toPromise
const effect = http.getJson("/posts/1");
const promise = toPromise(effect, {});
const result = await promise;
Callout:
getJson은 Promise를 반환하지 않습니다.Effect를 반환합니다.
toPromise는 단지 하나의 인터프리터일 뿐입니다.
일급 개념으로서의 재시도
const http = httpClient({ baseUrl }).withRetry({
maxRetries: 3,
retryOnStatus: (s) => s >= 500,
retryOnError: (e) => e._tag === "FetchError",
});
이것은 래퍼가 아닙니다.
재시도:
while루프나try/catch + setTimeout이 아닙니다- 프로미스 재귀가 아닙니다
💡 재시도는 효과 설명의 일부입니다.
재시도 라이프사이클
HTTP Effect
│
▼
Attempt #1 ──┐
│ │
│ error │ schedule
▼ │ delay
Retry Policy │ (scheduler)
│ │
▼ │
Attempt #2 ──┘
│
▼
Attempt #N
│
▼
Success / Failure
Callout:
재시도는 스케줄링되며, 루프되지 않습니다.
실제로 작동하는 취소
효과를 취소하면 모든 것이 취소됩니다.
섬유와 구조화된 동시성 덕분에, 단일 섬유를 취소하면 전체 HTTP 라이프사이클에 걸쳐 전파됩니다.
Fiber
│
├─▶ HTTP Effect
│ ├─▶ fetch
│ ├─▶ retry delay
│ └─▶ response decode
│
└─▶ other effects
Cancel Fiber
│
▼
╳ fetch aborted
╳ retry timers cleared
╳ decode stopped
Callout:
취소는 구조적이며, 최선 노력에 의존하지 않습니다.
원시 와이어 응답 (전체 제어)
const wire = await toPromise(http.get("/posts/1"), {});
console.log(wire.status);
console.log(wire.bodyText);
Callout:
원시 와이어 응답도 재시도 가능하고, 취소 가능하며, 예약될 수 있습니다.
요청은 데이터다 (optics FTW)
const req = mergeHeaders({ accept: "application/json" })(
setHeaderIfMissing("content-type", "application/json")({
method: "POST",
url: "/posts",
body: JSON.stringify({
userId: 1,
title: "Hello Brass",
body: "Testing POST from Brass HTTP client",
}),
})
);
const response = await http.request(req).toPromise({});
Base Request
│
▼
mergeHeaders
│
▼
setHeaderIfMissing
│
▼
Final Request
│
▼
http.request(effect)
Callout:
요청은 행동이 아니라 값이다.
파이프라인: HTTP를 효과의 흐름으로
Request
│
▼
[ Enrich ]
│
▼
[ Retry Policy ]
│
▼
[ Fetch ]
│
▼
[ Decode ]
│
▼
Response / Wire
Streaming (design‑ready)
Producer (HTTP Body)
│
▼
Stream Effect
│
├─▶ Consumer A
│
└─▶ Consumer B
Cancel
│
▼
Stream stops
Callout:
스트림은 콜백이 아니라 효과입니다.
왜 이것이 중요한가?
Promises:
async + hope
Effects:
describe → schedule → execute → control
왜냐하면 HTTP가 효과가 되면:
- 재시도가 더 이상 취약하지 않다
- 취소가 예측 가능해진다
- 테스트가 간단해진다
- 구성이 자연스러워진다
다음 단계
- 전체 스트리밍 API
- 효과로서의 타임아웃
- 추적
- 메트릭
- 리소스 스코프