1. 비동기 프로그래밍 개요
보통 동기/비동기 방식은 Client-Server간의 요청과 응답 관계를 통해 설명한다. 동기 방식은 클라이언트와 서버가 함께 협력하여 일한다는 것이다. client가 일을 안할 때는 server가 일을 하고, server가 일을 안할때는 client가 일을 하게 된다. 비동기 방식은 client가 요청한 일을 server가 완료하기 전에, client가 다른일들을 처리하는 것이다. 다시 말해 client가 어떤 시간이 걸리는 일을 server에 시켜놓고, server가 그 일을 하는 동안 client는 그 다음 일들에 접근하여 처리하는 방식이다. sever에서 실행이 끝날 때까지 기다리는 방식으로 일을 해서는 웹페이지나 프로그램의 정상적인 작동이 어렵기 때문이다.
client - server 단에서가 아닌, local 프로그램에서 생각해보자면, 아래 예시와 같이 프로그램은 실행 후 완료에 시간이 소요되는 코드(함수)가 있더라도, 일단 실행하고 그 코드가 완료되기 전에 다음 코드를 실행한다. 즉, 비동기적으로 코드를 처리하는 방식이다.
2. 비동기 프로그램의 작동원리(Javascript)
Javascript는 Single-threaded engine 으로써 내부적으로는 한 번에 한개씩의 작업밖에 할 수 없다. 하지만 setTimeout(), DOM Event와 같이 나중에 실행되거나 특정 event 발생 시에만 실행되는 Web APIs를 사용한다. 이런 작업들은 어떻게 사용이 될까? 아래의 설명은 참조1)의 링크에서 가져온 내용이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const processImage = (image) => {
/**
* doing some operations on image
**/
console.log('Image processed');
}
const networkRequest = (url) => {
/**
* requesting network resource
**/
return someData;
}
const greeting = () => {
console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
|
cs |
processImage 라는 함수가 이미지에 어떤 작업을 하는 함수라서 실행에 일정 시간이 소요되고, networkRequest 함수 또한 network resource를 요청하는데 시간이 소요되는 함수라고 가정하자. greeting 함수는 console.log만 실행하므로 거의 즉시 실행이 가능한 함수이다.
Javascript는 call stack과 Callback queue(message queue)에 함수들을 담으면서 실행을 한다. 우선 processImage 함수가 실행되기 때문에 Call stack에 processImage 함수가 쌓이게 된다. Image processing을 하는 함수와 console.log('Image processed')가 call stack에 차례대로 쌓이고, console.log('Image processed')는 곧바로 실행되어 stack에서 빠지게 된다.
Image processing에 시간이 걸리기 때문에, image processing 함수는 callback queue 쪽으로 넘어가고, 바로 다음 networkRequest 함수가 callback에 쌓인다. networkRequest 함수 또한 시간이 걸리기 때문에, image processing 함수 다음으로 callback queue에 담긴다. (networkRequest 함수의 실행시간이 image processing 함수의 실행시간보다 길다면)
greeting 함수가 실행되어 greeting 함수와 console.log('Hello World')가 call stack에 쌓이고, 바로 실행되어 call stack이 비워진다. call stack이 비워지고 난 후에야 callback queue에 쌓여있던 image processing 함수와 networkRequest 함수가 차례대로 실행된다(callback). 즉, 차례대로 다시 call stack으로 넘어가서 실행되게 된다.
이런 코드들처럼, 시간이 걸려서 비동기적으로 실행이 되는 코드는 대표적으로 setTimeout, DOM Events, Fetch 함수 등이 있으며 이런 함수들은 원래 Javascript engine에 포함된 것이 아니라, WEB APIs에서 불러오는 함수이다. 즉 browser나 Nodejs와 같은 Javascript의 runtime 환경에서 불러오는 API임을 기억하자.
DOM Events
Dom Events도 동일한 원리로 작동한다고 보면된다. 어떤 요소에 이벤트를 걸어놓으면, 그 이벤트가 실행될 때까지 해당 요소는 callback 형태로 callback queue에 담겨있다가, 이벤트가 발생하면 call stack으로 넘어오면서 실행이 되는 것이다.
1
2
3
|
document.querySelector('.btn').addEventListener('click',(event) => {
console.log('Button Clicked');
});
|
cs |
요소에 'click' EventListener를 달아주면, 그 요소가 실제로 클릭될때까지는 callback queue에 담겨있게 된다.
ES6 Job Queue / Micro-Task queue
ES6에서는 message queue 말고도 Job Queue / Micro-Task queue라는 개념이 도입되었다. Callback 되는 함수들도 우선순위를 가져서, 특정 함수는 message queue가 아닌 job queue나 micro-task queue에 담겨서 message queue보다 더 빨리 실행되게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
console.log('Script End');
//결과
Script start
Script End
Promise resolved
setTimeout
|
cs |
비동기적으로 실행되는 setTimeout과 Promise에 대해, setTimeout이 먼저 message queue에 저장되었음에도 불구하고, Promise가 먼저 실행되었음을 확인할 수 있다. Promise는 micro-task queue에 저장되어 setTimeout보다 먼저 실행된 것이다.
3. 비동기 프로그램의 순서 제어 (Callback)
만약 여러 함수들의 실행에 걸리는 시간이 제각각이라도 정해진 순서대로만 비동기 함수를 실행해야 한다면, callback 함수를 중첩하는 개념을 사용해야 한다.
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
|
// 1. 랜덤한 순서로 함수들이 실행됨
let printer = function (letters) {
setTimeout(
() => {
console.log(letters)
},
Math.floor(Math.random() * 100) + 1
)
}
let allPrinter = function() {
printer("a")
printer("b")
printer("c")
}
allPrinter()
// 2. 정해진 순서대로 함수들이 실행됨
let printer = function (letters, callback) {
setTimeout(
() => {
console.log(letters)
callback()
},
Math.floor(Math.random() * 100) + 1
)
}
let allPrinter2 = function() {
printer("a", () => {
printer("b", () => {
printer("c", () => {} )
})
})
}
allPrinter2()
|
cs |
1. 랜덤한 순서대로 함수가 실행되는 부분은 callback 함수가 없다. 따라서 setTimeout의 random 요소에 의해 함수의 실행시간이 임의적으로 설정되고, allPrinter()로 실행 시, 원하는 순서("a" -> "b" -> "c")대로 실행되지 않고 실행시마다 매번 실행 순서가 달라지게 된다. 구체적으로는 13~15번 줄의 printer 함수가 순서대로 call stack에 쌓이고나서 timer로서 web APIs 처리 부분으로 넘어가게 되고, 먼저 완료된 함수들부터 message queue에 쌓인 후 call stack에서 다시 실행되는 구조이다.
원리에 대한 그림은 다음 참조 사이트에서 확인 가능하다. (참조 2)
(helloworldjavascript.net/pages/285-async.html)
2. 정해진 순서대로 함수들이 실행되게 하기 위해서, 비동기 함수 setTimeout을 사용하는 printer 함수에 callback 인자를 추가해주고, callback 함수 실행문을 삽입하였다. 그리고 printer 함수를 실행하는 allPrinter2 함수에서 다음 순서로 실행되야 하는 함수를 각 함수의 콜백함수로 넣어주었다. 예를들어 "printer("a", 콜백함수...)" 구문의 경우 실행을 하게되면 console.log("a")가 실행되고 그 다음 callback 함수가 실행되게 되어 있으므로 콜백함수인 'printer("b", 콜백함수...)'는 "printer("a", 콜백함수...)"의 실행에 걸리는 시간이 얼마이든지 간에 해당 함수의 실행 완료 후에야 실행되는 구조인 것이다.
이렇게 callback 함수를 중첩하면, 원하는 순서대로 비동기 함수가 실행되도록 만들 수 있다.
다만, 많은 수의 callback 함수를 중첩하면 코드의 가독성이 떨어지게 되므로(Callback hell, 콜백지옥) 각 callback 함수의 return 값을 다음 callback 함수의 파라미터 값으로 넣어주는 등의 조치가 필요하다.
(참조 코드 - joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/)
또는 Promise, async/await 문법을 많이 사용하게 되는데, 다음 글에서 살펴보도록 하자.
실행 순서가 헷갈리는 예제들
1. 비동기 함수와 동기 함수의 연속 실행
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
|
console.log("a")
setTimeout(() => { //실행에 1초가 걸림
console.log("b")
}, 1000);
functionTakesFiveSeconds(); //실행에 5초가 걸림
setTimeout(() => { //실행에 0.5초가 걸림
console.log("c")
}, 500);
console.log("d");
function functionTakesFiveSeconds() {
let count = 1;
let start = new Date();
for(let i = 0; i < 500000000; i++) {
count++;
}
let endTime = new Date();
console.log(endTime - start);
}
//실행 결과
a
5805
d
b
c
|
cs |
위 코드의 실행 순서 및 동작원리는 다음과 같다.
시사점은 비동기 함수는 따로 실행순서를 설정해주지 않으면, 동기 함수들이 모두 실행되고 나서야 실행 결과를 출력한다는 것이다.
1. console.log("a")가 실행된다.
2. setTimeout(console.log("b"))에 해당하는 코드가 실행되며 Timer가 작동하고, 바로 다음에 functionTakesFiveSeconds() 함수가 실행된다. 해당 함수 실행 중에 Timer는 완료되어 setTimeout(console.log("b"))는 callback queue에서 대기한다.
3. functionTakesFiveSenconds 완료 후 setTimeout(console.log("c")가 실행되어 Timer가 진행된다.
4. callstack에 console.log("d")가 쌓였다가 실행되면서 , d 가 출력된다.
5. callstack이 완전히 비워졌으므로, callback queue에서 대기 중이던 console.log("b")가 실행된다.
6. 대기 중이던 console.log("c")가 실행된다.
2. callback 함수 오적용
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
|
// 정상작동 함수. a > x > b
console.log("a")
setTimeout(project, 1000);
setTimeout(function() {
console.log("b")
}, 5000);
function project() {
console.log("x")
}
// 비정상작동 함수 a > x > error
console.log("a")
setTimeout(project(), 1000);
setTimeout(function() {
console.log("b")
}, 5000);
function project() {
console.log("x")
}
|
cs |
비정상작동 함수는 setTimeout의 callback 함수로 project함수의 실행문 'project()'를 넣어주었다. project 함수는 "x"를 콘솔에 출력하는 기능을 할 뿐이므로 return 값이 없고, 결과적으로 'project()'는 undefined 함수가 된다. 따라서 setTimeout(undefined, 1000)을 입력하는 것으로 취급되어 error 메시지가 출력된다.
4. Callback error handling
클라이언트 - 서버 간 통신 등에 비동기 프로그래밍을 많이 적용하는데, 클라이언트나 서버에 문제가 생겨 error가 날때가 있다. 이럴 경우 callback 함수의 인자로 err, data를 받아와서 처리하게 된다. 'err'는 '에러가 났을 경우 error와 관련한 정보', 'data'는 '성공적으로 실행되었을 경우의 정보'를 담고 있는 파라미터이다. 정확한 이해를 위해서는 다음 글에서 살펴볼 Promise의 resolve, reject 개념과 비교해보면 되니, 일단 callback 함수에서도 error와 data를 처리할 수 있는 코드가 있다는 것만 이해하고 넘어가자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 사용 예시
let interactionClientWithServer = callback => {
if(active) {
callback(null, resultWhenSucceed)
}
if(inactive) {
callback(resultWhenFailed, null)
}
}
// 실제 코드 예시
interactionClientWithServer((err, date) => {
if (err) {
console.log('Failed to interact')
return ;
}
return data
})
|
cs |
다음글
Javascript / 기초 / 비동기 프로그래밍(Asynchronous) : Promise
참조
1) Javascript Asynchronous/Synchronous 및 Stack, queue, Event loop 구동원리 설명 (쉬운 영어)
blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff
2) Javascript로 만나는 세상 - 비동기 프로그래밍
helloworldjavascript.net/pages/285-async.html
3) 캡틴판교 - 자바스크립트 비동기 처리와 콜백 함수
joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/
'Programming-[Frontend] > Javascript' 카테고리의 다른 글
Javascript / 기초 / 비동기 프로그래밍(Asynchronous) : Async/await (0) | 2020.09.27 |
---|---|
Javascript / 기초 / 비동기 프로그래밍(Asynchronous) : Promise (0) | 2020.09.27 |
Javascript / Tips / MDN 문서에서 대괄호 [ ] (brackets)의 의미 (0) | 2020.09.18 |
Javascript / 기초 / Inheritance, Object - oriented (상속, 객체지향) (0) | 2020.09.09 |
Javascript / 배열, 객체 / 구조 분해 할당(destructing) (0) | 2020.09.05 |