함수 조립하기
함수 조립하기
함수형 자바스크립트 기법을 잘 활용하면 소프트웨어의 기능을 변경하거나 추가하기가 쉽다. 작은 단위로 쪼갠 함수들을 조합하여 큰 기능을 만들면 조합된 함수 사이사이에 새로운 함수를 추가하는 식으로 쉽게 확장해 나갈 수 있다.
함수형 자바스크립트 10가지 기법
- 함수를 되도록 작게 만들기
- 다형성 높은 함수를 만들기
- 상태를 변경하지 않거나 정확히 다루어 부수 효과를 최소화하기
- 동일한 인자를 받으면 항상 동일한 결과를 리턴하는 순수 함수 만들기
- 복잡한 객체 하나를 인자로 사용하기보다는 되도록 일반적인 값 여러개를 인자로 사용하기
- 큰 로직을 고차 함수로 만들고 세부 로직을 보조 함수로 완성하기
- 어느 곳에서든 바로 실행하거나 혹은 미뤄서 실행할 수 있도록 일반 함수이자 순수 함수로 선언하기
- 모델이나 컬렉션 등의 커스텀 객체보다는 기본 객체를 이용하기
- 로직의 흐름을 최대한 단방향으로 흐르게 하기
- 작은 함수를 모아 큰 함수를 만들기
이번 장에서는 작은 함수로 큰 함수를 만드는 방법들을 다룬다. 객체지향에 빗대어 표현하면 클래스와 인스턴스에 대해서 처음 다루는 장이라고 할 수도 있겠다.
고차 함수와 보조 함수
이번 장에서는 고차 함수의 다양한 사례를 통해 함수 조립에 대한 생각들을 확장하고자 한다. 함수를 주재료로 다루는 함수들을 다룰 것이다.
한 번만 실행하는 함수
1 | _.once = function (func) { |
_.once는 받아 둔 익명 함수가 한 번만 실행되도록 설정된 함수를 리턴한다. 이런 기능을 구현하기 위해서는 flag 값이 필요하며 flag 값에 따라 실행할 것인지 말 것인지 대해 판단하는 로직이 어딘가에 있어야 한다.
1 | var a = _.once(function () { |
다시 물어 보지 않는 함수
1 | function skip(body) { |
최초 한번만 실행을 하고 그 이후로는 실행이 되지 않는다. skip은 고차 함수이고 body는 skip이 남겨 놓은 로직을 완성하는 함수다. 함수형 자바스크립트는 함수로 함수를 다루거나 함수로 함수를 만드는 것의 반복이고, 고차 함수 응용의 반복이다.
skip은 고차 함수이자 함수를 만드는 함수다. 함수로 함수를 만들 때는 재료로 함수가 사용되기도 하고 일반 값이 사용되기도 한다. 함수로 만들어진 함수는 대부분 클로저다.
앞서 받은 인자 혹은 상황을 변경해 나가는 경우
skip같은 함수는 앞서 만든 상황을 변경해 나가는 사례다. 처음에는 false로 시작했지만 true로 변경하여 이후 동작을 다르게 만들기 위해 사용한다.
1 | function idMaker(start) { |
idMaker는 원하는 시작점부터 시작해 실행할 때마다 증가한 고유한 아이디 값을 만드는 함수를 만드는 함수다. idMaker는 메신저 등을 만들 때 사용할 수 있다. 사용자가 메시지를 입력하고 엔터 키를 쳤을 때, 임시로 클라이언트 측 고유 아이디를 만들어 메시지에 해당하는 HTML 요소를 즉시 그려 둔 다음, 서버에게 정보를 보내어 DB에 저장하고 응답으로 온 데이터를, 만들어 두었던 클라이언트 측 고유 아이디를 기준으로 매핑한다.
앞서 받은 인자를 잘 유지해야 하는 경우
클로저가 기억하고 있는 외부 변수도 일반 변수처럼 언제든지 값이 변경될 수 있다. 앞선 _.once, skip, idMaker 사례는 값이 변경되는 점을 이용한 기법이다. 이번에는 반대로 값을 잘 유지해야 하는 상황을 살펴 볼 텐데, 이런 상황에서 실수가 많이 생긴다.
특히 앞서 받은 인자와 나중에 받은 인자를 조합하여 결과를 만들려고 할 때는 실수하기가 쉽다. 이럴 때는 계속 사용할 객체는 원래 상태를 잘 유지하도록, 한 번만 쓰이고 사라져야 할 값은 사라지도록 잘 관리해 주어야 한다.
앞서 받은 인자의 상태가 변경되지 않도록 concat이나 slice를 이용해 항상 새로운 객체를 만든다거나, _.rest 같은 함수를 이용해 인자의 일부분을 잘 제외시켜야 하는데, 이 것을 어떤 타이밍에 하는지가 중요하다.
부분 적용
_.partial로 함수 만들기
1 | var pc = _.partial(console.log, 1); |
_.partial 함수를 이용하면 원하는 위치에 인자를 부분적으로 적용할 수 있다. _.partial을 활용한 다양한 함수 조립 사례를 확인해보자.
1 | var add_all = _.partial(_.reduce, _, function (a, b) { return a + b }); |
_.partial은 함수를 다루는 고차 함수다. _.reduce도 고차 함수다. 위 코드 처럼 _.partial을 이용해 _.reduce와 같은 고차 함수에 미리 보조 함수를 적용해 두는식으로 add_all 같은 함수를 구현할 수 있다.
_.partial은 정말 강력하다. _.partial을 이용하면, 인자를 조합하기 위해 함수로 함수를 만드는 경우를 모두 대체할 수 있다.
1 | var method = function (obj, method) { |
이번 method 함수로 함수를 만드는 함수가 아닌 혼자서도 실행할 수 있는 일반 함수가 되었다. 이러한 방식의 이점은 method 같은 함수가 혼자서도 활용 가능한 함수가 된다는 점이다.
_.partial과 _.compose로 함수 만들기
_.partial은 함수를 연속으로 실행해 주는 _.compose 등의 함수와 함께 더 재미있게 사용할 수 있다. _.compose는 오른쪽의 함수를 실행한 결과를 왼쪽의 함수에게 전달하는 것을 반복하는 고차 함수이다. _.compose는 인자로 함수만 받는 함수다.
1 | _.compose(console.log, function (a) { return a - 2 }, function (a) { return a + 5 })(0); |
- _.isEqual 함수에 -1을 부분 적용하여, 앞에서 나온 결과가 -1과 같은지를 검사하는 함수 만들기
- -1 과 비교하는 함수가 실행되기 전에는 _.findIndex에 _.identity 를 부분 적용해 둔 함수가 실행된다. _.findIndex는 긍정적인 값을 처음 만났을 때의 index를 리턴한다. _.compose 를 통해 두 함수를 역순으로 나열했고, falsy_values 는 배열에 들어있는 모든 값이 부정적인 값인지를 판단하는 함수가 된다.
- 받은 함수를 실행한 후, 결과를 반대로 만드는 함수를 리턴하는 함수인 _.negate 와 앞서 조합한 falsy_values를 조합하여 하나라도 긍정적인 값이 있는지를 체크하는 some 함수를 만들었다.
- falay_values를 조합던 코듸의 _.identity 부분만 _.negate로 감싸서 모두 긍정적인 값이 맞는지를 체크하는 every 함수
더 나은 _.partial 함수
_.partial은 인자를 왼쪽에서부터 하나씩 적용하면서 _로 구분하여 인자가 적용될 위치를 지정해 둘 수 있도록 한다. 이런 _.partial에도 한 가지 아쉬움이 있다. 자바스크립트 함수는 인자 개수가 유동적일 수 있고 함수의 마지막 인자를 중요하게 사용할 수도 있는데, 이 같은 함수화 _.partial은 합이 잘 맞지 않는다.
1 | function add(a, b) { |
f1의 상황처럼 인자를 유동적으로 다루는 함수는 _.partial로 다루기 좋지 않다. 맨 왼쪽 인자나 맨 왼쪽에서 두 번째 인자를 적용해 두는 것은 가능하지만 맨 오른쪽 인자나 맨 오른쪽에서 두 번째에만 인자를 적용해 두는것은 불가능 하기 때문이다. Lodash 는 이를 위해 _.partialRight를 구현했지만 양쪽 끝 모두를 부분 적용하고, 가운데 부분을 가변적으로 가져가고 싶을 때도 있기에 아직 아쉽다.
1 | var ___ = {}; |
복잡해 보이지만 생각보다 단순하다. 우선 새로운 구분자인 _ 가 추가 되었다. _.partial을 실행하면 _를 기준으로 왼편의 인자들을 왼쪽부터 적용하고 오른편의 인자들을 오른쪽부터 적용할 준비를 해 둔 함수를 리턴한다. 부분 적용된 함수를 나중에 실행하면 그때 받은 인자들로 왼쪽과 오른쪽을 먼저 채운 후, 남은 인자들로 가운데 ___자리를 채운다.
1 | var pc = _.partial(console.log, ___, 2, 3); |
_.partial을 이용하면 인자를 조합하기 위해 함수로 함수를 만드는 경우를 모두 대체할 수 있고, 코드에 함수 표현식이 나오는것도 많이 줄일 수 있다. 이렇게 하면 _.chain, _.compose, _.pipeline 등의 함수 합성 패턴과도 잘 어울리고 함수를 조립하는 것도 즐거워 진다. 함수에 인자를 미리 적용해 두는 기법은 비동기 상황에서도 효과적으로 쓰인다.
연속적인 함수 실행
체인의 아쉬운점
체인은 메서드를 연속적으로 실행하면서 객체의 상태를 변경해 나가는 기법이다. 체인은 표현력이 좋고 실행 순서를 눈으로 따라가기에도 좋다. 체인 방식은 많은 장점을 가지고 있지만 체인 방식으로만 모든 로직을 구현하기에는 다소 불편한 점이 있다.
체인 방식은 체인 객체가 가지고 있는 메서드만 이용할 수 있기 때문에 체인 객체와 연관 없는 로직이나 다른 재료를 중간에 섞어 사용하기 어렵다. 정해진 메서드나 규격에 맞춰서 사용해야 하기 때문에 인자를 자유롭게 사용하기 어렵고 다양한 로직을 만들기도 어렵다. 따라서 결과를 완성해 나가는 과정에서 체인을 끊어야 하는 경우가 많다.
체인 방식은 사용하기는 쉽지만 잘 만들어 두기는 어렵다. this만 리턴하면 되는데 뭐가 어렵냐고 할 수 있지만, 가만히 생각해 보면 잘 쓰이는 체인 API는 그렇게 많지 않다. 잘 쓰이는 체인 API가 되려면 우선 체인 패턴과 잘 어울리는 주제여야 한다.
체인 방식은 객체가 생성되어야만 메서드를 사용할 수 있기 때문에 반드시 생성 단계를 거쳐야 한다. 그리고 this 등의 상태와 흐름과 깊이에 의존하기 때문에 언제 어디서나 아무 때나 사용이 가능한 순수 함수보다는 접근성면에서 좀 불편하다.
_.compose의 아쉬운 점
_.compose 함수는 디자인 패턴과 같은 특별한 개념이나 지식 없어도, 바로 코딩 및 설계가 가능하다는 장점이 있다. 인자와 결과만을 생각하면서 작은 함수들을 조합하면 된다. 몇 가지 아쉬운 점이 있는데 그중 가장 큰 아쉬움은, 함수 실행의 순서가 오른쪽에서부터 왼쪽이기 때문에 읽기가 어렵다는 점이다. 함수 실행을 중첩해서 하는 것과 코드의 표현력이 크게 다를 바가 없고, 기능적으로도 특별히 나을 점이 없다.
파이프라인
파이프라인은 _.compose의 장점을 그대로 가지고 있다. _.compose와 기본적인 사용법은 동일하다. 다만, 함수 실행 방향은 왼쪽에서부터 오른쪽이다. 왼쪽에서부터 오른쪽, 위에서부터 아래로 표현되어 코드를 읽기 쉽다. 또한 체인과 달리 아무 함수나 사용할 수 있어 자유도가 높다. 여기서는 이런 파이프라인의 장점들을 살펴볼 것이다.
Michael Fogus의 _.pipeline
1 | _.pipeline = function () { |
_.pipeline은 _.reduce를 이용해서 만들어졌다. _.reduce는 정말 강력한 함수다. 위 상황에서 가지고 있는 데이터는 함수들이고 만들고자 하는 데이터는 최초 인자로부터 시작해 모든 함수를 통과한 마지막 결과이다.
- arguments를 지역 변수 funs에 담았다.
- funs를 기억하는 함수를 리턴한다.
- 리턴된 함수가 나중에 실행되면 받은 인자인 seed를 _.reduce 의 마지막 인자로 넘겨주어 seed는 최초의 l이 된다.
- 예측해 보건대 l 은 left고 r은 right인 듯 하다. 오른쪽 함수를 r을 실행하며 왼쪽 함수의 결과 l을 넘겨주고 있다. funs의 개수만큼 반복되며 마지막 함수의 결과가 곧 _.pipeline으로 만든 함수의 결과가 된다.
클래스를 대신하는 파이프라인
_.pipeline은 작은 함수들을 모아 큰 함수를 만드는 함수다. 파이프라인은 클래스와 기능적인 면과 개념적인 면이 다르지만 비슷한 역할을 대신할 수 있다. 작은 함수들을 조합하여 큰 함수들을 만들고 함수 조합을 조금씩 변경하거나 추가하면서 새로운 로직을 만들어 갈 수 있다.
회원가입을 예로 들어 보자. 개인 회원과 기업 회원이 있다고 가정하면, 회원 가입과 관련된 작은 로직들을 작은 함수 단위로 쪼개어 나눈 후 약간 변경하여 조합하거나 더할 수 있고, 뺄 수도 있다.
1 | var users = []; |
join_user와 join_company는 두 번째 함수만 다르고 첫 번째와 세 번째 함수는 동일하게 조합되었다. 위에서 부터 내려오면서 가입 날짜를 만들고 서로 다른 배열에 담은 후 인사말을 남기고 있다.
_.partial을 함께 이용하면 아래와 같은 표현이 가능하다.
1 | function joined_at(attrs) { |
이번에는 _.partial을 이용해 users에 담을지 companies에 담을지를 선택했다. 이런 방식은 로직을 단순하게 한다. 서로 다른 기능을 하지만 조건문이 없다. 각자 자신이 해야 할 일만 순서대로 수행할 뿐이다. 작은 함수는 작성이 쉽고 테스트도 쉬우면 오류도 적기 마련이다. 앞뒤로 받을 인자와 결과만을 생각하면서 문제를 작게 만들면 문제 해결도 쉬워진다.
더 나은 파이프라인, 그리고 Multiple Results
Underscore.js의 _.pipeline이나 Lodash의 _.flow에는 아쉬운 점이 있다. 인자를 하나만 받을 수 있다는 점이다. 파이프라인에 사용된 내부 함수들 역시 마찬가지다. 파이프라인 내부에서 function (a, b) { return a + b; } 와 같은 함수는 사용할 수 없다는 얘기다. 물론 객체나 배열에 담아 다음 함수에게 전달할 수도 있겠지만 function(args) { return args[0] + args[1]; } 과 같은 함수는 파이프라인만을 위한 함수라고 봐야 한다. 클로저나 _.partial을 이용해서 인자나 재료를 늘릴 수 있지만 자칫 외부 상황에 의존하는 함수가 될 수 있다.
함수형 자바스크립트는 순수 함수를 많이 사용할수록, 인자들을 적극 활용할수록 강력해진다. 인자는 특히 2~3개 사용할 때도 많고 개수가 가변적인 경우도 많다. 인자를 2개 이상 필요로 하는 함수들을 파이프라인 사이에 끼워 넣지 못한다는 것은, 곧 파이프라인 사이에 정의된 함수들의 재사용성도 낮아진다는 얘기다.
만일 언어가 Go였다면 함수의 결과로 Multiple Results를 리턴할 수 있고 파이프라인 중간중간에 여러 개의 인자를 받는 함수들을 얼마든지 끼워 넣을 수 있을 것이다.
Multiple Results는 함수의 결과값을 여러 개로 리턴하는 개념이다. 자바스크립트에는 이러한 기능이 없지만 이것을 대체하는 기능을 구현할 수 있다. 여러 개의 값을 모아 Multiple Results를 뜻하는 객체로 만든 후 파이프라인 안에서 Multiple Results에 담긴 인자를 다시 여러개로 펼쳐서 실행하도록 구현하면 된다.
1 | _.mr = function () { |
apply는 배열이나 arguments 객체를 받아 함수의 인자들로 펼쳐준다. 함수를 실행하기 전 l이 Multiple Results라면 r.apply()를 이용해 r 함수에게 인자를 여러개로 전달할 수 있도록 기능을 추가했다.
1 | function add(a, b) { |
이제 add와 sub 같이 인자를 2개 이상 사용하는 일반 함수들도 파이프라인 사이에 넣을 수 있게 되었다. _.pipeline으로 함수를 정의하면 Multiple Results 를 지원하는 함수가 되어, 함수를 중첩하기만 해도 마치 Go 언어 처럼 동작한다.
더 나은 함수 조립
함수를 조립하는 데에도 함수가 사용된다. 함수를 재료로 사용하고, 재료를 함수로 실행하면서 로직을 완성한다. 함수로 함수를 만드는 방법들을 더 정교하게 잘 다루면 훨씬 다양하고 재밌게 조합할 수 있다. _.partial이나 _.pipeline의 기능을 높인다거나 그동안 살펴보았던 함수형 스타일의 함수들에게 부분 커링을 내장하도록 한다거나 하면 함수 조립의 효과를 더욱 높일 수 있다.
Partial.js의 _.pipe
마이클 포거스의 _.pipeline은 Multiple Results를 지원하지 않는 아쉬움이 있었다. 한 가지 아쉬운 점이 더 있는데, 파이프라인의 내부 함수에서 this를 사용할 수 없다는 점이다. 자바스크립트에서의 함수는 메서드든 아니든 this를 사용할 수 있도록 되어있다. 새로운 기능을 만들 때, 기존의 기본 기능을 유지하는 것은 언제나 중요한 일이다.
Partial.js의 파이프라인 함수인 _.pipe는 Multiple Results와 this를 모두 지원한다. 그리고 Multiple Results를 사용할 수 있기 때문에 인자를 2개 이상 필요로 하는 함수도 함께 사용할 수 있고, this를 사용할 수 있기 때문에 메서드를 만들거나 this를 사용하는 라이브러리들과 협업이 가능하다.
1 | _.pipe( |
즉시 실행 파이프라인 _.go
_.go는 _.pipe의 즉시 실행 버전이다. 왼쪽에서 오른쪽, 위에서 아래로 읽는 것이 편하기 때문에 첫 번째 인자를 파이프라인에서 사용할 인자로 정했다.
1 | _.go(10, |
참조: 함수형 자바스크립트 프로그래밍