본 포스트의 내용은 FUNCTIONAL PROGRAMMING IN JAVASCRIPT (함수형 자바스크립트) 교재의 스터디 내용을 기반으로 작성하였습니다

이 장의 내용

  • 함수형 사고방식
  • 함수형 프로그래밍의 정의와 필요성
  • 불변성, 순수함수 원리
  • 함수형 프로그래밍 기법 및 그것이 설계 전반에 미치는 영향

근래의 웹 플랫폼은 굉장히 빠른 속도로 발전 중이고 브라우저 역시 꾸준히 진화하고 있지만, 최종 사용자들의 요건이 웹 어플리케이션 설계 방식에 지대한 영향을 미칩니다.

오늘날의 웹은 수년 전 웹과는 근본적으로 다릅니다. 과거에 기술적으로 거의 불가능했던 일들이 지금은 얼마든지 구현가능 합니다.

최근 유행하기 시작한 리액티브 프로그래밍 은 데이터 흐름(data flow)과 변경 전파(Propagation of change)에 초점을 둡니다. 이 개념은 자바스크립트 비동기 또는 이벤트 중심(event-driven)코드를 다룰 때에도 아주 중요합니다.

이러한 흐름에 맞추어 반드시 자문해봐 할 애플리케이션의 설계 요소는

확장성: 추가 기능을 지원하기 위해 계속 코드를 리팩토링 해야 하는가?
모듈화 용이성: 파일 하나를 고치면 다른 파일도 영향을 받는가?
테스트성 : 함수를 단위 테스트하기 어려운가?
헤아리기 쉬움: 체계도 없고 따라가기 어려운 코드인가

이 중 한가지라도 “예” 또는 “잘 모르겠는데요”라고 대답했다면 이 책은 여러분의 생산성을 높여주는 적합한 안내서가 될것입니다.

1.1 함수형 프로그래밍은 과연 유용한가?

자바스크립트의 맥락에서 보면 FP(Functional Programming)사고방식은 자바스크립트만의 매우 표현적인 특성을 가다듬어, 깔끔하면서도 모듈적인, 테스트하기 좋은 간결한 코드들 작성하는데 도움이 됩니다.

사실 자바스크립트는 함수형 스타일로 작성해야 더 효과적이라는 측면이 오랫동안 간과되었습니다. 자바스크립트라는 언어를 많이 오해한 부분도 있지만 언어 내부에 상태를 적절히 관리할 장치가 마땅치 않았던 이유도 있습니다. (상태관리를 개발자에게 떠넘기는 동적인 플랫폼이기 떄문)

결국 이는 각종 애플리케이션 버그를 양산하는 근원이 되었습니다.

FP로 코드를 작성하면 대부분의 문제가 해결됩니다. 순수함수에 기반을 두고 이미 검증된 기법과 관례에 따라 구현하면 코드가 점점 복잡해지더라도 헤아리기 쉬운 방향으로 작성할 수 있습니다.

1.2 함수형 프로그래밍이란?

함수형 프로그래밍의 진정한 목표는 애플리케이션의 부수효과(side effect)를 방지 하고 상태변이(mutation of state)를 감소하기 위해 데이터의 제어흐름과 연산을 추상화(abstract)하는 것입니다.

간단한 예제로서 Hello world로 시작해보겠습니다.

  • javascript
1
document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>';

이 예제는 모든 것을 하드코딩한 예제로서 메시지를 동적으로 표시할 수 없습니다. 내용이나 형식을 바꾼다든가, 타깃 요소를 달리한다든지 할 땐 표현식을 전부 재작성해야 합니다. 함수를 만들어 달라지는 부분만 매개변수로 주면 같은 코드를 다시 사용할 수 있습니다.

  • javascript
1
2
3
4
5
6
function printMessage(elementId, format, message) {
document.querySelector(`#${elementId}`).innerHTML =
`<${format}>${message}</${format}>`;
}

printMessage('msg', 'h1', 'Hello World');

이런식으로 작성하면 조금 나아지는듯 하나 완벽히 재사용 가능한 코드는 아닙니다.

메시지를 HTML 페이지 대신 파일에 쓴다면 어떨까요? 매개변수가 단순히 스칼라(scalar)값이 아닌, 특정 기능을 함수에 추가하여 매개변수로 전달하는, 다시 말해 함수를 매개변수화(parameterize)하는 전혀 다른 차원의 과정을 떠올려야 합니다. 함수형 프로그래밍은 함수를 아주 왕성하게 활용합니다.

함수형 printMessage
  • javascript
1
2
3
4
var printMessage = run(addToDom('msg'), h1, echo);
printMessage('Hello World');

//임시함수 run에 관한 자세한 내용은 http://mng.bz/nmax 참조

언뜻 봐도 기존 코드와는 다릅니다. 일단 h1은 스칼라값이 아닌, addToDom, echo와 같은 함수입니다. 작은 함수들을 재료로 새로운 함수를 만들어내는 것처럼 보입니다.

여기에는 그럴 만한 이유가 있습니다. 위 예제는 재사용성과 믿음성(reliability)이 좋고 이해하기 쉬운, 더 작은 조각들로 프로그램을 나눈 후, 전체적으로 더 헤아리기 쉬운 형태의 프로그램으로 다시 조합하는 과정을 나타냅니다.

모든 함수형 프로그래밍이 이 기본원리를 따릅니다. run함수는 세 함수를 마치 자전거 체인처럼 연결해서 한 함수의 반환값이 다른 함수의 입력값으로 전달되게끔 합니다. 그래서 echo가 “Hello World” 문자열을 반환하면 h1으로 전달되고, 마지막으로 이 함수의 결과값이 addToDom에 넘어갑니다.

함수형 코드는 왜 이런 모습일까요? 핵심개념은 본연의 기능은 그대로 간직한 채 코드 자체를 매개변수화 하는 것 입니다. 이렇게 하면 내부로직이 변경되지 않고도 printMessage를 2회 표시하거나, 또는 h2요소를 DOM대신 콘솔에 출력하는 일도 수월해 집니다.

printMessage를 확장
  • javascript
1
2
3
var printMessage = run(console.log, repeat(2), h2, echo);

printMessage('Get Functional')

함수형/비함수형 해법을 견주어보면 근본적으로 스타일이 다르다는 것을 알 수 있습니다. 이는 FP특유의 선언적 개발방식 때문에 그렇습니다. 함수형 프로그래밍을 온전히 이해하려면, 먼저 그 이면에 깔려 있는 다음 기본 개념을 숙지해야 합니다.

  • 선언적 프로그래밍
  • 순수함수
  • 참조 투명성
  • 불변성

1.2.1 함수형 프로그래밍은 선언적

함수형 프로그래밍은 큰 틀에서 선언적(declarative)프로그래밍 패러다임에 속 합니다. 내부적으로 코드를 어떻게 구현했는지, 데이터는 어떻게 흘러가는지 밝히지 않은 채 연산/작업을 표현하는 사상입니다.

아직은 C#, C++ 등의 구조적/객체지향 언어가 지원하는 명령형(imperative) 또는 절차적(procedural) 모델이 더 많이 쓰입니다. 명령형 프로그램은 위에서 아래로 축 늘어놓은 순차열 코드에 불과합니다.

숫자배열의 원소들을 모두 제곱수로 바꾸는 간단한 예제를 봅시다. 명령형은 다음과 같은 모습입니다.

  • javascript
1
2
3
4
5
6
var array = [0,1,2,3,4,5,6,7,8,9];
for(var i=0; i< array.length; i++) {
array[i] = Math.pow(array[i], 2);
}

//array --> [0,1,4,9,16,25,36,49,64,81]

명령형 프로그래밍은 컴퓨터에게 원하는 작업(루프를 반복하면서 각 숫자의 제곱수를 계산)을 어떻게 하는지 상세히 이릅니다. 사실 이게 가장 흔한 코딩 방법이고 여러분도 처음에 이런 코드를 떠올렸을 겁니다.

이와 달리 선언적 프로그래밍은 프로그램의 서술부(description)평가부(evaluation)를 분리하여, 제어 흐름이나 상태 변화를 특정하지 않고도 프로그램 로직이 무엇인지를 표현식(expression)으로 나타냅니다. (SQL 구문도 선언적 프로그래밍의 한 예)

같은 작업이라도 함수형으로 접근하면, 개발자가 각 요소를 올바르게 작동시키는 일에만 전념하고 루프 제어는 시스템의 다른 파트에 일임할 수 있습니다. 다음과 같은 힘든 일은 Array.map()에게 모두 맡기면 그만입니다.

  • javascript
1
2
3
4
5
6
7
[0,1,2,3,4,5,6,7,8,9].map(
function(num) {
return Math.pow(num, 2);
}
)

// --> [0,1,4,9,16,25,36,49,64,81]

이전 코드와 비교하면 루프 카운터를 관리하고 배열 인덱스에 정확하게 접근하는 일 따위는 개발자가 신경 쓸 필요가 없어 부담이 줄어듭니다.

사실 코드가 길어지면 버그가 날 가능성도 높아지고, 일반 루프는 함수로 추상하지 않는 한 재사용 자체가 안됩니다. 지금부터 우리가 할 일이 바로 함수로 추상하는 작업입니다.

3장에서는 수동 루프를 완전히 들어내고 함수를 매개변수로 받는 map, reduce, filter 같은 일급 고계함수(higher-order function)를 이용해 재사용성, 확장성이 우수한 선언적 코드로 대체합니다.

루프를 함수로 추상하면 ES6부터 새로 선보인 람다표현식(lambda expression)이나 화살표 함수(arrow fucntion)를 사용할 수 있습니다. 람다 표현식은 함수 인수로 전달 가능한 익명함수(anonymous function)를 대체할 수 있는 깔끔한 수단입니다.

  • javascript
1
2
[0,1,2,3,4,5,6,7,8,9].map(num => Math.pow(num, 2));
// --> [0,1,4,9,16,25,36,49,64,81]

왜 루프를 제거해야 할까요? 루프는 재사용하기도 어렵거니와 다른 연산에 끼워 넣기도 어려운 명령형 구조물입니다. 또 루프는 성격상 반복할 때마다 값이나 상태가 계속 바뀝니다.

그러나 함수형 프로그램은 무상태성(statelessness)불변성(immutability)을 지향합니다. 무상태 코드는 전역 상태를 바꾸거나 혼선을 일으킬 가능성이 단 1%도 없습니다. 상태를 두지 않으려면 부수효과와 상태 변이를 일으키지 않는 순수함수(pure funciton)를 써야합니다.

1.2.2 순수함수와 부수효과

함수형 프로그래밍은 순수함수로 구성된 불변 프로그램 구축을 전재로 합니다.
순수함수의 특성을 정리하면 다음과 같습니다.

  • 주어진 입력에만 의존할 뿐, 평가 도중 또는 호출 간 변경될 수 있는 숨겨진 값이나 외부상태와 무관하게 동작합니다.
  • 전역 객체나 레퍼런스로 전달된 매개변수를 수정하는 등 함수 스코프 밖에서 어떠한 변경도 일으키지 않습니다.
    위 요건이 성립되지 않는 함수는 모두 불순(impure) 하다고 볼수 있습니다.

다음 함수를 봅시다.

  • javascript
1
2
3
4
var counter = 0;
function increment() {
return ++counter;
}

이 함수는 자신의 스코프에 없는 외부 변수 counter를 읽고 수정하므로 불순합니다.
일반적으로 외부 자원을 상대로 데이터를 읽고 쓰는 함수는 부수효과를 동반합니다.
Date.now()처럼 많이 쓰이는 날짜/시간 함수도 미리 헤아릴 수 있는 일정한 결과값을 내지 않기 때문에 순수함수가 아닙니다.

여기서 counter는 암시적(implicit) 전역변수를 통해 접근합니다. window객체를 지칭하는 this로 접근하기 때문에 해당함수의 런타임 콘텍스트에 따라 값이 다르게 도출 될 수 있습니다.

부수효과가 발생하는 상황은 다양합니다.

  • 전역 범위에서 변수, 속성, 자료구조를 변경
  • 함수의 원래 인수 값을 변경
  • 사용자 입력을 처리
  • 예외를 일으킨 해당 함수에서 catch하지 않고 그대로 throw함
  • 화면 또는 로그 파일에 출력
  • HTML 문서, 브라우저 쿠키, DB에 질의

실제로 FP는 모든 상태변이를 근절하자는게 아니라 상태변이를 줄이고 관리할 수 있는 프레임워크를 제공하여 순수/불순 함수를 구분하고자 사용합니다.

좀 더 현실적인 예제를 들어봅시다. 당신은 학생 데이터를 관리하는 프로젝트를 참여중이며 학생 레코드를 검색하여 브라우저에 표시하는 함수를 작성한다 가정해 봅시다.

코드1-3 부수효과를 일으키는 명령형 showStudnet함수
  • javascript
1
2
3
4
5
6
7
8
9
10
11
function showStudent(ssn) {
let student = db.find(ssn);
if(student !== null) {
document.querySelector(`#${elementId}`).innerHTML =
`${student.ssn},${student.firstname},${student.lastname}`;
} else {
throw new Error('학생을 찾을 수 없습니다!');
}
}

showStudent('444-44-4444')

이 함수는 확실히 자신의 스코프를 벗어나 몇 가지 부수효과 파장을 일으킵니다.

  • 변수 db를 통해 데이터에 접근하는데, 함수 서명(signature)에는 이런 매개변수가 없으니 이는 외부 변수 입니다. 문제는 이 변수가 실행 중 언제라도 null을 참조하거나 호출 단계마다 상이한 값을 가리키면 결과값이 완전히 달라지고 프로그램 무결성이 깨질 수 있다는 점입니다.
  • elementId는 그 값이 언제라도 바뀔 수 있는 전역 변수라 이 함수가 어쩔 도리가 없습니다.
  • HTML요소를 직접 고칩니다. HTML 문서는 그 자체로 가변적인, 전역 공유자원입니다.
  • 학생 레코드를 찾지 못해 예외를 던지면 전체 프로그램의 스택이 툭 풀리면서 종료될 것 입니다.

위 함수는 외부자원에 의존하므로 코드가 유연하지 않고 다루기가 힘들뿐더러 테스트 역시 어렵습니다.
반면, 순수함수는 서명에 정규 매개변수(format parameter)를 빠짐 없이 명시하므로 코드를 이해하고 사용하기가 쉽습니다. 그럼, 함수형 마음가짐으로 두가지 FP원칙에 따라 코드를 개선해 봅시다.

  • 긴 함수를 하나의 목적을 가진 짧은 함수로 각각 분리한다.
  • 함수가 해야 할 작업에 필요한 인수를 모두 명시하여 부수효과 개수를 줄인다.

먼저 학생 레코드를 조회하는 일과 이를 화면에 그리는 일을 분리합시다. 이때 커링(curring) 이라는 기법을 사용할 것입니다. 커링은 함수의 여러 인수를 부분적으로 나누어 세팅하는 것입니다. 다음 [코드1-4]는 find와 append 두 함수를 커링을 통해 쉽게 조합해서 실행 가능한 단항 함수(unary function)로 나눕니다.

코드1-4 프로그램을 분해
  • javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//find 함수는 객체가 저장된 곳을 가리키는 레퍼런스와 검색할 학생 ID를 받습니다.
var find = curry((db, id) => {
let obj = db.find(id);
if(obj === null) {
throw new Error('객체를 찾을 수 없습니다!');
}
return obj;
})

//student 객체를 콤마로 분리된 문자열로 바꿉니다.
var csv = student => `${student.ssn}, ${student.firstname} ${student.lastname}`;

//학생 상세 정보를 페이지에 표시하려면 요소 ID, 학생 데이터가 필요합니다.
var append = curry((selector, info) => {
document.querySelector(selector).innerHTML = info;
})

한 가지만 개선했는데도 벌써 여러 가지 장점이 눈에 띄네요.

  • 재사용 가능한 컴포넌트 3개로 나뉘어 코드가 훨씬 유연해졌습니다.
  • 이렇게 잘게 나뉜(find-grained)함수를 재사용하면 신경 써서 관리할 코드 크기가 확 줄기 때문에 생산성을 높일 수 있습니다.
  • 프로그램이 해야 할 일들을 고수준(high-level)에서 단계별로 명확하게 보여주는 선언적 스타일을 따르므로 코드 가독성이 향상됩니다.
  • 무엇보다 중요한 건, HTML 객체와 상호작용을 자체 함수로 빼내어 순수하지 않은 로직을 순수함수에서 배제했다는 점입니다.

find 함수를 자세히 보면 예외를 내는 별도의 null 체크 분기문이 포함되어 있습니다. 이처럼 함수가 일관된 반환값을 보장하도록 해서 전체 함수 결과를 예측 가능한 방향으로 유도하면 여러모로 이롭습니다. 이것이 바로 참조 투명성(referential transparency)이라는 순수함수 본연의 특징입니다.

1.2.3 참조 투명성과 치환성

참조 투명성은 순수함수를 정의하는 좀 더 공식적인 방법이며, 여기서 순수성(purity)이란 함수의 인수와 결과값 사이의 순수한 매핑 관례를 의미합니다. 따라서 어떤 함수가 동일한 입력을 받았을 때 동일한 결과를 내면 이를 참조 투명한 함수라고 합니다. 이러한 순수함수는 테스트하기 쉽고 전체 로직을 파악하는것도 쉽습니다. 좀 더 구체적으로 살펴볼까요? 주어진 입력을 처리해서 결과를 내는 일련의 함수들로 임의의 프로그램을 정의한다고 합시다. 의사형식(pseudo form)으로 나타내면 이런 모습이겠죠.

1
Program = [Input] + [func1, func2, func3, ...] => Output

[func1, func2, func3, …]이 모두 순수함수라면 이들이 내는 결과를 바꾸지 않고 [val1, val2, val3 …]이런 식으로 나열하여 프로그램을 쉽게 고칠 수 있습니다. 학생들의 평균 점수를 계산하는 간단한 예제를 예로 들어보겠습니다.

1
2
3
var input = [80, 90, 100];
var average = (arr) => divide(sum(arr), size(arr));
average(input); // --> 90

sum, size는 둘 다 참조 투명한 함수라서 이 표현식은 다음과 같이 입력값을 넣어 쉽게 바꿔 쓸 수 있습니다.

1
var average = divide(270, 3); // --> 90

divide는 100% 순수함수여서 수식으로 표기할 수도 있습니다. 그래서 평균은 항상 270 / 3 = 90이겠죠. 참조 투명성 덕분에 이렇게 체계적인, 거의 수학적인 형태로 프로그램을 헤아릴 수 있는 것입니다. 다음은 전체 프로그램입니다.

  • javascript
1
2
3
4
5
6
7
8
var sum = (total, current) => total + current;
//reduce는 map처럼 전체 컬렉션을 반복하는 새로 나온 함수 입니다.
//인수가 sum 함수라서 배열 숫자를 하나씩 합한 총계를 냅니다.
var total = arr => arr.reduce(sum);
var size = arr => arr.length;
var divide = (a,b) => a / b;
var average = arr => divide(total(arr), size(arr));
average(input);

부수효과가 있는 함수라면 이런 일이 불가능하다는 것을 꼭 이해하시기 바랍니다. 함수 인수를 전부 명확하게 정의하면 스칼라 값을 비롯해 대부분의 경우 부수효과를 예방할 수 있지만, 객체를 레퍼런스로 넘길때 실수로 객체에 변이를 일으키지 않도록 주의해야합니다.

1.2.4 불변 데이터 유지하기

불변 데이터는 한번 생성된 후에는 절대 바뀌지 않습니다. 다른 언어도 그렇듯이 문자열, 숫자 등 자바스크립트의 모든 기본형(primitive type)[원시자료형]은 처음부터 불변입니다. 그러나 배열등의 객체는 불변이 아니어서 함수 인수로 전달해도 원래 내용이 변경되어 부수효과가 발생할 소지는 남아있습니다. 배열을 정렬하는 간단한 코드를 봅시다.

1
2
3
var sortDesc = arr => {
arr.sort((a,b) => b - a);
};

얼핏 보기에 위 코드는 부수효과와 전혀 무관한, 좋은 코드 같습니다. 인수로 받은 배열의 원소를 내림차순으로 정렬하고 반환합니다.

1
2
var arr = [1,2,3,4,5,6,7,8,9];
sortDesc(arr); // --> [9,8,7,6,5,4,3,2,1];

하지만 불행히도 상태적 함수인 Array.sort는 원본 레퍼런스가 가리키는 배열의 원소를 정렬하는 부수효과를 일으킵니다. 이는 언어적 결함이기도 한데, 이를 극복하는 방안은 다음 장 이후에 논의합니다. 함수형 프로그래밍을 대략 엿보았으니 이제 좀 더 간명하게 정의를 내리겠습니다.

함수형 프로그래밍은, 외부에서 관찰 가능한 부수효과가 제거된 불변 프로그램을 작성하기 위해 순수함수를 선언적으로 평가하는 것입니다.

오늘날 자바스크립트 개발자가 직면한 문제의 원인은, 대부분 뚜렷한 체계 없이 분기 처리를 남발하고 외부 공유 변수에 지나치게 의존하는 덩치 큰 함수를 과용하는데 있습니다. 안타깝게도 아직도 많은 자바스크립트 애플리케이션이 이런 딱한 상황에 처해 있고, 심지어 성공적이라는 작품조차 많은 파일이 한데 뒤섞여 추적/디버깅이 어려운 가변/전역 데이터를 공유하는 촘촘한 그물망이 형성된 경우가 있습니다.

함수를 순수 연산의 관점에서 고정된 작업 단위(unit of work)로 바라본다면 확실히 잠재적인 버그는 줄게 될 것입니다. 함수형 프로그래밍을 도입해서 반드시 이익을 보려면, 복잡성을 극복하는 길로 안내하는 함수형 프로그래밍의 핵심 원리를 반드시 이해해야 합니다.

1.3 함수형 프로그래밍의 좋은 점

이 절에서는 함수형 인지력을 향상시키고자 핵심 기법 몇 가지를 소개합니다. FP로 개발한 자바스크립트 애플리케이션은 어떤 점이 좋은지 고수준에서 살펴봅시다. 다음 세 가지 측면에서 하위 절로 나누어 살펴보겠습니다.

  • 간단한 함수들로 작업을 분해한다
  • 흐름 체인(fluent chain)으로 데이터를 처리한다.
  • 리액티브 패러다임을 실현하여 이벤트 중심 코드의 복잡성을 줄인다.

1.3.1 복잡한 작업을 분해하도록 유도

함수형 프로그래밍은 고수준에서 보면, 사실상 분해(프로그램을 작은 조각들로 쪼갬)와 합성(작은 조각들을 다시 합침) 간의 상호작용이라 할 수 있습니다. 이러한 양면성(duality) 덕분에 함수형 프로그램은 하나의 모듈로서 효율적으로 동작합니다. 모듈성의 단위, 곧 작업 단위는 바로 함수 자신입니다.

FP에서 모듈화(modularization)는 단일성(singularity)의 원리와 밀접한 관련이 있습니다. 모름지기 함수는 저마다 한 가지 목표만 바라봐야 한다는 사상이지요. 이제 함수를 묶을 때 사용했던 run함수라는 흑마술의 내막을 밝힐 때가 되었네요. run은 이 책에서 가장 중요한 합성(composition)이라는 기법을 구현한 함수로, 두 함수를 합성하면 첫 번째 함수의 결과를 다음 함수에 밀어 넣는 새로운 함수가 탄생합니다. 두 함수 f, g의 합성 함수를 수학적으로 쓰면 다음과 같습니다.

f · g = f(g(x))

이 수식은 f 합성 g라고 읽습니다. 이로써 g의 반환값과 f의 인수 간에 느슨하고(loose) 형식 안전한(type-safe) 관계가 맺어집니다. 두 함수를 섞어 쓰려면 당연히 인수 개수와 형식이 맞아야 겠지요? 자세한 내용은 3장에서 다시 이어지니, 지금은 함수명을 compose로 바로잡고 showStudent의 합성을 도식화한 [그림 1-4]를 봅시다.

1
2
3
var showStudent = compose(append('#student-info'), csv, find(db));

showStudent('444-44-4444');

그림 추가할 것

[함수를 합성한 상태에서의 데이터 흐름] find의 반환값은 csv에 전달하는 인수,형식,개수가 일치해야 하며, csv역시 append가 사용가능 한 값을 전달해야 합니다.

compose함수는 함수형 애플리케이션의 모듈성과 재사용성을 학습하는데 매우 각별한 의미를 지닙니다. 함수형으로 합성한 코드는 전체 표현식의 의미를 개별 조각의 의미에서 추론할 수 있습니다. 또한 함수 합성은 고수준의 추상화를 통해 자세한 내막을 밝히지 않아도 코드가 수행하는 전 단계를 일목요연하게 나타냅니다. compose는 다른 함수를 인수로 받으므로 고계함수(higher-order-function)라고 합니다. 다음 절에서는 체인을 걸 듯 연산을 연결하여 연산 순차열을 만드는 방법을 살펴보겠습니다.

1.3.2 데이터를 매끄럽게 체이닝하여 처리

체인(chain)은 같은 객체를 반환하는 순차적인 함수 호출입니다. 체인도 합성처럼 코드를 간결명료하게 작성하게 하고, 함수형은 물론 리액티브 자바스크립트 라이브러리에서도 활발히 쓰입니다. 이번에는 수강과목이 2개 이상인 학생들의 평균 점수를 계산하는 프로그램을 작성해보겠습니다. 과목 수와 평균 점수 데이터는 다음과 같습니다.

1
2
3
4
5
let enrollment = [
{enrolled: 2, grade: 100},
{enrolled: 2, grade: 80},
{enrolled: 1, grade: 89}
];

명령형으로 짜면 이런 코드가 되겠죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
var totalGrades = 0;
var totalStudentsFound = 0;
for(let i=0; i<enrollment.length; i++) {
let student = enrollment[i];
if(student !== null) {
if(student.enrolled > 1) {
totalGrades += student.grade;
totalStudentsFound++;
}
}
}

var average = totalGrades / totalStudentsFound; // --> 90

좀 전에 보았던 예제처럼 함수형 마음가짐으로 이문제를 분해하면 대략 세 가지 단계를 거쳐야 합니다.

  • (수강 과목이 2개 이상인) 자료 집합을 적절히 선택합니다.
  • 학생의 점수를 얻습니다.
  • 평균 점수를 계산합니다.

각 단계에 해당하는 함수를 lodash.js로 묶으면 [코드 1-5]같은 함수 체인이 형성됩니다. 함수 체인은 필요한 시점까지 실행을 미루는 느긋한 평가(lazy evaluation; 게으른 평가)를 수행합니다. 다른 데에선 전혀 쓸 일이 없는 일련의 코드를 전부 실행하지 않아도 되니 CPU 부하가 줄어들어 성능이 좋아지죠. 이러게 하면 다른 함수형 언어에 기본 탑재된 필요 시 호출(call-by-need) 동작을 효과적으로 모방할 수 있습니다.

코드 1-5 함수 체인으로 프로그래밍
  • javascript
1
2
3
4
5
6
7
_.chain(enrollment)
.filter(student => student.enrolled > 1)
.pluck('grade')
.average()
.value(); // --> 90
//_.value()를 호출해야 체인에 연결된 모든 연산들이 실행됩니다.
//lodash 4.0 부터 pluck사라짐. map()으로 대체 가능 (원래 기능은 반복가능한 JSON 배열을 일반 배열로 추려주는 기능을 함)

지금은 이 코드의 로직을 너무 깊이 파고들 필요가 없습니다. 명령형 프로그램에서 변수를 선언하여 그 값을 바꾸고, 루프를 반복하고, if-else 구문으로 분기했던 일들을 더 이상 할 필요가 없다는 사실만 기억하기 바랍니다.

하지만 공정하게 보자면 위 예제는 에러 처리 코드를 모두 무시하고 건너뛰었습니다. 예외를 던지는 건 부수효과를 유발한다고 했었죠. 순수 학문적인 함수형 프로그래밍에는 예외가 존재하지 않지만, 실세계에서 예외를 완전히 배제하기란 어렵습니다. 순수 에러 처리와 예외 처리는 구별해야 하는데, 어쨌든 우리의 목표는 가급적 순수 에러 처리를 하도록 구현하고, 이전 코드처럼 진짜 예외적인 상황에서는 예외가 나게끔 허용하는 것입니다.

1.3.3 복잡한 비동기 애플리케이션에서도 신속하게 반응

원격 데이터 조회, 사용자 입력 데이터 처리, 지역 저장소와 연동… 이런 일들을 경험한 독자라면 비지니스 로직이 콜백헬로 뒤범벅 되었던 끔찍한 기억을 떠올릴지도 모르겠습니다. 콜백 패턴은 성공/실패 처리 로직이 중첩된 형태로 흩뿌려져 있기 대문에 코드의 선형 흐름이 깨지고 무슨 일을 하는지 파악하기 어렵습니다.

최근에는 리액티브 프로그래밍 패러다임을 따르는 프레임워크에 더 많은 관심이 쏠리고 있습니다. 앵귤러JS 같은 웹 프레임워크가 아직 널리 쓰이고 있긴 하지만, RxJS 처럼 FP의 강력한 장점으로 무장하여 난제를 척척 해결하는 신흥 강자들이 실무에 등장하고 있습니다.

리액티브 패러다임의 가장 큰 장점은, 더 높은 수준으로 코드를 추상화하여 반복되는 판박이(boilerplate) 코드는 아에 잊고 비지니스 로직에만 전념할 수 있게 해준다는 것 입니다.

어떤 학생의 SSN이 올바른 번호인지 검증하는 함수를 만들어 봅시다. 명령형으로 생각하면 다음 [코드1-6]과 같습니다.

코드1-6 학생의 SSN을 읽고 올바른지 검증하는 명령형 프로그램
  • javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var valid = false;
var elem = document.querySelector('#student-ssn');
elem.onkeyup = function(event) {
var val = event.value;
if(val !== null && val.length !== 0) {
//입력 데이터를 정제/변경합니다.
val = val.replace(/^\s*|\s*$|\-s/g, '');
if(val.length === 9) {
console.log(`올바른 SSN : ${val}!`);
//아래 줄 코드에서는 함수 스코프 바깥 데이터에 접근하는 부수효과 발생
valid = true;
}
} else {
console.log(`잘못된 SSN: ${val}!`);
}
}

하려는 일은 단순한데 코드는 적잖이 복잡해 보이고, 게다가 비즈니스 로직이 모두 한곳에 집중되어 있어 모듈성도 결여되어 있습니다. 무엇보다 이 함수는 외부 상태에 의존하는 탓에 재사용이 어렵습니다. 함수형 프로그래밍에 기반을 둔 리액티브 프로그램은 순수함수를 이용하여 map, reduce처럼 많이 쓰는 연산으로 데이터를 처리할 수 있고 람다 표현식의 간결함을 누릴 수 있다는 이점이 있습니다.

리액티브 패러다임은 옵져버블(observable; 관찰가능)이라는 아주 중요한 장치를 매개로 움직입니다. 옵저버블을 이용하면 데이터 스트림을 구독해서 원하는 연산을 우아하게 합성 및 체이닝(chaining)하여 처리할 수 있습니다. 학생 SSN 입력 필드를 구독하는 간단한 예를 봅시다.

코드 1-7 학생의 SSN을 읽고 올바른지 검증하는 함수형 프로그램
  • javascript
1
2
3
4
5
6
7
Rx.Observable.fromEvent(document.querySelector('#student-ssn'), 'keyup')
.pluck('srcElement', 'value')
.map(ssn => ssn.replace(/^\s*|\s*$|\-s/g, ''))
.filter(ssn => ssn !== null && ssn.length === 9)
.subscribe(validSsn => {
console.log(`올바른 SSN ${validSsn}!`);
})

[코드 1-7]에서 가장 주목해야 할 부분은, 수행하는 모든 연산이 완전한 불변이고 비즈니스 로직은 모두 개별 함수로 나뉘었다는 점입니다. 굳이 리액티브/함수형을 섞어 쓸 필요는 업지만, 함수형으로 사고하다 보면 두 가지를 혼용하게 되어 결국 함수형 리액티브 프로그래밍(functional reactive programming; FRP)이라는 정말 기막힌 아키텍처에 눈을 뜨게 됩니다.

FP는 불변성과 공유 상태를 엄격하게 통제하므로 멀티스레드 프로그램보다 직관적으로 작성 할 수 있습니다. 자바스크립트는 싱글스레드로 작동하는 플랫폼이므로 멀티스레드는 우리가 걱정하거나 이 책에서 다룰 주제는 아닙니다. 앞의 내용들을 잘 따라오셨다면 앞으로는 모든 문제를 함수형으로 바라보기 시작해야 합니다.

1.4 마치며

순수함수를 사용한 코드는 전역 상태를 바꾸거나 깨뜨릴 일이 전혀 없으므로 테스트, 유지보수가 더 쉬운 코드를 개발하는 데 도움이 됩니다.

함수형 프로그래밍은 코드를 선언적으로 작성하므로 헤아리기 쉽고 전체 애플리케이션의 가독성 역시 향상됩니다. 또 함수와 람다 표현식을 조합하여 깔끔하게 코딩할 수 있습니다.

여러 원소로 구성된 컬렉션 데이터는 map, reduce 같은 연산을 함수 체인으로 연결하여 물 흐르듯 매끄럽게 처리할 수 있습니다.

함수형 프로그래밍은 함수를 기본적인 구성 요소로 취급합니다. 이는 일급/고계함수 개념에 기반을 두며 코드의 모듈성, 재사용성을 높입니다.

리액티브/함수형 프로그래밍을 융합하면 이벤트 기반 프로그램 특유의 복잡성을 줄일 수 있습니다.