함수형 프로그래밍, async 구현하기 [ TIL/ JavaScript ]

알락·2023년 4월 9일
0

함수형프로그래밍

목록 보기
1/3

javascript banner

배경

❗️이 포스트에서 다뤄지는 예시와 인용은 유인동 - ⌜함수형 자바스크립트 프로그래밍⌟을 거의 그대로 사용하고 있다. 변경사항은 몇몇의 var ⇒ let 변경이 있다. 이외 내용은 필자가 작성했다.

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);
};

console.log( ((10+15)-5)/10 );

add(10, 15, function(a){
	sub(a, 5, function(a){
		div(a, 10, function(r){
			console.log(r);
			// 3초 뒤 2 출력
		})
	})
});

console.log(div(sub(add(10,15), 5), 10));

JavaScript의 고차함수와 클로저가 복합적으로 사용되기 시작하면 정신이 어질어질하다. 간단하고 직관적인 코딩을 원하는 나는 사실, 고차함수를 사용하면서 생기는 복잡성은 의문이었다. 하지만 이와 같은 함수형 프로그래밍은 구현하고나면 코드의 재사용이 용이하고 또 결과적으로 코드를 이해하기 더 쉽게 만든다.

위의 코드는 비동기실행이 함수의 중첩실행에서는 실행이 안되는 문제를 제시하는 예제다. 그리고 이를 해결하기 위해서 JavaScript 함수의 특징을 활용하여 async 역할을 하는 함수를 따로 작성하는 것이 목표이다. 이를테면 실행이 안되는 마지막 코드를 작동할 수 있는 함수를 만드는 것을 목표로 두고 있다고 할 수 있다.

_async 함수

function _async(func){
	return function(){ // 1
		arguments[arguments.length++] = function(result){ // 2
			_callback(result);
		};
		
		(function wait(args){ // 3
			for(let 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){ // 4
			_callback = callback
		}
		return _async_cb_receiver;
	};
}

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)
});

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

log(div(sub(add(10, 15), 5), 10));
// 4초 후 2 출력

add를 예시로 들어 _async가 어떻게 쓰이는지 풀어써본다.

콜백 패턴은 끝이 나면 컨텍스트를 다시 돌려주는 단순한 협업 로직을 가진다.
필자는 위 경우만을 ‘콜백’ 함수라고 부르는 것이 맞다고 생각한다. 컨텍스트를 다시 돌려주는 역할을 가졌기 때문에 callback이라고 함수 이름을 지은 것이다.

-- 책 중

우선 필자는 작가의 콜백 패턴에 대한 개념에 대해 고민해보고, 다시 예제를 생각해봤다. 이 예제에서의 콜백의 의미는, setTimeout에 의해 이동했던 컨텍스트에서 다시 add가 실행되는 함수로 돌아오는 것이라고 생각했다. setTimeout에 넘겨준 콜백함수에서 만들어내는 결과값들은 setTimeout 밖으로 꺼내오지를 못한다. 그렇기 때문에 현재 이 예제에서는 클로져와 콜백함수를 적극 활용하여 마치 동기적으로 실행되는 것처럼 만들었다.

우선 add는 setTimout을 통해 시간차를 두고 두 수를 더하는 함수다. 해당 함수를 _async 함수로 한 번 래핑을 해주면서 문제를 해결한다. 실질적으로 add에는 _async 에서 반환하는 async_cb_receiver 함수가 할당된다. add를 실행시키면 바로 이 async_cb_receiver가 반환된다. 여기서 반환되는 async_cb_receiver 함수는 말그대로 callback 함수를 받는 함수다. 이는 add함수가 실행된 자리에서 callback을 받는 함수로 치환되어 다른 함수의 인자로 사용된다. 위의 예제에서는 sub(add(10, 15), 5) 에서 add(10, 15)의 예시가 해당된다.

add에 할당되기 위하여 실행되는 _async 를 확인해보자. 실제 실행하고자 하는 코드는 _async 실행에 전해지고 있는 익명함수다. 여기서는 setTimout을 이용하고 있는 비동기 익명함수다. 인자로 전해지는 익명함수의 인자는 ‘a’, ‘b’, ‘callback’이 있다. 하지만 실상 add를 실행할 때는 add(10, 15) 처럼 콜백에 해당하는 값은 인자값으로 전해지고 있지 않다. 이는 _async 함수 내부에서 콜백함수를 만들어내기 때문이다. 주석 2번에서 확인할 수 있다

잠시 콜백함수에 대한 논의를 뒤로 미뤄두고 피연산자들의 경로를 추적해보자. add함수에 전해진 10과 15는 그럼 어떤 함수의 arguments로 쓰일까? 필자가 분석하기로는 1번 함수의 arguments일 것이다. 하지만 이 함수가 실제로 10과 15의 사용처일까? 아직은 아니다. 실질적으로 사용되는 곳은 3번 ‘바로 실행되는 익명함수’이다. 또 정확히 꼽자면 func.apply(null, args) 문장이며 args는 이후 실행될 때 넘겨지는 arguments와 동일하다.

실제로 10과 15가 더해져서 콜백함수가 실행되길 기다리는 함수가 바로 _async 에 넘겨줬던 setTimout 익명함수다. 이 익명함수가 마침내 실행되는 코드가 func.apply(null, args) 이다. 여기서 중요한 것은 args에는 앞에서 미리 만들어두었던 콜백함수가 포함된다는 것이다. 이전에 callback 인자 위치에 함수가 전해지지 않아서 의문이었던 부분이 바로 이 실행문과 함께 새로 만든 콜백함수와 같이 전해지고 있으며, setTimeout 익명함수는 해당 콜백함수를 연산결과값을 인자로 실행을 해준다.

정리하자면 setTimeout에서 실행된 컬백함수를 통해 원래의 컨텍스트로 돌아올 수 있게 된 것이다.

이제 wait 함수를 살펴보도록 하자. wait은 sub(add(10, 15), 5) 에서 sub의 관점에서 보면 이해하기 쉽다.

add, sub, div 모두 실행하면 _async_cb_receiver 함수를 반환한다. 하지만 add(10, 15) 처럼 숫자가 있어야 연산이 가능하다. 그렇기에 sub(add(10, 15), 5) 처럼 둘 중 하나의 인자라도 함수를 사용하고 있으면 연산하기가 곤란한다. wait 함수는 그래서 자체적으로 실행할 때의 전해진 인자값들을 확인하면서 _async_cb_receiver 로 확인이 되면 값을 가져와 치환해주는 콜백함수를 전해준다.

그럼 실질적으로 setTimout 익명함수에서 실행되는 콜백함수는 wait에서 정의해준 값 치환 함수로 실행되어 앞서 연산된 결과값을 가져와 사용할 수 있게 만든다. 이를테면 add(10, 15) 의 결과값 25를 sub(25, 5) 로 치환시켜주는 역할을 한다는 뜻이다.

💡 여기서 질문. `log(sub(add(10,15), add(5,7)));` 는 어떤 순서로 실행이 될까? 괄호 가장 바깥에 있는 함수부터 실행이 될까, 아니면 괄호 가장 안쪽에 있는 함수부터 실행이 될까. 답은 가장 안쪽에 있는 함수부터 실행이된다. 생각해보면 기본적인 것인데도 인지를 못 하는 경우가 있다.

정리

클로저, 콜백, 컨텍스트, 렉시컬 스코프, arguments 등 많은 개념이 총집합해 있는 재밌는 예제였다. 고민을 많이 한만큼 남은 게 많은 공부였다. 특히 기존에 알고 있던 콜백의 개념을 다르게 생각하는 부분이 인상깊다. 컨텍스트를 돌려준다는 표현이 이제는 어떤 것을 의미하는지 더 와닿게 된 경험인 것 같다.

profile
블록체인 개발 공부 중입니다, 프로그래밍 공부합시다!

0개의 댓글