본문 바로가기

Study/JavaScript

[JavaScript] 함수형 프로그래밍(1)

요즘 흥미롭게 읽고 있는 책이 하나 있는데, 함수형 프로그래밍에 대해 알려주는 책이다.

 

 

무슨 개발이 되었든 함수를 사용하여 코드를 작성해두면 어떤 로직을 재사용하기 편리한 것 같다.

 

가령 쇼핑몰에서 사용자가 구매할 물건을 장바구니에 넣는 로직을 작성한다고 할 때

 

/** 사용자가 고르려 하는 상품 **/
const goods = {
    id: 200,
    name: "식기세척기",
    price: 560000,
    isPurchasable: true
}

/** 장바구니 배열 **/
const cart = [];

/** 장바구니에 사용자가 선택한 상품을 추가하는 함수 **/
function addGoodsToCart(product) {
    cart.push(product)
}

/** 카트 버튼 **/
const btnAddCart = document.getElementById("btnAddCart");

/** 카트버튼 클릭 이벤트 리스너 **/
btnAddCart.addEventListener("click", () => addGoodsToCart(goods))

 

이렇게 카트 버튼을 클릭하면 전역으로 선언된 카트 배열에 사용자가 고른 상품을 밀어넣는 액션을 갖는 함수를 만들 수 있다.

 

다만 이런 형태는 다소 이상하다.

 

함수형 코딩의 핵심은 함수의 인자를 통해 함수 내부에서 액션이나 계산이 일어나고 리턴(출력)이 발생하는 것이라 생각한다.

 

/** 장바구니에 사용자가 선택한 상품을 추가하는 함수 **/
function addGoodsToCart(cartArray, product) {
    const carts = [...cartArray]
    return carts.push(product)
}

 

이렇게 장바구니 배열을 매개변수로 받아 새로운 배열로 복사한 후 사용자가 선택한 상품을 밀어넣어 리턴해 주는 방식이 깔끔해보인다..

 

전역으로 선언한 배열을 참조하여 작성된 함수는 다른 로직에 영향을 줄 가능성이 높으며, 해당 배열이 아니라면 재사용면에서 효율이 떨어진다. 그러나 배열까지 매개변수로 받아주면 전역으로 선언한 배열에 영향을 주지 않으면서 다른 배열에 대해서도 재사용할 수 있다.

 


 

이 책에서 처음 접한 내용은 액션, 계산, 데이터의 차이를 구분하는 것이다.

 

액션은 실행 시점과 횟수에 의존하는 것으로 다른 말로 부수효과(side-effect), 부수 효과가 있는 함수(side-effect function), 순수하지 않은 함수(impure function)라고 부른다. 예를 들면 이메일을 보내는 로직이나 DB를 읽어들이는 행위가 포함된다.

 

계산은 어떤 입력에 대해 출력을 계산하는 것으로 다른 말로 순수함수(pure function) 또는 수학함수(mathematical function)라고 부른다. 최대/최솟값을 찾거나 이메일 주소의 형식이 올바른지 확인하는 작업이다.

 

데이터는 이벤트에 대한 사실이다. 사용자가 입력한 이메일 주소 그 자체 등이 포함된다.

 

조금 더 쉽게 이해하기 위해 장보기 과정을 액션, 계산, 데이터로 구분했을 때에 대한 예시를 들고 있다.

 

냉장고 확인하기 : 이 동작은 액션에 해당한다. 냉장고를 확인하는 시점에 따라 냉장고에 들어있는 물건은 항시 달라질 수 있기 때문이다.

운전해서 상점으로 가기 : 이 동작 또한 액션에 해당한다. 

필요한 물품 구입하기 : 구입하는 행위 자체가 액션이 될 수 있다. 누군가 브로콜리라는 상품을 구매하면 브로콜리가 매진될 수 있기 때문에 시점에 의존한다고 볼 수 있다. 그러므로 액션이다.

운전해서 집으로 복귀 : 이 동작 또한 액션이다. 이미 집에 있다면 현재 상점에 있는 것이 아니므로 상점에서 집으로 올 수 없다. 이 또한 시점에 의존하므로 액션이다.

 

그럼 장보기의 모든 과정은 액션인가? 라는 물음을 제시한다면 그런 것은 아니다. 계산의 과정과 데이터도 분명 들어있다.

 

냉장고를 확인하는 것 자체는 액션이 맞다. 그러나 냉장고 안에 있는 물품들에 대한 정보는 데이터이다. 

운전해서 상점으로 가는 것 또한 데이터가 행위 안에 내재하고 있다. 상점의 위치나 경로 정보를 데이터로 볼 수 있다.

필요한 물품을 구입하는 행위 안에서 구입의 과정을 몇가지 단계로 나눌 수 있다. 예를 들어 우유를 구매해야 한다면 이미 냉장고에 우유가 있다는 전제하에 현재 재고와 구매하고자 하는 재고, 두개의 데이터를 계산하여 장보기 목록을 만들고 이 목록을 바탕으로 구매라는 행위가 일어난다.

 

다시 말해서 현재 재고는 데이터, 필요한 재고 또한 데이터, 두 데이터의 차이를 표현하는 계산, 계산을 바탕으로 도출한 장보기 목록이라는 데이터, 데이터를 기반으로 구매라는 행위가 물품구매라는 액션안에 내재하고 있는 것이다.

 

운전을 해서 다시 집으로 복귀하는 과정은 이 책에서 다루고자 하는 과정이 아니라 소개하고 있지 않지만 앞에서 살펴본 "운전해서 상점으로 가기"라는 내용의 반대과정을 대입해 볼 수 있을 것 같다. 

 


 

쿠폰에 관심있는 구독자들에게 이메일로 쿠폰을 매주 보내주는 서비스를 구현한다. 커다란 이메일 데이터베이스가 존재하고, 여기에는 이메일별로 각 사용자가 추천한 친구수를 기록한다.

 

email rec_count
john@coldmail.com 2
sam@pmail.co 16
linda1989@oal.com 1
jan1940@ahoy.com 0
mrbig@pmail.co 25
lol@lol.lol 0
coupon rank
MAYDISCOUNT good
10PERCENT bad
PROMOTION45 best
IHEARTYOU bad
GETADEAL best
ILIKEDISCOUNT good

 

이 데이터베이스를 가지고 서비스를 구현한다고 할 때 무엇을 알아야하고 결정해야하고 어떤 일을 해야할까?

 

서비스 구현에 필요한 액션, 데이터, 계산이라는 과정을 생각해보아야 한다.

 

쿠폰을 사용자에게 이메일로 보내기 위해서 DB에서 구독자 목록을 가져와야 한다. 구독자 목록은 계속 바뀔 수 있기 때문에 구독자 목록을 가져오는 작업은 실행 시점에 의존하고, 액션이라고 할 수 있다.

 

액션을 통해 구독자 목록을 가져왔다면 이는 데이터가 된다.

 

비슷한 방법으로 쿠폰 데이터베이스에서 쿠폰 목록을 가져오고(액션) 가져온 쿠폰 목록은 데이터이다. 쿠폰 목록 데이터는 DB 쿼리 이벤트에 대한 사실이다.

 

데이터베이스로부터 구독자 목록과 쿠폰 목록을 얻었으니 이제 이메일을 보내기만 하면 된다. 이 서비스에서는 친구를 10이상 추천한 사람에게 best 쿠폰을 보내줄 것이고 bad 레벨의 쿠폰은 사용자가 선호하지 않는 쿠폰이기 때문에 어느 누구에게도 보내지 않는다.

 

이메일을 전송하기 위해 모든 준비는 다 끝났고, 이를 바탕으로 이메일을 구독자들에게 전송한다.

이메일 보내는 과정을 조금 더 자세하게 살펴보면 이메일을 보내기 전에 이메일 목록을 만드는게 의아하다고 생각할 수 있으나 함수형 프로그래밍 관점에서 자연스러운 방법임을 소개하고 있다.

이메일 목록을 만드는 과정은 데이터베이스로부터 구독자 목록과 쿠폰 목록을 받아 계산하여 이메일 목록이라는 데이터가 산출되는 것이다.

 

이제 쿠폰 보내는 과정을 구현한다.

구독자 데이터를 바탕으로 보낼 쿠폰의 등급을 결정하고 쿠폰의 등급을 매겨 발송한다.

 

위의 구독자 테이블에서 sam@pmail.co을 예로 든다면 자바스크립트에서 객체로 다음과 같이 표현할 수 있다.

 

const subscriber = {
    email: "sam@pmail.co",
    recCount: 16
}

 

쿠폰의 등급은 문자열이다. 그리고 쿠폰의 등급을 결정하는 것은 함수이다.

 

function subCouponRank(subscriber) {
    if (subscriber.recCount >= 10) {
        return "best"
    } else {
        return "good"    
    }
}

 

매개변수로 어떤 값이 입력되었을 때,  매개변수를 참조하여 10보다 크거나 같은지 계산하고 결과에 따라 best나 good을 출력하도록 한다.

 

이 함수가 어떤 구독자가 어떤 등급의 쿠폰을 받을 지 결정하는 함수이다. 재사용 가능하다고 보인다.

 

쿠폰 또한 객체로 나타낼 수 있으며

 

const coupon = {
    code: "10PERCENT",
    rank: "bad"
}

 

특정 등급의 쿠폰을 선택하는 계산을 함수로 구현할 수 있다.

 

function selectCouponByRank(coupons, rank) {
    const ret = []
    for (const coupon of coupons) {
        let co = coupon
        if (co.rank === rank) {
            ret.push(co.code)
        }
    }
    
    return ret
}

 

이렇게 쿠폰의 등급까지 결정했다면 구독자가 받을 이메일을 계획하는 계산이 들어가고 이 또한 함수로 구현한다. 그리고 보낼 이메일 목록을 준비하여 준비된 메일을 보내기만 하면 된다.

 

/** 구독자가 받을 이메일을 계획하기 **/
function emailForSubscriber(subscriber, goods, bests) {
    const rank = subCouponRank(subscriber)
    if (rank === "best") {
        return {
            from: "보내는 사람",
            to: subscriber.email,
            subject: "제목",
            body: `${최고등급의 쿠폰을 보내드립니다}: ${bests.join(", ")}`
        }
    } else {
        return {
            from: "보내는 사람",
            to: subscriber.email,
            subject: "제목",
            body: `${상급 쿠폰을 보내드립니다}: ${goods.join(", ")}`
        }
    }
}

/** 보낼 이메일 목록 준비 **/
function emailsForSubscribers(subscribers, goods, bests) {
    const emails = []
    for (const subscriber of subscribers) {
        const email = emailForSubscriber(subscriber, goods, bests)
        emails.push(email)
    }
    
    return emails
}

/** 이메일 보내기 **/
function sendIssue() {
    const coupons = fetchCouponsFromDB()
    const goodCoupons = selectCouponsByRank(coupons, "good")
    const bestCoupons = selectCouponsByRank(coupons, "best")
    const subscribers = fetchSubscribersFromDB()
    const emails = emailsForSubscribers(subsribers, goodCoupons, bestCoupons)
    
    for(const email of emails) {
        emailSystem.send(email)
    }
}

 

모든 것을 코드로 구현했을 때 데이터를 파악하는 것부터 계산과정과 추가적으로 들어가는 데이터를 도출하였고 액션단위로 모든 것을 묶어두었다. 데이터는 사용 상의 제약이 많고 액션은 제약이 없기 때문에 데이터를 먼저 구현하고 계산을 구현한 후에 액션을 구현하는 것이 함수형 프로그래밍의 가장 일반적인 구현 순서이다.

 

 

 

 

 

 

출처

에릭 노먼드(2022). 쏙쏙 들어오는 함수형 코딩. 파주: 제이펍