함수형 프로그래밍과 에러처리

2023. 1. 9. 22:35·프론트엔드/함수형 프로그래밍

패캠의 "TypeScript를 활용한 함수형 프로그래밍 온보딩"의 함수형 프로그래밍 4,5,6강을 듣고 정리해봤다.

함수형 프로그래밍에서는 부수효과를 찾아내고, 분리해서 공통적인 방법으로 추상화한다.

1) map을 활용해서 공통적인 부수효과를 격리해서 다룬다.

map(f): Array<A> => Array<B>

효과와 계산을 분리

// 명령형 프로그래밍
function list() {
    let html = '<ul>'; // 값 변경하는 부수효과
    for(let i = 0; i <= cart.length; i++) {
        html += item(cart[i])
    }
    html += '</ul>'
}

// 함수형 프로그래밍
function list() {
    return `<ul>
      ${cart.map(c => item(c))}
    </ul>`
}

map은 제네릭으로 구현해서 다양한 functor에 적용할 수 있다. (ex: Array, Option, Try 등)

type map<A,B> = (functor<A>, A=>B) => functor<B>

2) Option을 활용해 예외값(undefined, null) 처리하기

명령형 프로그래밍에서 예외 값 처리는 다음과 같이 if문을 활용하여 처리한다.

// item을 받아서 화면에 표시한다.
const CartItem = (item: Item):string => {
  let discountPrice = 0
  let saleText = ""
  if(item.discountPrice !== undefined) {
    discountPrice = item.discountPrice
    saleText = `(${item.discountPrice}원 할인)`
  }

  return `<li>
    <h2>${item.name}</h2>
    <p>가격: ${item.price - discountPrice} ${saleText}</p>
    <p>수량: ${item.quantity}상자</p>
  </li>`
}

이렇게 하면 CartItem 컴포넌트는 변수를 수정하는 부수효과가 내부에 존재한다.

컴포넌트가 많아지면 이런 부수효과들이 많아지고 관리하기 어려워진다.

이런 부수효과들을 공통적 방법으로 추상화하는 방법으로 Option 이 있다.

Option

Option은 값이 있을 수도 없을 수도 있는 값들을 나타내는 타입이다. 범용적으로 사용하기 위해 generic을 사용한다.

export type Some<A> = {
  readonly _tag: "Some",
  readonly value: A,
}

export type None = {
  readonly _tag: "None",
}

export type Option<A> = Some<A> | None;

이런 식으로 Some, None타입을 선언하다. 프로퍼티들을 전부 readonly로 줘서 수정이 불가능하게 한다. _tag프로퍼티를 활용해서 식별이 용이하게 한다.

Some은 값이 존재할 때 None은 값이 없을때 쓴다. 두 타입을 union으로 묶어서 Option타입을 선언한다.

Option 타입을 사용하기 편하게 여러 유틸 함수들을 만들어준다.

export const some = <A>(value: A): Option<A> => ({_tag: "Some", value})

export const none = (): Option<never> => ({_tag: "None"})

export const isSome = <A>(oa: Option<A>): oa is Some<A> => oa._tag === "Some"

export const isNone = <A>(oa: Option<A>): oa is None => oa._tag === "None" 

export const fromUndefined = <A>(a: A | undefined): Option<A> => {
  if(a === undefined) return none()
  return some(a)
}

export const getOrElse = <A>(oa: Option<A>, defaultValue: A): A => {
  if(isNone(oa)) return defaultValue
  return oa.value
}

export const map = <A,B>(oa: Option<A>, f: (a: A) => B): Option<B> => {
  // 값이 없으면 값이 없는 상태를 유지
  if(isNone(oa)) return oa
  // 값이 있으면 값을 함수에 적용한다
  return some(f(oa.value))
}

export const mapOrElse = <A,B>(oa: Option<A>, f: (a: A) => B, defaultValue: B) : B => {
  return getOrElse(map(oa,f), defaultValue)
}
  • some, none 타입인 옵션 생성, 판별 함수
  • js, ts에서는 값이 없거나 속성이 없을때 undefined가 들어가 있는 경우가 정말 많기 때문에 해당 값을 Option으로 변환하는 fromUndefined도 만든다.
  • getOrElse함수로 none타입일 때 기본값을 설정해서 사용할 수 있다.
  • map: Option<A>, f:(a: A) =>B를 인자로 받아 옵션의 A에 함수 f를적용해서 B 로 변환하고 걔가 담긴 Option<B>를 리턴한다.
  • mapOrElse: getOrElse와 map을 합성한 함수다. map으로 변환한 애의 타입이 None이면 defaultValue를 주고 아니면, 변환된 Option을 준다.

위의 함수를 이용해서 부수효과를 공통적 방법으로 추상화하고 함수형으로 코드를 리팩토링할 수 있다.

초반부 예시 코드를 리팩토링하면 다음과 같다.

const CartItem = (item: Item):string => {
  const optionDiscountPrice = O.fromUndefined(item.discountPrice)
  const discountPrice = O.getOrElse(optionDiscountPrice, 0)
  const saleText = O.mapOrElse(optionDiscountPrice,(discountPrice) => `(${discountPrice}원 할인)`, '')

  return `<li>
    <h2>${item.name}</h2>
    <p>가격: ${item.price - discountPrice} ${saleText}</p>
    <p>수량: ${item.quantity}상자</p>
  </li>`
}

discountPrice가 undefined일 때 처리를 getOrElse와 mapOrElse를 활용해서 처리했다. 컴포넌트 내부에 부수효과가 없어졌다.

그리고 부수효과를 Option을 활용해서 공통적인 방식으로 처리할 수 있다.

보기에도 좀더 깔끔하다

3) Try로 Error 처리하기

명령형 프로그래밍에서 Error 처리는 if문을 활용하거나 try catch문을 활용해서 다룬다.

const ex1 = () => {
  try {
    return getSome()    
  } catch(e) {
    return 1
  }
}

const ex2 = () => {
  if(n === 0) return 1
  return getSome()
}

const getSome = () => {
  if(n === 0) throw Error()
}

함수형 프로그래밍에서 이렇게 처리하면, 함수들의 합성이 어려워진다.

try catch의 경우 에러 발생시 함수를 중단하고 제일 가까운 catch절로 이동한다. => 매번 동일한 값에 동일한 결과를 보장하지 않는다

if문은 평가식이 순수하지 않을 경우 해당 문이 있는 함수는 순수함수가 되지 못한다. => 매번 동일한 값에 동일한 결과를 보장하지 않는다.

이러한 에러들을 따로 추출해서 공통적으로 다루기 위해서 Try를 활용한다.

Try

Try는 전체적으로 Option과 비슷하지만 다음의 큰 차이점이 있다.

  • Option<A>: 에러가 발생햇다는 사실만 중요
  • Try<E,R>: 어떤 에러가 발생했는지 그 내용도 중요

이외에는 Option과 거의 동일하게 구현한다.

type Success<R> = {
  readonly _tag: "success";
  readonly result: R;
};

type Failed<E> = {
  readonly _tag: "failed";
  readonly error: E;
};

export type Try<E, R> = Failed<E> | Success<R>;

// 이하 Option과 동일한 방식
success()
failed()
isSuccess()
isFailed()
getOrElse(ta: Try<E,R>, defaultValue: (e: E) => R) : R
map(ta: Try<E, A>, f: (a: A) => B): Try<E, B>
//

장바구니 페이지를 구현할 때,

장바구니에 담긴 항목들을 Try를 활용해서 깔끔하게 예외 처리를 할 수 있다.

type ParsedItem = { _tag: "parsedItem" } & Item;

type ParseError = {
  name: string;
  message: string;
};

// 중간중간 생략

const parseItem = (item: Item): T.Try<ParseError, ParsedItem> => {
  if (item.quantity < 1) {
    return T.failed({
      name: item.name,
      message: "상품은 반드시 한 개 이상 담아야 합니다."
    });
  } else if (item.quantity > 10) {
    return T.failed({
      name: item.name,
      message: "한 번에 10개를 초과하여 구매할 수 없습니다."
    });
  }

  return T.success({
    _tag: "parsedItem",
    ...item
  });
};

const stockItem = (item: ParsedItem): string => {
  return `
    <li>
      <h2>${item.name}</h2>
      <div>가격: ${item.price}원</div>
      <div>수량: ${item.quantity}상자</div>
    </li>
  `;
};


const errorItem = (e: ParseError): string => `
  <li style="color: red">
    <h2>${e.name}</h2>
    <div>${e.message}</div>
  </li>
`;

// 중간중간 생략

const list = (list: ArrayItem) => {
  return `
    <ul>
    ${list
      // 1. 목록에 있는 모든 아이템을 태그로 변경
      .map(item => T.getOrElse(
        T.map(item, parsedItem => renderItem(parsedItem)),
        errorItem
      ))
      // 2. 태그의 목록을 모두 하나의 문자열로 연결
      .reduce((tags, tag) => tags + tag, "")}
    </ul>
  `;
};

처리 흐름은 다음과 같다.

  1. 아이템 파싱, 에러 발생시 failed에 에러명, 에러 메시지를 담아 놓는다. 성공시에는 아이템을 담는다.
  2. getOrElse와 map을 활용해서 파싱이 성공시에는 정상적으로 stockItem을 활용해서 렌더링하고 실패시에는 errorItem을 활용해서 렌더링해준다.

이렇게 함으로써 에러를 격리하고 Try를 활용해서 공통적으로 다룬다.

깔끔하고, 부수효과도 없어지고 에러 처리 로직이 공통적 방법으로 다뤄지기 때문에 좋다.

저작자표시 (새창열림)

'프론트엔드 > 함수형 프로그래밍' 카테고리의 다른 글

함수형 프로그래밍과 부수효과  (2) 2023.01.03
'프론트엔드/함수형 프로그래밍' 카테고리의 다른 글
  • 함수형 프로그래밍과 부수효과
정현우12
정현우12
  • 정현우12
    정현우의 개발 블로그
    정현우12
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 프론트엔드 (56)
        • JavaScript, TypeScript (12)
        • 스타일링 (1)
        • React (13)
        • Next.js (4)
        • 개발 환경 (9)
        • 테스트 (3)
        • 성능 최적화 (11)
        • 함수형 프로그래밍 (2)
        • 구조 (1)
      • 프로젝트 회고 (23)
        • 이미지편집기 개발 (7)
        • 엑셀 다운로드, 업로드 공통 모듈 개발 (4)
        • 사용자 매뉴얼 사이트 개발 (3)
        • 통계자동화 솔루션 개발 (1)
        • 엑셀 편집기 개발 (5)
        • API 플랫폼 (1)
        • 콜센터 솔루션 OB 캠페인 (1)
        • AI 스튜디오 (1)
      • 백엔드 (0)
  • 블로그 메뉴

    • 홈
    • 포트폴리오
    • 태그
  • 인기 글

  • 태그

    React-boilerplate
    JavaScript
    렌더링 성능 최적화
    webpack
    사용자 매뉴얼 사이트
    이미지 편집기
    라이브러리 선정
    frontend
    엑셀 에디터
    로딩 성능 최적화
    React
    Next.js
    엑셀
    memoization
    웹 성능 최적화
    회고
    TypeScript
    Github Actions
    useReducer
    커스텀 훅
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
정현우12
함수형 프로그래밍과 에러처리
상단으로

티스토리툴바