함수형 자바스크립트를 위한 문법 다시 보기-2

if else || && 삼항 연산자 다시 보기

if의 괄호

if 와 else if 다음에는 괄호가 나온다. 괄호에서는 기본적으로 true와 false를 받으며, true로 해석되는 값과 false로 해석되는 값도 받는다. 그리고 괄호 안에서는 거의 모든 일을 할 수 있다. 코드를 실행할 수 있다는 얘기다. if의 괄호에서 못 하는 일이 있는데 지역 변수를 선언하는 것과 지역 함수를 선언하는 것이다.

괄호에서 할 수 없는 일이 한 가지 더 있는데, 바로 비동기 프로그래밍이다. 자바스크립트에서는 비동기 코드를 if와 함께 사용하기 어렵다.

if (expression) { statements } 중 statements 부분에는 비동기 코드를 활용할 여지가 있고 몇 가지 아이디어를 통해 어느정도 제어가 가능하지만 if의 괄호 부분은 비동기 코드와 거리가 좀 있다. 하지만 괄호 안에서 할 수 있는 일들은 많다. 새로운 객체를 생성할 수도 있고 객체의 key에 값을 할당할 수도 있으며 함수를 실행할 수도 있다. 먼저 에러가 발생하는 코드를 확인해보자.

1
if (var a = 0) console.log(a);

위 코드는 문법 에러가 난다. 괄호에서는 표현식만 사용할 수 있기 때문이다. 아래 코드는 에러는 나지 않지만 쓸모없는 코드다.

1
2
if (function f1() {}) console.log('hi');
f1();

f1을 정의하는 곳에서는 에러가 나지 않았고 hi도 출력했지만 f1은 실행할 수 없다. f1이 값으로 다뤄져서 유명 함수로 선언되었기 때문이다. f1은 어디에서도 참조할 수 없어 위 코드는 사실상 에러가 나지 않지만 아무런 의미가 없는 코드다.

1
2
3
4
5
6
7
8
9
10
var a;
if (a = 5) console.log(a); // 1

if (a = 0) console.log(1); // 2
else console.log(a);

if (!(a = false)) console.log(a); // 3

if (a = 5 - 5); // 4
else console.log(a)

미리 선언된 변수에 값을 할당하는 것은 가능하다. 동시에 if의 괄호에는 a가 사용된다. 1에서는 if (5)인 셈이므로 5가 출력된다. 2에서는 if (0) 인 셈이므로 else로 넘어가게 된다. 3에서는 false를 담았고 !으로 반전하여 false가 결과로 나오도록 했다. 4에서는 a에 0이 담기고 else로 넘어간다.

1
2
3
4
5
6
7
8
9
var obj = {};

if (obj.a = 5) console.log(obj.a); // 1

if (obj.b = false) console.log(obj.b); // 2
else console.log('hi');

var c;
if (c = obj.c = true) console.log(c); // 3

이번에는 if의 괄호 안에서 객체의 key에 값을 할당했다. obj에 값을 할당했고, if의 괄호에서는 obj가 아닌 할당한 값이 쓰인다. 2와 3을 통해 알 수 있다. c에는 obj가 아닌 true가 담긴다.

1
2
3
4
5
6
7
8
9
10
function add(a, b) {
return a + b;
}

if (add(1, 2)) console.log('hi1');

var a;
if (a = add(1, 2)) console.log(a);

if (function () { return true; }()) console.log('hi')

함수를 실행할 수도 있고 실행한 결과를 변수에 담으면서 참과 거짓을 판단할 수도 있다. 익명 함수나 유명 함수를 정의하면서 즉시 실행할 수도 있다.

위에서 확인한 모든 코드들은 자바스크립트의 대부분의 괄호에서 동일하게 동작한다. 이를테면 while문의 괄호에서도 동일하게 동작한다. 괄호 안에서 어떤 코드들을 돌릴 수 있는지 잘 알고 있다면 코드를 더 깔끔하게 정리하거나 코드 구조를 크게 변경하지 않고도 기능을 발전시킬 수 있다.

|| &&

1
2
3
4
5
6
7
8
9
10
11
console.log(0 && 1) // 0
console.log(1 && 0) // 0
console.log([] || {}); // []
console.log([] && {}); // {}
console.log([] && {} || 0); // {}
console.log(0 || 0 || 0 || 1 || null); // 1
console.log(add(10, -10) || add(10, -10)); // 0
console.log(add(10, -10) || add(10, 10)); //20
var v;
console.log((v = add(10, -10)) || v++ && 20); // 0
console.log((v = add(10, 10)) || ++v && 20); // 20

||과 &&의 활용법은 생각보다 다양하다. 오른쪽으로 더 갈 것인가 말 것인가를 한 줄로 만들어 if else를 대체할 수도 있다. 상황에 따라 if else가 가독성이나 효율이 좋을 수 있고 ||, && 가 좋을 수도 있다. 다양한 도구를 상황에 맞게 잘 사용하면 된다.

삼항 연산자

삼항 연산자는 조건이 간단하고 실행 코드도 간단할 때 많이 사용된다. 보통 값을 담을 때 사용된다. 삼항 연산자를 이용해도 여러 줄을 코딩할 수 있다. 익명 함수, 유명 함수, 화살표 함수 등으로 즉시 실행 패턴을 사용하는 것이다.

1
2
3
4
5
6
7
var a = false;

var c = a ? 10 : function f(arr, v) {
return arr.length ? f(arr, v + arr.shift()) : v;
}([1, 2, 3], 0);

console.log(c)

위 코드에서는 a 가 false이므로 삼항 연산자에서 10을 건너뛰고 함수 부분이 실행된다. 함수 정의 끝부분을 보면 알 수 있듯이 즉시 실행했다. 그리고 [1,2,3]과 0을 인자로 받는다. 위와 같이 즉시 실행 함수를 이용하면 어디에서든 한 줄만 작성할 수 있던 곳을 확장할 수 있다. 또한 다른 함수를 실행할 수도 있고 재귀를 돌면서 얼마든지 복잡한 로직도 넣을 수 있다.

함수 실행의 괄호

함수 실행을 통해 생기는 새로운 공간

이전에 가장 특별한 괄호는 함수를 실행하는 괄호라고 했었다. 함수를 실행하는 괄호와 그렇지 않은 다른 괄호의 차이는 무엇일까?

1
2
(5);
(function () { return 10; });

위 코드의 괄호 두 가지는 모두 일반적인 괄호다. 함수를 실행하는 괄호가 아닌 일반 괄호에서는 코드가 실행되면 해당 지점에 즉시 값을 만들고 끝난다. 해당 지점에서 만들어진 값을 참조할 수는 있지만 여기서 할 일은 바로 모두 끝난다.

1
2
3
4
5
6
7
8
9
10
11
var add5 = function (a) { // 새로운 공간
return a + 5;
}

var call = function (f) { // 새로운 공간
return f();
}

// 함수를 실행하는 괄호
add(5);
call(function () { return 10; });

함수를 실행하는 괄호는 일반 괄호와 특성이 모두 같지만 한 가지 특성을 더 가지고 있다. 이 괄호를 통해 새로운 실행 컨텍스트가 열린다는 점이다. 이 점은 매우 중요하다. 함수를 실행하는 괄호에서는 코드가 실행되었을 때 해당 지점에 값을 만든 후 끝나지 않고, 이 값이 실행된 함수의 공간으로 넘어간다. 새롭게 열린 공간이 끝나기 전까지는 이전 공간의 상황들도 끝나지 않는다. 이 공간들을 실행 컨텍스트라고 한다.

새로운 공간이 생긴다는것, 콜 스택에 쌓인다는 것, 태스크 큐와 이벤트 루프에 의해 제어된다는 것, 이것들을 통해 개발자가 시작과 끝을 제어할 수 있다는 점들이 함수를 실행하는 괄호가 가진 가장 특별한 차이다.

함수가 정의되거나 실행되는 지점에서는 클로저도 만들어질 수 있고, 비동기 상황이 생길 수도 있으며 서로 다른 컨택스트가 연결되는 등의 특별한 일들이 생긴다. 이것들은 함수에 대한 매우 실제적이고 중요한 개념이다.
for문을 사용할 때 어떤 지점들을 확인하면서 코드 블록을 반복시키는지, 언제 어떻게 끝나는지 정확히 이해하지 않고는 코딩할 수 없듯이 함수도 마찬가지다.

기본적인 비동기 상황

1
2
3
4
5
6
7
8
9
console.log(1);
setTimeout(function () {
console.log(3)
}, 1000);
console.log(2);

// 1
// 2
// 3 (1초 뒤)

코드 라인 순서와 달리 1, 2, 3 으로 실행 되었다.

1
2
3
4
5
6
7
8
9
var add = function (a, b, callback) {
setTimeout(function () {
callback(a + b);
}, 1000);
}

add(10, 5, function (r) {
console.log(r);
})

비동기 상황이 생기는 함수의 결과는 return 문으로 반환할 수 없다. 비동기 상황이 생기는 함수의 결과를 받는 방법 중 하나는 콜백 함수를 넘겨서 돌려받는 방법이다. add의 마지막 인자로 넘겨진 익명 함수 callback은 add 안에서 모든 상황이 끝날 때 실행된다. 이를 통해 add를 실행한 스코프 내부에서 다시 add의 결과를 받을 수 있게 된다.
일반 괄호였다면 10, 5, function등이 그 자리에 정의되고 끝났겠지만 함수를 실행하는 괄호에서는 그 값들이 다른 공간으로 넘어간다. 새롭게 열린 공간에서는 넘겨받은 재료들로 새로우 일을 할 수 있다.

함수 실행 괄호의 마법과 비동기

비동기 상황을 제어하는 방법은 함수 실행을 일렬로 나열하는 것이다.

add 함수 실행 -> setTimeout 함수 실행 -> setTimeout이 1초 뒤 익명 함수를 실행 -> 받아 둔 callback 실행

위와 같이 함수들의 실행을 일렬로 나열하여 한 가지 일이 순서대로 일어나도록 하는 것이다.
이 함수 나열을 숨겨서 비동기 코드가 동기식으로 실행되는 것처럼 보이도록 해볼 것이다. Promise하고 비슷할 것이다. 이 작업을 통해 함수 실행의 괄호에서 다른 공간으로 이동되는 사이에 할 수 있는 일을 확인할 것이다. 이런 기법들을 통해 프로미스의 내부 코드를 예상해 볼 수도 있을 것이다.

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
var add = function (a, b, callback) {
setTimeout(function () {
callback(a + b);
}, 1000);
}

var sub = function (a, b, callback) {
setTimeout(function () {
callback(a - b);
}, 1000);
}

var div = function (a, b, callback) {
setTimeout(function () {
callback(a / b);
}, 1000);
}

add(10, 15, function (a) {
sub(a, 5, function (a) {
div(a, 10, function (r) {
console.log(r); // 약 3초 후에 2가 찍힘
})
})
})

원래 비동기가 일어나는 함수들은 아래처럼 중첩 실행을 할 수 없다. 함수의 몇 가지 특성을 활용해 비동기 함수도 아래처럼 중첩 실행이 가능하도록 해보자.

1
2
console.log(div(sub(add(10, 15), 5), 10));
// undefined가 찍히고 callback이 없다는 에러가 남.

우선 함수가 실행되는 사이에 무언가를 할 수 있도록 함수로 한 번 감싸서 공간을 만들 것이다. wrap에게 함수를 전달하여 함수를 리턴 받으면 원래 기능을 유지하면서 코드 사이에 공간이 생긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function wrap(func) { // 1. 함수 받기
return function () { // 2. 함수 리턴하기, 이것이 실행됨.
// 여기에 새로운 공간이 생김, 나중에 함수를 실행했을 때 이 부분을 거쳐감
return func.apply(arguments); // 3
}
}

var add = wrap(function (a, b, callback) {
setTimeout(function () {
callback(a + b);
}, 1000);
});

add(5, 10, function (r) {
console.log(r);
})

1에서 받은 함수를 기억하는 2 클로저를 만들어 리턴했고, add는 2가 된다. 나중에 2가 실행되면 1에서 받아 둔 3 함수를 실행하면서 2가 받은 모든 인자를 넘겨준다. 이전 add와 완전히 동일하게 동작하면서도 사이사이에 코드를 끼워 넣을 수 있는 공간들이 더 생겼다. wrap을 조금만 더 고치고 _async 라고 이름을 바꿔보자.

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
function _async(func) {
return function () {
arguments[arguments.length++] = function (result) { // 1
_callback(result) // 6
};

func.apply(null, arguments); // 2

var _callback; // 3
function _async_cb_receiver(callback) { // 4
_callback = callback; // 5
}

return _async_cb_receiver;
}
}

var add = _async(function (a, b, callback) {
setTimeout(function () {
callback(a + b);
}, 1000);
});

add(20, 30)(function (r) { // 7
console.log(r);
})

우선 마지막 부분 7을 보면 add를 실행하는 방법이 바뀌었다. 한 번에 인자 3개를 넘기지 않고 마치 커링처럼 add에 필요한 재료를 넘긴 후 한 번 더 실행하면서 callback 함수를 넘기고 있다.

  1. add가 실행되면 인자로 20과 30이 넘어온다. 원래는 callback 함수를 받아야 하므로 arguments에 마지막 값으로 함수를 추가한다. 그리고 그 함수는 나중에 개발자가 넘겨준 callback 함수를 실행할 수 있게 준비해 두었다.
  2. add를 정의할 때 받아 둔 func를 실행하면서 인자 3개를 넘긴다.
  3. _callback 이라는 지역 변수를 만들어서 1과 4가 기억해 두도록 했다. 클로저를 활용하여 서로 다른 컨텍스트가 협업할 수 있도록 이어주었다.
  4. _async_cb_receiver라는 이름을 가진 유명 함수이자 클로저를 만들어 리턴한다.
  5. _async_cb_receiver가 실행될 때 받은 함수 callback을 _callback에 할당한다.
  6. 1초가 지나면 1이 실행될 것이고 add가 callback을 통해 넘긴 결과인 result를 받아 두었던 _callback을 실행하면서 다시 넘겨주고 있다.
  7. 이 익명 함수가 _callback 이므로 6에서 넘겨진 r을 받게 되고 로그를 남겼다.
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
var add = _async(function (a, b, callback) {
setTimeout(function () {
callback(a + b);
}, 1000);
});

var sub = _async(function (a, b, callback) {
setTimeout(function () {
callback(a - b);
}, 1000);
});

var div = _async(function (a, b, callback) {
setTimeout(function () {
callback(a / b);
}, 1000);
});

add(10, 15)(function (a) {
sub(a, 5)(function (a) {
div(a, 10)(function (r) {
console.log(r);
// 약 3초 후 2가 찍힘
})
})
})

비동기와 재귀

일반 콜백 패턴의 함수를 실행하는 것과 아직 큰 차이는 없지만 연산에 필요한 실행과 결과를 받기 위한 실행이 분리되었다. 함수를 실행하는 괄호에서는 값을 다른 공간으로 넘겨 새로운 일들을 더 할 수 있다. add, sub, div는 async를 통해 본체에 가기 전 새로운 공간을 가지고 있고, 그 공간에서는 시작과 끝을 제어하고 있다. 이 내부 공간을 손보면 좀 더 재밌는 일을 할 수 있다.

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
53
54
55
56
57
function _async(func) {
return function () {
arguments[arguments.length++] = function (result) {
_callback(result)
};

// 변경된 부분
(function wait(args) {
// 새로운 공간 추가
for (var i = 0; i < args.length; i++) {
if (args[i] && args[i].name === '_async_cb_receiver') {
return args[i](function (arg) { args[i] = arg; wait(args) });
}
}
func.apply(null, args);
})(arguments);

var _callback;
function _async_cb_receiver(callback) {
_callback = callback;
}

return _async_cb_receiver;
}
}

var add = _async(function (a, b, callback) {
setTimeout(function () {
console.log('add', a, b);
callback(a + b);
}, 1000);
});

var sub = _async(function (a, b, callback) {
setTimeout(function () {
console.log('sub', a, b);
callback(a - b);
}, 1000);
});

var div = _async(function (a, b, callback) {
setTimeout(function () {
console.log('div', a, b);
callback(a / b);
}, 1000);
});

var log = _async(function (val) {
setTimeout(function () {
console.log(val);
}, 1000)
})

log(div(sub(add(10, 15), 5), 10));
// 약 4초 뒤 2
log(add(add(10, 10), sub(10, 5)));
// 약 3초 뒤 25

모두 비동기 함수들 인데도 마치 즉시 완료되는 동기 함수들을 중첩하여 실행한 것처럼 동작하고 있다. 추가된 부분만 다시 자세히 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
// 변경 전
func.apply(null, args);

// 변경 후
(function wait(args) {
for (var i = 0; i < args.length; i++) {
if (args[i] && args[i].name === '_async_cb_receiver') {
return args[i](function (arg) { args[i] = arg; wait(args) });
}
}
func.apply(null, args);
})(arguments);

크게 보면 wait라는 유명 함수를 만들었고 내부에서 재귀를 돌다 func를 실행하도록 변경되었다. 재귀는 인자 중에 _async_cb_receiver 가 있다면 모두 결과값으로 치환될 때까지 돌게 된다.
add의 실행 결과는 숫자가 아닌 _async_cb_receiver라는 이름을 가진 함수다. 이 함수에 함수를 넣으면 결과를 받을 수 있다. 이를 이용하여 add의 실행 결과를 받은 sub는 자신의 본체(func) 로 가기 전에 wait로 _async_cb_receiver가 있는지 확인하고, 있다면 실행하여 결과값을 받고 재귀를 돌며 해당 번째 args[i]를 결과값으로 변경한다.

앞의 예제는 특정 지점에 함수를 정의하거나 함수로 감싸고, 함수를 즉시 실행하거나 재귀를 하는 식으로 기존 로직 사이에 선행 로직이나 후행 로직을 만들면서 프로그램의 순서를 제어할 수 있음을 보여준다. 위 예제에서는 이런 기법을 통해 비동기 상황을 제어하고, 다른 라이브러리들과의 연결 고리를 만드는 중요한 단위가 된다.

일반 괄호에서는 할 수 없는 일이지만 함수를 실행하는 괄호에서는 새로운 공간들을 레이어처럼 얼마든지 만들 수 있다. 앞의 예제 처럼 본체까지 가기 전 레이어들을 통과하면서 비동기 함수의 결과를 기다렸다가 결과값으로 변형해 넘겨줄 수도 있다. 함수를 실행하는 괄호에서 함수를 실행할 수 있고, 실행한 결과가 함수여서 그 함수를 다시 함수에게 인자로 넘길 수 있고, 그렇게 받은 함수를 실행할 수 있다.

자바스크립트에서 재귀는 충분히 실용적이라는 이야기를 했었다. 앞의 상황에서는 재귀를 통해 비동기 상황을 제어했다. 재귀는 로직들을 함수라는 단위로 일자로 나열하는 것이다. 비동기 제어의 핵심 역시 함수 실행의 나열이다. 비동기가 발생되면 스택이 초기화되므로 재귀에 대한 부담도 없다. 아무리 많은 재귀가 일어나도 ‘Maximum call stack size exceeded’ 에러는 절대로 발생하지 않는다.

참조: 함수형 자바스크립트 프로그래밍

댓글

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