본문 바로가기
Frontend/Etc

액션, 계산 분리해보기(함수형 프로그래밍)

by 이의찬 2024. 1. 8.

1. Why?

함수형 프로그래밍은 오래동안 도전해보고 싶은 영역이였다.특히 테오님의 블로그에서 함수형 프로그래밍을 소개한 글을 읽고서는 '나도 코드를 저렇게 써봐야지'라고 맘먹고 "쏙쏙 들어오는 함수형 코딩" 책도 구매했었다.

나도 코드 이쁘게 짜고싶어 ㅠ

내가 쓴 코드를 리팩토링하려다 너무 복잡해서 때려친 적도 있었고, 다른 팀원이 질문을 했을 때, 로직을 살펴보다가 한시간 넘게 잡아먹은 적도 있었다. 이런 상황을 겪으며 함수를 나누고 복잡성을 낮추는것의 필요성을 느꼈기 때문이다.

 

하지만, 싸피의 타이트한 일정(7주간 프로젝트 X 3번 + 쉬는 기간 없음 + 남는 시간에는 자소서 쓰고 코테 준비) + 프로덕트에 대한 욕심(실제로 사용 가능한 프로덕트를 만들고 싶어!) 때문에 구현을 마치기에도 늘 벅차서 미뤄두고 있었다.

그런데 마침! 원티드에서 12월에 진행한 프리온보딩 프론트엔드 챌린지의 한 챕터가 데이터 / 계산 / 액션 분리였다. 그리고 또 마침! 아워홈에서 사전과제를 던져줬다.

 

이제 부담없이 수정할 수 있는 토이 프로젝트가 생겼으니, 코드를 한 번 고쳐보자.


2. 그래서 어떻게 해야하는데?

함수형 프로그래밍의 정의

  • 순수 함수: Pure Function, 인자에만 의존하고 부수 효과가 없는 함수 -> 같은 인자를 넣으면 같은 결과를 반환
    • ex) 수학에서의 함수 등
  • 부수 효과: Side Effect, 함수 내부에서 동작하는 행동들이 함수 외부로 영향을 끼치는 것
    • ex) 이메일 보내기, 파일 읽기 등 

함수형 프로그래밍의 사전적 정의는 순수 함수를 사용하고 부수 효과를 지양하는 것.

-> 좋은 말이긴 한데 이메일 보내기가 없는 이메일 소프트웨어가 무슨 소용?

그래서 책에서는 새로운 개념을 제시한다.

 

액션, 계산, 데이터로 나눠서 살펴보자!

  • 액션: 외부 세계와 소통 -> 실행 시점과 횟수에 의존, 부수 효과 일으킴
  • 계산: 같은 입력 -> 같은 출력, 순수 함수
  • 데이터: 이벤트에 대한 사실. 문자열, 객체 등 단순한 값 그 자체

이번 글에서 할 일은 액션과 계산을 나누는 것이다. 

액션은 이런 식으로 연결된 코드 전체로 ‘전염’ 됩니다. 하지만 액션을 사용하지 않을 수는 없습니다. 결국 우리가 프로그램을 통해 목적한 결과를 달성하려면 최종적으로 외부 세계에 영향을 미쳐야 하기 때문입니다.
(by 프리온보딩 강사님)

액션: 계산 야미~~

그래서 액션이 계산을 전염시키지 못하도록 액션과 계산을 확실히 분리해야한다. 이를 통해 액션을 최소화하고 계산함수를 많이 만들어서 관리하는 것이 함수형 프로그래밍의 첫 걸음이다.

예시 코드

function App() {
  // 데이터
  const [count, setCount] = useState(0)
  // 액션
  const onClick = () => setCount(count + 1)
  
  return <button onClick={onClick}>{count}</button>
}

버튼을 누르면 1씩 count가 증가하는 간단한 react 코드다. 여기서 액션과 계산을 분리해보자.

잘못된 분리

function App() {
  // 데이터
  const [count, setCount] = useState(0)
  // 계산....?
  const increase = () => setState(count + 1)
  // 액션
  const onClick = () => increase()
  
  return <button onClick={onClick}>{count}</button>
}

단순히 함수만 쪼갠다고 액션과 계산이 분리되는게 아니다.

  • 계산은 명시적인 입출력으로 이루어져야한다.
  • 같은 입력에 대해서는 항상 같은 출력값을 돌려줘야 한다.
  • 실행이 되어도 외부세계에 영향을 주지 않아야한다. 

하지만 const increase는 명시적인 입출력 대신 외부세계로부터 암묵적 입력인 count를 받고, 암묵적 출력인 setCount를 돌려주고 있다. 

 

잘 된 분리

function App() {
  // 데이터
  const [count, setCount] = useState(0)
  // 계산
  const increase = (value) => value + 1
  // 액션
  const onClick = () => setCount(increase(count))

  return <button onClick={onClick}>{count}</button>
}

암묵적 입출력을 제거하고, 명시적인 입력인 value를 받고, 명시적인 출력인 value + 1을 돌려주도록 수정했다. 

  • 명시적인 입출력, 같은 입력 => 같은 출력, 외부세계에 영향 X

세 가지에 모두 충족되는 계산 함수, 즉 순수 함수가 만들어졌다.


3. 도전!

아래의 코드들은 제가 이해한대로 수정한거라 잘못된 수정이 있을수도 있습니다.
혹시라도 이상하다 싶은 부분이 있다면 댓글로 알려주시면 정말 감사드리겠습니다.
 

GitHub - Legitgoons/eCommerce-ourHome: 아워홈의 사전과제입니다.

아워홈의 사전과제입니다. Contribute to Legitgoons/eCommerce-ourHome development by creating an account on GitHub.

github.com

마침 사전과제로 만들어 놓은 토이프로젝트가 있기에 이 코드를 수정했다. 전체 코드는 위의 GitHub에서 확인할 수 있다. 

장바구니 수량 변경

위에서 수량 변경하는 부분 로직이다.

기존 코드

//import ...

interface CartBoxProps {
  data: CartItem;
}
export default function CartBox({ data }: CartBoxProps) {
  const { imgSrc, name, price, originalPrice, quantity } = data;
  const [itemQuantity, setItemQuantity] = useState<number>(quantity);
  const dispatch = useCartDispatch();

  const decreaseQuantity = () => {
    if (itemQuantity > 1) {
      const newQuantity = itemQuantity - 1;
      setItemQuantity(newQuantity);
      dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
    }
  };

  const increaseQuantity = () => {
    const newQuantity = itemQuantity + 1;
    setItemQuantity(newQuantity);
    dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
  };

  const totalItemPrice = itemQuantity * price;

//return TSX...

위의 코드를 먼저 수정해보자. 어떤 부분을 고쳐야 할까?

  const decreaseQuantity = () => {
    if (itemQuantity > 1) {
      const newQuantity = itemQuantity - 1;
      setItemQuantity(newQuantity);
      dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
    }
  };

  const increaseQuantity = () => {
    const newQuantity = itemQuantity + 1;
    setItemQuantity(newQuantity);
    dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
  };
  • 문제 : 액션과 계산이 하나의 함수 decreaseQuantity, increaseQuantity에서 처리되고 있다. 위 함수를 아래와 액션과 계산으로 분리할 수 있다.
  • 액션 : 변경된 수량을 장바구니에 담는다.
  • 계산 : +-1을 계산한다.

수정 코드

// import ...

interface CartBoxProps {
  data: CartItem;
}
export default function CartBox({ data }: CartBoxProps) {
  const { imgSrc, name, price, originalPrice, quantity } = data;
  const [itemQuantity, setItemQuantity] = useState<number>(quantity);
  const dispatch = useCartDispatch();

  // 계산들
  const increase = (num: number) => {
    return num + 1;
  };

  const decrease = (num: number) => {
    return num - 1;
  };
  
  // 액션들
  const decreaseQuantity = () => {
    if (itemQuantity > 1) {
      const newQuantity = decrease(itemQuantity);
      setItemQuantity(newQuantity);
      dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
    }
  };

  const increaseQuantity = () => {
    const newQuantity = increase(itemQuantity);
    setItemQuantity(newQuantity);
    dispatch({ type: 'UPDATE_QUANTITY', itemName: name, newQuantity });
  };

  const totalItemPrice = itemQuantity * price;
 
 // return TSX...

액션은 그대로 decreaseQuantity, increaseQuantity에 두고 수량을 변경하는 계산함수 increase와 decrease를 분리했다.

총 가격 변경

장바구니 화면 하단 총 금액, 배송비, 결제 금액을 보여주는 부분이다.

기존 코드

/** CartItem
 * @param {string} imgSrc 이미지 주소
 * @param {string} name 제품명
 * @param {number} price 제품 가격
 * @param {number} originalPrice 제품 원래 가격 (할인 전 가격, 옵션)
 * @param {number} quantity 수량
 */

export interface CartItem {
  imgSrc: string;
  name: string;
  price: number;
  originalPrice?: number;
  quantity: number;
}
// import ...

export default function CartTemplate() {
  const cart = useCartState();
  const [totalPrice, setTotalPrice] = useState(0);
  const [shippingFee, setShippingFee] = useState(3000);

  useEffect(() => {
    const newTotalPrice = cart.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
    setTotalPrice(newTotalPrice);

    if (newTotalPrice >= 30000 || newTotalPrice === 0) {
      setShippingFee(0);
    } else {
      setShippingFee(3000);
    }
  }, [cart]);

  const totalAmount = totalPrice + shippingFee;

// return TSX...

이 코드는 위에 코드보다는 조금 복잡한데 로직을 간단하게 설명하자면

  1. useEffect를 사용해서 Context Api로 선언해둔 cart의 상태를 추적
  2. cart가 변경될때마다 newTotalPrice 함수를 실행시켜서 TotalPrice의 값을 변경시킴
  3. newTotalPrice의 결과값에 따라 shippingFee의 값도 변경

인데, useEffect 내에 액션과 계산이 얽혀있다.

  • 액션: TotalPrice의 값 변경 / shippingFee의 값 변경
  • 계산: 각 항목 당 수량*가격을 한 다음 더하기 / 매개변수에 따라 3000 or 0을 return하기
혹시 Context Api를 어떻게 사용했는지 궁금하다면 여기로

수정 코드

// import ...

// 계산
const calculateTotalPrice = (cart: CartItem[]) =>
  cart.reduce((total, item) => total + item.price * item.quantity, 0);

const calculateShippingFee = (totalPrice: number) =>
  totalPrice >= 30000 || totalPrice === 0 ? 0 : 3000;

export default function CartTemplate() {
  //데이터
  const cart = useCartState();
  // 액션
  const totalPrice = calculateTotalPrice(cart);
  const shippingFee = calculateShippingFee(totalPrice);
  const totalAmount = totalPrice + shippingFee;

// return TSX...
  • calculateTotalPrice에서는 매개변수로 cartItem 배열을 받는다. reduce를 이용해 각 제품마다 가격 * 수량을 한 다음 모두 더해서 return 해준다.
  • calculateShippingFee에서는 매개변수에 따라 0 or 3000을 return해준다.
  • 아래의 액션들에서는 계산함수에 매개변수를 넣어주고, 돌려받은 결과값을 출력한다.

이렇게 액션과 계산함수를 분리해봤다. 이후에도 책을 더 읽어보고 코드에 적용할 내용이 있다면 추가로 실습해서 작성할 계획이다.


4. 래퍼런스

프리온보딩 프론트엔드 챌린지 12월

쏙쏙 들어오는 함수형 코드

https://dev-in-book.github.io/imfp/

 

3장 액션과 계산, 데이터의 차이를 알기 | 쏙쏙 들어오는 함수형 코딩

이번 장에서 살펴볼 내용

dev-in-book.github.io

https://velog.io/@teo/functional-programming

 

다시 쓰는 함수형 프로그래밍

> 참 좋은데 어떻게 표현할 방법이 없네... 오랜 기간 개발을 공부하게 되면서 여러가지 패러다임의 변화를 겪었는데 그 중에서 인상깊었던 것중에 하나는 객체지향 패러다임에서 함수형 패러다

velog.io