Brass TS — TypeScript에서 Effect Runtime 구축하기 (파트 4)

발행: (2026년 1월 11일 오전 12:23 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

TL;DR

brass-httpfetch 래퍼가 아닙니다.
이는 효과를 가진 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:
getJsonPromise를 반환하지 않습니다. 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
  • 효과로서의 타임아웃
  • 추적
  • 메트릭
  • 리소스 스코프
Back to Blog

관련 글

더 보기 »