함수형 프로그래밍 - 시작

최근 사내에서 함수형 프로그래밍을 사용하여 기존 코드의 리팩토링을 시작했다.
함수형 프로그래밍 해야지 해야지 하고 항상 미뤄왔던 스터디인데 실무에서 사용하기에
터무니없이 실력이 부족함을 느껴 스터디를 시작하고자 한다.

기본적으로 함수형 자바스크립트 프로그래밍 책을 기반으로 진행하며, 필요한 내용이나 궁금한 내용은 구글링을 통해 진행한다.

도서 링크: 함수형 자바스크립트 프로그래밍

함수형 자바스크립트 소개

  • 함수형 프로그래밍은 성공적인 프로그래밍을 위해 부수 효과를 최대한 멀리하고 조합성을 강조하는 프로그래밍 패러다임이다.
  • 함수형 프로그래밍이 부수 효과를 최대한 멀리하는 이유
    1. 오류를 줄이기 위함.
    2. 조합성 혹은 모듈화 수준을 높이기 위함.

함수형 프로그래밍을 검색하면 나오는 예제

1
2
3
4
5
6
7
function addMaker(a) {
return function(b) {
return a + b;
}
}

addMaker(10)(5);

함수형 자바스크립트에 관심을 가져 본 적이 있다면 아마 위 예제와 같은 코드를 봤을 것 이다. 커링 혹은 부분 적용과 관련된 코드들이다.
함수를 리턴한다거나 괄호가 많은 코드들을 처음 보면 난해하고 생소하게 느껴진다.

addMaker는 함수를 값으로 다루는 함수다. addMaker에서는 단 하나의 값이 선언되며 그 값은 함수다.
addMaker(10)의 결과는 function(b) { return 10 + b }; 와 같다.
이후 리턴된 함수를 바로 실행해줬기 때문에 결과값은 15가 된다.

1
2
3
var add5 = addMaker(5);
add5(3) // 8
add5(4) // 9

위와 같이 변수에 값을 할당해서 사용할 수 도 있다.

값으로써의 함수와 클로저

위의 예제들을 보면 함수는 값을 리턴할 수 있고 함수는 값이 될 수 있다. addMaker는 내부에서 함수를 정의하고 리턴했다. addMaker가 리턴한 익명 함수는 클로저가 되었다.
리턴된 익명 함수 내부에서 a가 정의된 적은 없지만 a를 참조하고 있고 a는 부모 스코프에 있다.

addMaker가 실행된 후, 어디서도 addMaker의 인자인 a 값을 변경시키지 않고 있기 때문에 항상 동일한 값을 갖는다. 때문에 위 상황에서 a는 불변하며 상수로 쓰이게 된다. 이 상황에서의 a는 불변하지만, 모든 경우의 클로저가 그렇지는 않다. 클로저가 기억하는 변수의 값은 변할 수 있다.


함수형 자바스크립트의 실용성

절차지향적으로 작성된 코드를 함수형으로 변경하면서 함수형 자바스크립트의 실용성을 알아보고자 한다.

회원 목록 중 여러명 찾기

회원 목록 중 특정 나이의 회원들만 뽑거나 특정 조건의 회원 한 명을 찾는 코드들을 함수형 자바스크립트로 리팩토링 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var users = [
{ id: 1, name: 'ID', age: 32 },
{ id: 2, name: 'HA', age: 25 },
{ id: 3, name: 'BJ', age: 32 },
{ id: 4, name: 'PJ', age: 28 },
{ id: 5, name: 'JE', age: 27 },
{ id: 6, name: 'JM', age: 32 },
{ id: 7, name: 'HI', age: 24 },
];

// 1
var temp_users = [];
for (var i = 0, len = users.length; i < len; i++) {
if (users[i].age < 30) temp_users.push(users[i]);
}
console.log(temp_users.length);

// 2
var ages = [];
for (var i = 0, len = temp_users.length; i < len; i++) {
ages.push(temp_users[i].age);
}
console.log(ages)

// 3
var temp_users = [];
for (var i = 0, len = users.length; i < len; i++) {
if (users[i].age >= 30) temp_users.push(users[i])
}
console.log(temp_users.length);

var names = [];
for (var i = 0, len = temp_users.length; i < len; i++) {
names.push(temp_users[i].name);
}
console.log(names);

1에서는 users 중에 age가 30 미만인 users[i]만 모아서 몇 명인지를 출력하고 2에서는 그들의 나이만 다시 모아 출력한다. 3에서는 나이가 30 이상인 temp_users가 몇 명인지를 출력하고 4에서는 그들의 이름만 다시 모아 출력한다.

위 코드를 함수형으로 리팩토링 하기 위해 먼저 중복되는 부분을 찾아본다. 1과 3의 for문에서 users를 돌며 특정 조건의 users[i]를 새로운 배열에 담고 있는데, if 문의 조건절 부분을 제외하고는 모두 동일한 코드이다. 30 부분은 변수로 바꿀 수 있겠지만 .age, <, >= 등은 쉽지 않아 보인다. 이때 함수를 활용하면 이런 부분까지도 쉽게 추상화 할 수 있다.

for에서 filter로, if에서 predicate로

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 기존코드
var temp_users = [];
for (var i = 0, len = users.length; i < len; i++) {
if (users[i].age < 30) temp_users.push(users[i]);
}
console.log(temp_users.length);

// refactoring
function filter(list, predicate) {
var new_list = [];
for (var i = 0, len = list.length; i < len; i++) {
if (predicate(list[i])) new_list.push(list[i]);
}

return new_list;
}

filter 함수는 인자로 list와 predicate 함수를 받는다. 루프를 도며 list의 i번째의 값을 predicate에 넘겨준다. predicate 함수는 list.length 만큼 실행되며, predicate 함수의 결과가 참일 때만 new_list.push를 실행한다.
filter 함수는 predicate 함수 내부에서 어떤 일을 하는지 모른다. id를 조회할지 age를 조회할지 어떤 조건을 만들지를 filter는 전혀 모른다. 오직 predicate 결과에만 의존한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// filter 사용
var users_under_30 = filter(users, function (user) { return user.age < 30 });
console.log(users_under_30)

var ages = [];
for (var i = 0, len = users_under_30.length; i < len; i++) {
ages.push(users_under_30[i].age);
}
console.log(ages);

var users_over_30 = filter(users, function (user) { return user.age > 30 });
console.log(users_over_30);

var names = [];
for (var i = 0, len = users_over_30.length; i < len; i++) {
names.push(users_over_30[i].name)
}
console.log(names);

filter 함수를 실행하면서 predicate 자리에 익명 함수를 정의해서 넘겼다. predicate 익명 함수의 리턴값(boolean)에 따라서 push를 해줄지 안해줄지가 결정된다. 기존 코드와 비교해 코드가 짧아졌고 재사용성 높은 함수 filter를 얻게됐다.

함수형 프로그래밍 관점으로 filter 보기

함수형 프로그래밍 관점에서 filter와 predicate 사이에는 많은 이야기가 담겨있다. filter 함수에는 for도 있고 if도 있지만, filter 함수는 항상 동일하게 동작하는 함수다. 동일한 인자가 들어오면 항상 동일하게 동작한다. filter 함수의 로직은 외부나 내부의 어떤 상태 변화에도 의존하지 않는다. new_list의 값을 바꾸고 있지만 그 변화에 의존하는 다른 로직이 없다. new_list가 완성될 떄까지는 외부에서 어떠한 접근도 할 수 없기 때문에 filter의 결과도 달라질 수 없다. new_list가 완성되고 나면 new_list를 리턴해버리고 filter는 완전히 종료된다.

filter의 if는 predicate의 결과에만 의존한다. filter를 사용하는 부분을 다시 보면 filter와 users, filter가 사용할 predicate 함수만 있다.

1
filter(users, function(user) { return user.age < 30 })

절차지향 프로그래밍과 달리 함수형 프로그래밍 에서는 항상 동일하게 동작하는 함수 를 만들고 보조 함수를 조합하는 식으로 로직을 완성한다. 내부에서 관리하고 있는 상태를 따로 두지않고 넘겨진 인자에만 의존한다. 동일한 인자가 들어오면 항상 동일한 값을 리턴 하도록 한다.

map 함수

리팩토링의 핵심은 중복을 제거하고 의도를 드러내는 것이다. 기존 코드를 보면 회원 목록을 통해 나이와 이름들을 추출하는데 두 코드에도 중복이 있다. 둘 다 for문에서 사용하는 회원 목록을 활용해 같은 크기의 새로운 배열을 만들고 원재료와 1:1로 매핑되는 다른 값을 만들어 담고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// map 사용
function map(list, iteratee) {
var new_list = [];
for (var i = 0, len = list.length; i < len; i++) {
new_list.push(iteratee(list[i]));
}

return new_list;
}

var users_under_30 = filter(users, function (user) { return user.age < 30 });
console.log(users_under_30.length)

var ages = map(users_under_30, function (user) { return user.age });
console.log(ages);

var users_over_30 = filter(users, function (user) { return user.age > 30 });
console.log(users_over_30.length)

var names = map(users_over_30, function (user) { return user.name });
console.log(names);

코드가 매우 단순해 진것을 확인할 수 있다. for도 없고 if도 없다. new_list에 무엇을 push 할지에 대해 iteratee 함수에게 위임했다.

실행 결과로 바로 실행하기

함수의 리턴값을 바로 다른 함수의 인자로 사용하면 변수 할당을 줄일 수 있다.
filter 함수의 결과가 배열이므로 map의 첫 번째 인자로 바로 사용 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 함수 중첩
var ages = map(
filter(users, function (user) { return user.age < 30 }),
function (user) { return user.age }
)
console.log(ages);

var names = map(
filter(users, function (user) { return user.age >= 30 }),
function (user) { return user.name }
)
console.log(names);

// 함수 중첩 2
function log_length(value) {
console.log(value.length)
return value;
}

console.log(
log_length(
map(
filter(users, function (user) { return user.age < 30 }),
function (user) { return user.age }
)
)
)

console.log(
log_length(
map(
filter(users, function (user) { return user.age >= 30 }),
function (user) { return user.name }
)
)
)

filter 함수는 predicate를 통해 값을 필터링하여 map에게 전달하고 map은 받은 iteratee를 통해 새로운 값들을 만들어 log_length에게 전달한다. log_length는 length를 출력한 후 받은 인자를 그대로 console.log에게 전달하고 console.log는 받은 값을 출력한다.

함수를 값으로 다룬 예제의 실용성

위에 만들었던 addMaker 와 비슷한 패턴의 함수가 실제로도 많이 사용된다.
addMaker와 비슷한 패턴의 함수인 bvalue 함수를 만들어 보려고한다.

1
2
3
4
5
6
7
function bvalue(key) {
return function (obj) {
return obj[key]
}
}

console.log(bvalue('a')({ a: 'hi', b: 'hello' })); // hi

bvalue를 실행할 때 넘겨준 인자 key를 나중에 obj를 받을 익명 함수가 기억한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* bvalue로 map의 iteratee 만들기
*/
console.log(
log_length(
map(
filter(users, function (user) { return user.age < 30 }),
bvalue('age')
)
)
)

console.log(
log_length(
map(
filter(users, function (user) { return user.age >= 30 }),
bvalue('name')
)
)
)

map이 사용할 iteratee 함수를 bvalue가 리턴한 함수로 대체했다. 익명 함수 선언이 사라져 코드가 더욱 짧아졌다.

화살표 함수로 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
console.log(
log_length(
map(
filter(users, user => user.age < 30),
user => user.age
)
)
)

console.log(
log_length(
map(
filter(users, user => user.age >= 30),
user => user.name
)
)
)

var under_30 = user => user.age < 30;
var over_30 = user => user.age >= 30;
var ages = list => map(list, user => user.age);
var names = list => map(list, user => user.name);

console.log(log_length(ages(filter(users, under_30))));
console.log(log_length(names(filter(users, over_30))));

function bvalues(key) {
return function (list) {
return map(list, function (user) { return user[key] })
}
}

var ages = bvalues('age');
var names = bvalues('name');
var under_30 = function (user) { return user.age < 30 };
var over_30 = function (user) { return user.age >= 30 };

console.log(log_length(ages(filter(users, under_30))));
console.log(log_length(names(filter(users, over_30))));

function bvalues(key) {
var value = bvalue(key);
return function (list) {
return map(list, value)
}
}

// 화살표 함수 사용
var bvalues = key => list => {
var value = bvalue(key)
return map(list, value);
}


Conclusion
어려운 점이 공부를 하면 할수록 많이 생기겠지만, 새로운 관점에서의 프로그래밍인 것 같아 흥미롭게 느껴진다.
본인은 아직 주니어라 절차지향적 사고 방식이 강한데 함수형 프로그래밍 공부하면서 새로운 사고 방식에 대해서도 생각해 봐야할 것 같다. 이번 챕터의 첫 코드(절차지향적 코드)는 쉽다고 느껴졌지만, 함수형으로 리팩토링 해 나가는 과정에서는 이게 정확히 어떤 방식으로 동작하는가, 인자값은 어떻게 넘겨 주는가에 대한 고민이 생기는것을 느끼면서 함수형 프로그래밍에 더 알고 싶어졌다.

처음에는 함수형 프로그래밍 방식이 “그래서 왜 좋은건데?”에 대한 의문이 있었는데 마지막 코드를 보고 재새용성이 뛰어나다는것을 느꼈다. 프로젝트를 시작할때 잘 구현해 놓은 함수는 계속 재사용하면서 코딩의 속도와 편리함을 제공해 줄 것 같다고 생각했다.

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.