💻 프로그래밍

Effect

TypeScript 함수형 프로그래밍 라이브러리

📖 상세 설명

Effect는 TypeScript를 위한 차세대 함수형 프로그래밍 라이브러리로, 타입 안전한 방식으로 부수 효과(side effect)를 관리합니다. Scala의 ZIO에서 영감을 받아 설계되었으며, 에러 핸들링, 동시성, 리소스 관리를 타입 시스템 수준에서 추적할 수 있습니다.

Effect의 핵심 개념은 Effect<Success, Error, Requirements> 타입입니다. 이 타입은 성공 시 반환값, 발생 가능한 에러 타입, 필요한 의존성을 모두 타입으로 명시합니다. 컴파일 시점에 에러 처리 누락을 감지할 수 있어 런타임 예외를 크게 줄일 수 있습니다.

Effect는 Promise의 한계를 극복하기 위해 설계되었습니다. Promise는 에러 타입을 추적하지 않고 즉시 실행(eager)되지만, Effect는 지연 실행(lazy)되며 에러 타입을 완전히 추적합니다. 또한 취소, 재시도, 타임아웃, 동시성 제어 등 프로덕션에 필요한 기능을 기본 제공합니다.

실무에서 Effect는 복잡한 백엔드 로직, API 통합, 데이터 파이프라인 구축에 특히 강력합니다. 의존성 주입(DI)이 타입 시스템에 내장되어 있어 테스트가 용이하고, Schema 모듈을 통해 런타임 데이터 검증까지 타입 안전하게 처리할 수 있습니다.

💻 코드 예제

import { Effect, pipe, Console } from "effect"

// 1. 기본 Effect 생성과 에러 핸들링
const fetchUser = (id: number): Effect.Effect<User, HttpError> =>
    Effect.tryPromise({
        try: () => fetch(`/api/users/${id}`).then(r => r.json()),
        catch: () => new HttpError("사용자 조회 실패")
    })

// 2. 타입 안전한 에러 처리
const program = pipe(
    fetchUser(1),
    Effect.flatMap(user =>
        Effect.succeed(`환영합니다, ${user.name}님!`)
    ),
    Effect.catchTag("HttpError", (error) =>
        Effect.succeed("게스트로 접속합니다")
    )
)

// 3. 동시성 처리 - 여러 API 병렬 호출
const fetchAllData = Effect.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
], { concurrency: 3 }) // 최대 3개 동시 실행

// 4. 재시도 및 타임아웃 설정
const resilientFetch = pipe(
    fetchUser(1),
    Effect.retry({ times: 3 }),           // 최대 3회 재시도
    Effect.timeout("5 seconds"),          // 5초 타임아웃
    Effect.tap(user =>
        Console.log(`사용자: ${user.name}`)
    )
)

// 5. 의존성 주입 (서비스 레이어 패턴)
class Database extends Effect.Tag("Database")<
    Database,
    { query: (sql: string) => Effect.Effect<any[], DbError> }
>() {}

const getUsers = Database.pipe(
    Effect.flatMap(db => db.query("SELECT * FROM users"))
)

// 실행
Effect.runPromise(program).then(console.log)

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"이 서비스는 외부 API 연동이 많아서 Effect로 구현하면 좋겠습니다. 에러 타입을 컴파일 타임에 추적할 수 있고, 재시도 로직이나 타임아웃도 선언적으로 처리할 수 있어요. Promise 체이닝보다 유지보수가 훨씬 쉬워집니다."
💬 면접에서
"Effect는 타입 레벨에서 에러와 의존성을 추적하는 함수형 라이브러리입니다. Effect<A, E, R> 타입에서 A는 성공 값, E는 에러 타입, R은 필요한 서비스를 나타냅니다. 이를 통해 'Railway Oriented Programming' 패턴을 TypeScript에서 타입 안전하게 구현할 수 있습니다."
💬 코드 리뷰에서
"여기서 Effect.runPromise 대신 Effect.runPromiseExit을 사용하면 에러 발생 시에도 Exit 타입으로 결과를 받을 수 있어요. 그리고 catchAll보다 catchTag를 쓰면 특정 에러만 선택적으로 처리할 수 있습니다."

⚠️ 흔한 실수 & 주의사항

Effect를 실행하지 않고 반환만 하기

Effect는 지연 실행(lazy)이므로 생성만 하면 아무 일도 일어나지 않습니다. 반드시 Effect.runPromise나 Effect.runSync로 실행해야 합니다. 단위 테스트에서 자주 빠뜨리는 실수입니다.

pipe 없이 메서드 체이닝 시도

Effect의 함수들은 파이프라인 스타일로 설계되었습니다. `effect.map(fn)` 대신 `pipe(effect, Effect.map(fn))` 또는 `effect.pipe(Effect.map(fn))`을 사용해야 합니다.

점진적 도입 권장

Effect는 학습 곡선이 있으므로 전체 코드베이스를 한번에 마이그레이션하지 마세요. 먼저 새로운 기능이나 복잡한 비동기 로직에서 시작하고, 팀의 이해도가 높아지면 점차 확장하세요.

🔗 관련 용어

📚 더 배우기