icon안동민 개발노트

함수형 유틸리티 라이브러리 사용 (fp-ts)


 fp-ts는 타입스크립트를 위한 강력한 함수형 프로그래밍 라이브러리입니다.

 이 라이브러리는 타입 안전성과 함께 함수형 프로그래밍의 핵심 개념들을 구현할 수 있게 해줍니다.

fp-ts 개요와 이점

 fp-ts는 다음과 같은 이점을 제공합니다.

  1. 타입 안전한 함수형 프로그래밍 구조
  2. 일관된 API를 통한 다양한 함수형 개념 구현
  3. 코드의 가독성과 재사용성 향상
  4. 부수 효과의 명시적 관리

주요 데이터 타입

  1. Option : null 또는 undefined를 안전하게 다루기 위한 타입
import { Option, some, none, pipe } from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
 
function divide(a: number, b: number): Option<number> {
    return b === 0 ? none : some(a / b)
}
 
const result = pipe(
    divide(10, 2),
    Option.map(x => x * 2),
    Option.getOrElse(() => 0)
)
 
console.log(result) // 10
  1. Either : 에러 처리를 위한 타입
import { Either, right, left } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
 
function sqrt(x: number): Either<string, number> {
    return x < 0 ? left('Cannot calculate square root of negative number') : right(Math.sqrt(x))
}
 
const result = pipe(
    sqrt(-1),
    Either.map(x => x * 2),
    Either.getOrElse(() => 0)
)
 
console.log(result) // 0
  1. Task : 비동기 연산을 위한 타입
import { Task } from 'fp-ts/Task'
import { pipe } from 'fp-ts/function'
 
const delay = (ms: number): Task<void> => () => new Promise(resolve => setTimeout(resolve, ms))
 
const program = pipe(
    delay(1000),
    Task.chain(() => Task.of('Hello, world!'))
)
 
program().then(console.log) // 1초 후 "Hello, world!" 출력

함수 합성, 에러 처리, 비동기 프로그래밍

  1. 함수 합성
import { flow } from 'fp-ts/function'
 
const double = (n: number) => n * 2
const addOne = (n: number) => n + 1
 
const doubleThenAddOne = flow(double, addOne)
 
console.log(doubleThenAddOne(3)) // 7
  1. 에러 처리
import { Either, right, left } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
 
const safeDivide = (a: number, b: number): Either<string, number> =>
    b === 0 ? left('Division by zero') : right(a / b)
 
const result = pipe(
    safeDivide(10, 2),
    Either.chain(x => safeDivide(x, 2)),
    Either.map(x => x * 2),
    Either.getOrElse(() => 0)
)
 
console.log(result) // 5
  1. 비동기 프로그래밍
import { TaskEither, tryCatch } from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
 
const fetchUser = (id: number): TaskEither<Error, User> =>
    tryCatch(
        () => fetch(`https://api.example.com/users/${id}`).then(res => res.json()),
        reason => new Error(String(reason))
    )
 
const program = pipe(
    fetchUser(1),
    TaskEither.map(user => user.name)
)
 
program().then(console.log)

타입 클래스와 다형적 코드

 fp-ts는 Functor, Applicative, Monad 등의 타입 클래스를 제공합니다.

import { Option, some, none } from 'fp-ts/Option'
import { Either, right, left } from 'fp-ts/Either'
import { Functor2 } from 'fp-ts/Functor'
 
const double = <F extends Functor2<any>>(F: F) => (fa: F['_1']) =>
    F.map(fa, (a: number) => a * 2)
 
const doubleOption = double(Option)
const doubleEither = double(Either)
 
console.log(doubleOption(some(5))) // some(10)
console.log(doubleEither(right(5))) // right(10)

고급 모나드 사용

  1. Reader 모나드 : 의존성 주입에 유용
import { Reader } from 'fp-ts/Reader'
import { pipe } from 'fp-ts/function'
 
interface Environment {
    config: { apiUrl: string }
    logger: { log: (message: string) => void }
}
 
const getApiUrl: Reader<Environment, string> = ({ config }) => config.apiUrl
const logMessage = (message: string): Reader<Environment, void> =>
    ({ logger }) => logger.log(message)
 
const program = pipe(
    getApiUrl,
    Reader.chain(url => logMessage(`API URL is ${url}`))
)
 
const env: Environment = {
    config: { apiUrl: 'https://api.example.com' },
    logger: { log: console.log }
}
 
program(env)
  1. State 모나드 : 상태 관리에 유용
import { State } from 'fp-ts/State'
import { pipe } from 'fp-ts/function'
 
const increment: State<number, void> = state => [undefined, state + 1]
const getState: State<number, number> = state => [state, state]
 
const program = pipe(
    increment,
    State.chain(() => increment),
    State.chain(() => getState)
)
 
console.log(program(0)) // [2, 2]

타입 안전성 극대화

 fp-ts와 타입스크립트의 타입 시스템을 결합하여 타입 안전성을 높일 수 있습니다.

import { Option, some, none } from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
 
type User = { name: string; age: number }
 
const getUser = (id: number): Option<User> =>
    id === 1 ? some({ name: "John", age: 30 }) : none
 
const getAge = (user: User): Option<number> =>
    user.age >= 18 ? some(user.age) : none
 
const program = pipe(
    getUser(1),
    Option.chain(getAge),
    Option.map(age => `User is ${age} years old`),
    Option.getOrElse(() => "User not found or not an adult")
)
 
console.log(program) // "User is 30 years old"

도입 전략과 리팩토링

  1. 부분적 도입 : 특정 모듈이나 기능에 fp-ts를 적용
  2. 점진적 확장 : 성공적인 적용 후 다른 부분으로 확장
  3. 기존 코드와의 통합 : fp-ts의 데이터 타입과 기존 타입 간 변환 함수 작성
  4. 팀 교육 : fp-ts와 함수형 프로그래밍 개념에 대한 워크샵 진행

성능 고려사항과 최적화

  1. 불필요한 래핑 피하기 : 과도한 모나드 사용은 성능 저하 가능
  2. 메모이제이션 활용 : 순수 함수에 대해 메모이제이션 적용
  3. 효율적인 데이터 구조 선택 : 상황에 맞는 적절한 데이터 구조 사용

Best Practices와 주의사항

  1. 일관성 유지 : 프로젝트 전체에서 fp-ts 사용 패턴 통일
  2. 문서화 : 복잡한 함수형 구조에 대한 설명 제공
  3. 테스트 작성 : 함수형 코드에 대한 단위 테스트 강화
  4. 과도한 추상화 주의 : 필요 이상의 복잡한 구조 지양
  5. 성능 모니터링 : fp-ts 사용에 따른 성능 영향 주기적 검토

대안 라이브러리 비교

  1. Ramda : 자바스크립트 중심, 타입 추론이 fp-ts에 비해 제한적
  2. Lodash/FP : 널리 사용되지만 타입 안전성이 fp-ts에 비해 떨어짐
  3. Purify-ts : fp-ts와 유사하지만 더 작은 규모의 라이브러리

 fp-ts는 타입스크립트에서 함수형 프로그래밍을 구현하는 강력한 도구입니다.

 이 라이브러리를 효과적으로 활용하면 더 안전하고 유지보수가 쉬운 코드를 작성할 수 있습니다.