주녁, DevNote
article thumbnail

개요

객체지향 프로그래밍은 필수적인 패러다임으로서 다루는 책과 글은 많다.

반면, 함수형 프로그래밍은 필수적이지도 않으며,

어떤 부분이 좋다고 콕 집어 말하기 어렵다.

 

이는 함수형 프로그래밍이 범용 패러다임이기 때문이다.

결국, 어디에서나 잘 어울릴 수 있다는 뜻이기도 하다

 

이 글은 아래 책을 읽고 난 후 작성되었습니다.

  • 쏙쏙 들어오는 함수형 코딩 / 에릭 노먼드

목표

함수형 프로그래밍의 주요 개념과 관점을 이해하고, 함수지향 설계와 아키텍쳐를 학습한다.


여정

자주 쓰이는 콜백 함수 만들어보기

지난 시간에 작성했던 콜백을 다시 한번 보자.

function repeatAction(array, action) {
    for (let i = 0; i < array.length; i++) {
        action(array[i]);
    }
}

repeatAction(foods, cookAndEat);
function withLogging(func) {
    try {
        func();
    } catch (e) {
        console.log(e);
    }
}

withLogging(() => { saveUserData(user); }

우리는 위와 같이 함수를 주입받아 계산으로 만드는 동작을 작성했었다.

이러한 행위를 체이닝(Chaining)이라고 한다.

 

이러한 체이닝 콜백 함수가 자주 쓰이는 예시에는 foreach, map, filter, reduce가 있다.

// Array의 각 요소에 특정 행동을 적용
function foreach(array, action) {
    for (let i = 0; i < array.length; i++) {
        action(array[i]);
    }
}

// Array의 각 요소에 특정 행동을 적용한 결과를 새로운 배열로 반환
function map(array, action) {
    let copy = [];
    foreach(array, item => copy.push(action(item)));
    return copy;
}

// Array의 각 요소 중 특정 조건을 만족하는 요소만을 새로운 배열로 반환
function filter(array, test) {
    let copy = [];
    for (let i = 0; i < array.length; i++) {
        if (test(array[i])) {
            copy.push(array[i]);
        }
    }
    return copy;
}

// Array의 각 요소를 특정 행동을 적용한 결과를 하나의 값으로 축약
function reduce(array, init, action) {
    let accumulator = init;
    for (let i = 0; i < array.length; i++) {
        accumulator = action(accumulator, array[i]);
    }
    return accumulator;
}

stream을 자주 사용하는 개발자분들이라면 한번쯤은 사용해보셨을지도 모른다.

자주 사용하는 함수형 코드이니만큼 실제 코드도 복잡하지 않다.

 

익숙하지 않은 분들을 위해 예시를 들면 아래와 같다.

let customers = [
    { name: '홍길동', age: 29, email: 'email1@example.com' },
    { name: '이순신', age: 28, email: 'email2@example.com' },
];
map(customers, c => c.email); // ['email1@example.com', 'email2@example.com']

filter(customers, c => c.age > 28); // [{ name: '홍길동', age: 29, email: 'email1@example.com' }]

reduce(customers, 0, (total, c) => total + c.age); // 57

function averageAge(customers) {
    return reduce(customers, 0, (total, c) => total + c.age) / customers.length;
}

averageAge(customers); // 28.5

 

코드가 아닌 영어로 읽어도 이해하는데 크게 어려움이 없다.

단 한줄로 쉽게 이해할 수 있게 작성할 수 있다는 것이 함수형 코드의 장점이다.

 

이제 이러한 콜백 함수를 조합해서 더욱 복잡한 예시를 살펴보자


좀 더 다양한 예시

아래는 다양한 상황에서 사용할 수 있는 함수들이다.

// Array에서 특정 속성값을 추출하는 함수
function pluck(array, key) {
    return map(array, item => item[key]);
}

// 중첩된 배열을 한 단계 추출(평탄화)하는 함수
function flatMap(arrays) {
    let result = [];
    foreach(arrays, array => {
        foreach(array, item => {
            result.push(item);
        });
    });
    return result;
}

// 고객의 장바구니 목록을 추출해서 평탄화
let allCarts = pluck(customers, 'carts'); // [['A', 'B'], ['C', 'D']]
flatMap(allCarts); // ['A', 'B', 'C', 'D']
function frequenciesBy(array, action) {
    let counts = {};
    foreach(array, item => {
        let key = action(item);
        if (counts[key]) {
            counts[key]++;
        } else {
            counts[key] = 1;
        }
    });
    return counts;
}

function groupBy(array, action) {
    let groups = {};
    foreach(array, item => {
        let key = action(item);
        if (groups[key]) {
            groups[key].push(item);
        } else {
            groups[key] = [item];
        }
    });
    return groups;
}

let products = [
    { name: '사과', price: 2000, type: '과일' },
    { name: '배', price: 3000, type: '과일' },
    { name: '고구마', price: 700, type: '채소' },
    { name: '감자', price: 600, type: '채소' },
    { name: '수박', price: 5000, type: '과일' },
];
let howMany = frequenciesBy(products, product => product.type);
console.log(howMany); // { 과일: 3, 채소: 2 }

let grouped = groupBy(products, product => product.type);
console.log(grouped); // { 과일: [ { name: '사과', price: 2000, type: '과일' }, ... ], 채소: [ ... ] }
// Array를 정렬하는 함수
function sortBy(array, evaluator) {
    let result = [];
    foreach(array, item => {
        result.push(item);
    });
    result.sort((a, b) => evaluator(a, b));
    return result;
}

let sorted = sortBy(products, (a, b) => a.price - b.price);
console.log(sorted); // [ { name: '고구마', price: 700, type: '채소' }, ... ]

좀 더 복잡한 예시

이번에는 중첩된 데이터 구조에 체이닝을 사용하는 예시이다.

// 객체를 복사하는 함수(이전에 작성했던 함수)
function objectSet(obj, key, value) {
    let copy = obj.assign({}, obj); // 객체 복사
    obj[key] = value;
    return copy;
}

// 객체를 수정하는 함수
function update(obj, key, modify) {
    let oldValue = obj[key];
    let newValue = modify(oldValue);
    return objectSet(obj, key, newValue); // 객체 복사
}

// 3겹으로 중첩된 데이터 구조(cart -> shirt -> options)
let cart = {
    shirt: {
        name: 'shirt',
        price: 10000,
        quantity: 2,
        options: {
            color: 'white',
            size: 'L'
        }
    },
}

// 옵션에서 사이즈를 수정하는 함수
function updateSize(item, newSize) {
    return update(item, 'options', (options) => {
        return update(options, 'size', () => {
            return newSize;
        });
    });
}

// 카트에서 셔츠의 사이즈를 수정하는 함수
function updateShirtSizeInCart(cart, newSize) {
    return update(cart, 'shirt', (shirt) => {
        return updateSize(shirt, newSize);
    });
}

console.log(updateShirtSizeInCart(cart, 'XL'));

이게 항상 올바른 예시라고는 할 수 없지만,

이런 식으로 중첩된 데이터 구조를 분해하여 함수형 코드를 적용할 수 있다.


체이닝 사용 시 주의할 점

체이닝을 사용하는 것은 간단해보이지만,

막상 에러가 발생했을 때 디버깅을 하는 것을 어렵게 만들기도 한다.

 

따라서, 체이닝을 이용한 고차함수 사용 시에는 아래와 같은 방법이 권장된다.

 

  • 체이닝을 위한 inline 변수이름을 의미있게 짓기
  • 복잡한 체인인 경우, 중간중간 출력해보기
  • 각 단계의 타입을 명확하게 하기

마무리

이번 시간으로 함수형 프로그래밍 시리즈를 마무리한다.

아직 못 다룬 주제들이 많은데, 관심이 생기신 분들은 

아래 키워드를 검색하여 추가로 공부해보아도 좋을 것 같다.

 

  • 타임라인 원칙
  • 암묵적 시간 모델 vs 명시적 시간 모델
  • 반응형 아키텍쳐와 어니언 아키텍쳐

바쁜 시간 내주어 부족한 글을 읽어주셔서 감사합니다!

profile

주녁, DevNote

@junwork

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!