Ch1. 비동기성: 지금과 나중
- 프로그램에서 '지금'에 해당하는 부분 그리고 '나중'에 해당하는 부분 사이의 관계가 바로 비동기 프로그램의 핵심이다.
1.1 프로그램 덩이
- 자바스크립트 프로그램은 보통 여러 개의 덩이들로 구성된다. 가장 일반적인 프로그램 덩이 단위는 함수다.
- '나중'은 '지금'의 직후가 아니다. 지금 당장 끝낼 수 없는 작업은 비동기적으로 처리되므로 프로그램을 중단하지 않는다.
var data = ajax('http://naver.com);
console.log(data)
- 위 코드에서 ajax는 비동기적으로 지금 요청하고 나중에 결과를 받는다.
- 가장 간단한 방법은 콜백 함수라는 장치를 이용하는 것이다.
ajax("http://naver.com", function myCallback(data) {
console.log(data)
})
1.1.1 비동기 콘솔
- console.log는 환경에 따라 작동 방식이 다르도 종종 혼돈을 유발하니 무조건 콘솔창 결과에 의존하지 말고 자바스크립트 디버거의 중단점을 활용하자.
1.2 이벤트 루프
- 실제로 자바스크립트에는 비동기라는 개념이 없다. 자바스크립트 엔진은 요청하면 프로그램을 주어진 시점에 실행할 뿐이다.
- 자바스크립트 엔진은 혼자서는 안 되고 반드시 호스팅에서 실행된다. (브라우저, 노드) 여러 프로그램 덩이를 시간에 따라 매 순간 한번씩 엔진을 실행시키는 것은 이벤트 루프이다.
- '이벤트'를 스케줄링하는 일은 언제나 엔진을 감싸고 있던 주위 환경의 몫이었다.
- 아래는 이벤트 루프의 의사 코드다 (저자의 코드)
var eventLoop = []
var event
while (true) {
if (eventLoop.length > 0) {
event = eventLoop.shift()
try {
event()
} catch (err) {
reportError(err)
}
}
}
위 코드에 while 무한 루프가 있는데 매 순회를 틱이라고 한다. 틱이 발생할 때마다 큐에 적재된 이벤트를 꺼내어 실행한다.
setTimeout은 콜백을 이벤트 루프 큐에 넣지 않는다. 단지 타이머를 설정하는 함수고, 타이머가 끝나면 환경이 콜백을 이벤트 루프에 삽입한 뒤 틱에서 콜백을 꺼내어 실행한다. 즉, 이벤트 루프가 이미 몇개의 원소들로 가득차 있다면 콜백은 기다리게 된다. setTimeout이 항상 완벽하게 정확한 타이밍으로 동작하지 않는것이 이때문이다.
ES6에 이르러서는 작동 방식이 정확히 규정되어 호스팅 환경이 아닌 자바스크립트 엔진의 관할이 되었다.
1.3 병렬 스레딩
- 비동기와 병렬은 아무렇게나 섞어 쓰는 경우가 많지만 그 의미는 완전히 다르다. 비동기는 지금과 나중 사이의 간극에 관한 용어고 병렬은 동시에 일어나는 일들과 연관된다.
- 프로세스와 스레드는 가장 많이 쓰는 병렬 컴퓨팅 도구로, 별개의 프로세서, 심지어는 물리적으로 분리된 컴퓨터에서도 독립적으로 실행되며 여러 스레드는 하나의 프로세스 메모리를 공유한다.
- 반면 이벤트 루프는 작업 단위로 나누어 차례대로 실행하지만 공유 메모리에 병렬로 접근하거나 변경할 수는 없다. 병렬성과 직렬성이 나뉜 스레드에서 이벤트 루프를 협동하는 형태로 공존하는 모습이다.
- 자바스크립트는 절대로 스레드 간에 데이터를 공유하는 법이 없으므로 비결정성의 수준은 문제가 되지 않는다. 하지만 자바스크립트가 항상 결정적인것도 아니다.
1.3.1 완전-실행
- 자바스크립트의 작동 모드는 단일 스레드이다. 아래 코드를 예를 들자면
var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
ajax('http://naver.com/1)
ajax('http://naver.com/2)
- foo와 bar는 상대의 실행을 방해할 수 없으므로 이 프로그램의 결괏값은 먼저 실행되는 함수가 좌우한다.
// 결과 1
var a = 1
var b = 2
// foo
b = b * a
a = b + 3
// bar
a = 8 + b
b = a * 2
a // 11
b // 22
// 결과 2
var a = 1
var b = 2
// bar
a = 8 + b
b = a * 2
// foo
b = b * a
a = b + 3
a // 183
b // 180
- 똑같은 코드인데 결괏값은 두 가지이므로 이 프로그램은 비결정적이다. 하지만 여기서 비결정성은 함수 순서에 따른것이지, 스레드처럼 표현식의 처리 순서 수준까지는 아니므로, 스레드보다는 결정적이라고 할 수 있다.
- 자바스크립트에서는 함수 순서에 따른 비결정성을 흔히 경합 조건이라고 표현한다.
1.4 동시성
- 동시성은 복수의 '프로세스'가 같은 시간 동안 동시에 실행됨을 의미하며, 각 프로세스 작업들이 병렬로 처리되는지와는 관계없다.
- 예를 들어, 사용자가 스크롤바를 아래로 내리면 계속 갱신된 상태 리스트가 화면에 표시되는 웹 페이지를 만들고자 한다. 이런 기능은 2개의 분리된 '프로세스'를 동시에 실행할 수 있어야 제대로 기능한다. 첫 번째 프로세스는 사용자가 페이지를 스크롤바로 내리는 순간 발생하는 onscroll 이벤트에 반응한다. 두 번째 프로세스는 ajax 응답을 받는다.
- onscroll 이벤트와 ajax 응답 이벤트는 동시에 발생할 수 있다. 자바스크립트는 한 번에 하나의 이벤트만 처리하므로 정확히 같은 시각에 실행되는 일은 결코 있을 수 없다.
onscroll, request1
onscroll, request2
response1
onscroll, request3
response2
response3
onscroll, request4
onscroll, request5
onscroll, request6
response4
onscroll, request7
response6
response5
response7
- 단일 스레드 이벤트 루프는 동시성을 나타내는 하나의 표현 방식이다.
1.4.1 비상호 작용
- 어떤 프로그램 내에서 복수의 '프로세스'가 단계/이벤트를 동시에 인터리빙 할 때 이들 프로세스 사이에 연관된 작업이 없다면 프로세스 간 상호 작용은 사실 의미가 없다. 즉, 비결정성은 완벽하게 수용이 가능하다.
var res = {};
function foo(results) {
res.foo = results;
};
function bar(results) {
res.bar = results
};
ajax('http://naver.com/1)
ajax('http://naver.com/2)
- 위 코드는 foo와 bar 중 누가 먼저 실행되는 서로 영향을 끼치지 않으니 실행 순서를 문제 삼을 필요가 없다.
1.4.2 상호 작용
- 동시 프로세스들은 필요할 때 스코프나 DOM을 통해 간접적으로 상호 작용을 한다. 이때 이미 한번 살펴봤던 것처럼 경합 조건이 발생하지 않도록 잘 조율해주어야 한다.
var res = []
function response(data) {
res.push(data)
}
ajax("http://naver.com/1", response)
ajax("http://naver.com/2", response)
- 위 코드에서 개발자 의도는 아마 1, 2 url 결과를 순서대로 res에 담고 싶었을 것이다. 하지만 코드 순서가 아니라 응답 순서대로 res 배열에 저장이 될 것이다. 따라서 이 순서를 해결하려면 상호 작용의 순서를 잘 조정해야 한다.
var res = []
function response(data) {
if (data.url === "http://naver.com/1") {
res[0] = data
} else if (data.url === "http://naver.com/2") {
res[1] = data
}
}
// ...
1.4.3 협동
- 협동적 동시성은 실행 시간이 오래 걸리는 '프로세스'를 여러 단계/배치로 쪼개어 다른 동시 '프로세스'가 각자 작업을 이벤트 루프 큐에 인터리빙 하도록 하는 게 목표다.
var res = []
function response(data) {
res = res.concat(
// 배열의 원소를 하나씩 변환한다.
// 원래 값을 2배로 늘린다.
data.map(function (val) {
return val * 2
})
)
}
ajax("http://naver.com/1", response)
ajax("http://naver.com/2", response)
- 위 코드에서 데이터가 천만개 정도라면 해당 함수를 실행하는 동안 페이지가 먹통이 된다. 좀 더 친화적이고 협동적인 동시 시스템이 되려면 각 결과를 비동기 배치로 처리하고 이벤트 루프에서 대기 중인 다른 이벤트와 함께 실행되게끔 해야 한다.
var res = []
function response(data) {
var chunk = data.splice(0, 1000)
res = res.concat(
chunk.map(function (val) {
return val * 2
})
)
if (data.length > 0) {
setTimeout(function () {
response(data)
}, 0)
}
}
// ...
- 위 코드는 최대 1000개 덩이 단위로 데이터 집합을 처리했다. 물론 더 많은 후속 '프로세스'를 처리해야 하지만 각 '프로세스' 처리 시간은 단축된다.
1.5 잡 큐
- 잡 큐는 ES6부터 이벤트 루프 큐에 새롭게 도입된 개념이다. 주로 프라미스의 비동기 작동에서 가장 많이 보게 될 것이다.
- 잡 큐는 이벤트 루프 큐에서 '매 틱의 끝자락에 매달려 있는 큐'라고 생각하면 알기 쉽다. 이벤트 루프 틱 도중 발생 가능한 비동기 특성이 내재된 액션으로 인해 전혀 새로운 이벤트가 이벤트 루프 큐에 추가되는 게 아니라 현재 틱의 잡 큐 끝 부분에 추가된다.
- 자세한 내용은 3장에서 프라미스 비동기 작동을 다룰 때 설명한다.
1.6 문 순서
- 자바스크립트 엔진은 반드시 프로그램에 표현된 문의 순서대로 실행하지 않는다.
- 중요한 것은 여기서 설명하는 내용은 육안으로 확인할 수 없다는 점이다.
- 자바스크립트 엔진은 코드를 컴파일한 뒤 문 순서를 재정렬하면서 실행 시간을 줄일 여지는 없는지 확인한다.
'JavaScript' 카테고리의 다른 글
[You don`t Know JS 정리] ch.3 프라미스 (0) | 2020.08.24 |
---|---|
[You don`t Know JS 정리] ch.2 콜백 (0) | 2020.08.21 |
[You don`t Know JS 정리] ch.6 작동 위임 (0) | 2020.08.18 |
[You don`t Know JS 정리] ch.4 클래스와 객체 혼합 (0) | 2020.08.17 |
[You don`t Know JS 정리] ch.3 객체 (0) | 2020.08.16 |