17.1 함수형 프로그래밍 패턴 — pipe/compose와 Option/Result 타입
함수형 프로그래밍 핵심 개념
TypeScript에서 함수형 프로그래밍은 불변성, 순수 함수, 함수 합성을 통해 예측 가능하고 테스트하기 쉬운 코드를 만듭니다.
// 순수 함수 — 같은 입력, 같은 출력
const double = (n: number): number => n * 2
const addOne = (n: number): number => n + 1
// 불순 함수 — 부작용 발생
let count = 0
const increment = () => ++count // 외부 상태 변경
pipe와 compose
// pipe — 왼쪽에서 오른쪽으로 실행
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C
function pipe<A, B, C, D>(
fn1: (a: A) => B,
fn2: (b: B) => C,
fn3: (c: C) => D
): (a: A) => D
function pipe(...fns: Array<(arg: unknown) => unknown>) {
return (value: unknown) => fns.reduce((acc, fn) => fn(acc), value)
}
// compose — 오른쪽에서 왼쪽으로 실행
function compose<A, B, C>(
fn2: (b: B) => C,
fn1: (a: A) => B
): (a: A) => C {
return (value: A) => fn2(fn1(value))
}
// 사용 예시
const processNumber = pipe(
(n: number) => n * 2, // double
(n: number) => n + 1, // add one
(n: number) => `Result: ${n}` // to string
)
console.log(processNumber(5)) // "Result: 11"
// 문자열 처리 파이프라인
const processName = pipe(
(s: string) => s.trim(),
(s: string) => s.toLowerCase(),
(s: string) => s.replace(/\s+/g, '-')
)
console.log(processName(' Hello World ')) // "hello-world"
Option 타입 (Maybe Monad)
// null/undefined를 안전하게 다루는 타입
type None = { readonly _tag: 'None' }
type Some<A> = { readonly _tag: 'Some'; readonly value: A }
type Option<A> = None | Some<A>
// 생성자
const none: None = { _tag: 'None' }
const some = <A>(value: A): Some<A> => ({ _tag: 'Some', value })
// 패턴 매칭
function fold<A, B>(
onNone: () => B,
onSome: (value: A) => B
): (option: Option<A>) => B {
return (option) => {
if (option._tag === 'None') return onNone()
return onSome(option.value)
}
}
// map — 값이 있을 때만 변환
function mapOption<A, B>(fn: (a: A) => B) {
return (option: Option<A>): Option<B> => {
if (option._tag === 'None') return none
return some(fn(option.value))
}
}
// flatMap — Option을 반환하는 함수와 체이닝
function flatMapOption<A, B>(fn: (a: A) => Option<B>) {
return (option: Option<A>): Option<B> => {
if (option._tag === 'None') return none
return fn(option.value)
}
}
// 실전 사용
function findUser(id: string): Option<User> {
const user = users.find(u => u.id === id)
return user ? some(user) : none
}
function getEmail(user: User): Option<string> {
return user.email ? some(user.email) : none
}
// 체이닝으로 안전한 처리
const getEmailById = (id: string): Option<string> =>
pipe(
findUser(id),
flatMapOption(getEmail)
)
const result = getEmailById('user-1')
const email = fold(
() => '이메일 없음',
(e) => e
)(result)
Result 타입 (Either Monad)
// 성공/실패를 타입으로 표현
type Ok<T> = { readonly _tag: 'Ok'; readonly value: T }
type Err<E> = { readonly _tag: 'Err'; readonly error: E }
type Result<T, E = Error> = Ok<T> | Err<E>
// 생성자
const ok = <T>(value: T): Ok<T> => ({ _tag: 'Ok', value })
const err = <E>(error: E): Err<E> => ({ _tag: 'Err', error })
// 유틸리티
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
return result._tag === 'Ok'
}
function mapResult<T, U, E>(fn: (value: T) => U) {
return (result: Result<T, E>): Result<U, E> => {
if (result._tag === 'Err') return result
return ok(fn(result.value))
}
}
function flatMapResult<T, U, E>(fn: (value: T) => Result<U, E>) {
return (result: Result<T, E>): Result<U, E> => {
if (result._tag === 'Err') return result
return fn(result.value)
}
}
// 에러를 던지는 함수를 Result로 래핑
function tryCatch<T>(fn: () => T): Result<T, Error> {
try {
return ok(fn())
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)))
}
}
// 비동기 버전
async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
try {
return ok(await fn())
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)))
}
}
// 실전 사용 — 에러 처리 파이프라인
async function processUserRegistration(data: unknown): Promise<Result<User, string>> {
return pipe(
validateInput(data), // Result<ValidInput, string>
flatMapResult(hashPassword), // Result<HashedInput, string>
flatMapResult(createUser), // Result<User, string>
)
}
const result = await processUserRegistration(userData)
if (isOk(result)) {
console.log('사용자 생성:', result.value)
} else {
console.error('에러:', result.error)
}
고수 팁
fp-ts 라이브러리 활용
npm install fp-ts
import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import * as TE from 'fp-ts/TaskEither' // 비동기 Either
// Option 체이닝
const result = pipe(
O.fromNullable(users.find(u => u.id === '1')),
O.map(user => user.email),
O.filter(Boolean),
O.getOrElse(() => 'default@example.com')
)
// TaskEither — 비동기 에러 처리
const fetchUserSafe = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(e) => e instanceof Error ? e : new Error(String(e))
)
const program = pipe(
fetchUserSafe('user-1'),
TE.map(user => user.email),
TE.fold(
(error) => async () => console.error(error.message),
(email) => async () => console.log(email)
)
)
await program()