주녁, DevNote
article thumbnail

1. 개요

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

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

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

 

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

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

 

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

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

2. 목표

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


3. 여정

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

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

<javascript />
function repeatAction(array, action) { for (let i = 0; i < array.length; i++) { action(array[i]); } } repeatAction(foods, cookAndEat);
<javascript />
function withLogging(func) { try { func(); } catch (e) { console.log(e); } } withLogging(() => { saveUserData(user); }

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

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

 

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

<javascript />
// 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을 자주 사용하는 개발자분들이라면 한번쯤은 사용해보셨을지도 모른다.

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

 

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

<javascript />
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

 

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

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

 

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


3.2. 좀 더 다양한 예시

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

<javascript />
// 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']
<javascript />
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: '과일' }, ... ], 채소: [ ... ] }
<javascript />
// 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: '채소' }, ... ]

3.3. 좀 더 복잡한 예시

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

<javascript />
// 객체를 복사하는 함수(이전에 작성했던 함수) 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'));

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

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


3.4. 체이닝 사용 시 주의할 점

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

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

 

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

 

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

4. 마무리

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

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

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

 

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

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

profile

주녁, DevNote

@junwork

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