동기 ∙ 비동기
DEV ·1. 동기(synchronous)
자바스크립트는 한 번에 하나의 작업만 수행한다.
‘동기’라는 상황은 쉽게 말해, 한 작업이 실행되는 동안 다른 작업은 실행되지 않고 기다리는 것이다.
이러한 자바스크립트의 특성은 그 엔진의 주요 구성 요소로 인해 나타나며, 그 종류로는 Memory Heap, Call Stack이 있다.
- Memory Heap : 변수, 객체의 메모리 할당을 담당한다.
- Call Stack : 호출된 함수가 쌓이는 곳을 의미한다. stack이라는 단어에서 알 수 있듯, LIFO(Last in First Out)의 구조로 이루어져 있다.
1-1. CALL STACK
함수 내의 다른 함수를 실행하는 경우, 이를 debugger
를 통해 보면 아래와 같이 callstack이 쌓여있는 것을 볼 수 있다.
이는 함수의 실행 순서를 stack의 형태로 표현한 것인데, foo
내에 있는 함수 bar
가 먼저 실행을 다 마쳐야 foo
가 다시 진행되는 것을 알 수 있다.
const baseData = [1, 2, 3, 4, 5, 6, 100];
function foo() {
baseData.forEach((v, i) => {
console.log("sync ", i);
bar();
});
}
function bar() {
baseData.forEach((v, i) => {
debugger;
console.log("sync 2", i);
});
}
foo();
2. 비동기(asynchronous)
자바스크립트의 단일 스레드, 동기식 동작과 달리, 어떠한 요청을 보내면 그 요청이 끝나기 전에 바로 다음 동작이 진행되는 것을 말한다.
동기식으로만 진행되는 경우, 하나의 작업에 많은 시간이 걸리게 되면, 다음 동작들이 모두 영향을 받게 되어 전체적인 속도가 느려지게 되고, 이는 매우 비효율적인 것으로 볼 수 있다.
이러한 작업의 비효율성을 줄이기 위해 나타난 방식이 비동기 방식이다.
2-1. 여러 이벤트의 동시 동작 원리
자바스크립트의 기본 동작 원리로는 안되는 비동기는 자바스크립트의 실행 환경(runtime)을 통해 이루어낼 수 있다.
브라우저가 바로 그 환경인데, 여기서는 DOM, AJAX와 같은 비동기를 위한 web API가 들어가있어 비동기 작업을 해낼 수 있다.
더불어, 이것들의 제어를 위한 Event Loop, Callback Queue가 존재한다.
2-2. 비동기의 동작 순서
console.log("FIRST");
setTimeout(() => console.log("THIRD"), 5000);
console.log("SECOND");
/*
THIS IS FIRST
THIS IS SECOND
THIRD
*/
우선 위 코드들을 실행함으로서, ‘first → setTimeout() → second’ 의 순서로 내용들이 진행된다.
위 코드에서 setTimeout은 web API(브라우저의 제공 API)로, 자바스크립트의 런타임 환경에서 별도의 API로 존재한다.
API가 실행되게 되면, stack에서 우선 실행되는데, 이는 호출을 시킨다는 의미로 호출 후 stack에서는 사라지게 된다.
그리고 이 API는 작성된 코드의 중간에 갑자기 끼어들어서는 안 된다.
실행이 완료된 모든 WEB API는 task queue에 들어가서 대기하게 된다.
event loop가 call stack과 task queue를 지속적으로 주시하는데, stack이 비어있게 되면, 첫 번째 실행될 API부터 순차적으로 가져와 실행하게 된다.
- 동기 ∙ 비동기 상황에 대한 다른 예시
function plus() {
let a = 1;
setTimeout(() => console.log(++a), 1000);
return a;
}
const result = plus();
console.log("result :", result);
/*
result : 1
2
*/
위 예시에서, console.log
가 마무리 된 후에 API가 진행된 것을 볼 수 있다.
이를 통해, API는 모든 stack내의 내용들이 정리된 후에 event loop를 통해서 진행됨을 알 수 있다.
3. 비동기의 예시
3-1. for, setTimeout()
const baseData = [1, 2, 3, 4, 5, 6, 100];
const asyncRun = (arr, fn) => {
for (var i = 0; i < arr.length; i++) {
setTimeout(() => fn(i), 1000);
}
};
asyncRun(baseData, (idx) => console.log(idx));
// 7 7 7 7 7 7 7
위 코드를 보면, 원래의 의도(0 1 2 3 4 5 6)와 달리 7이 계속 출력된 것을 볼 수 있다.
이는 var
가 가진 특징 때문인데, var
는 함수레벨 스코프를 가지며, 전역변수의 값이 변경될 수 있게 된다.
const baseData = [1, 2, 3, 4, 5, 6, 100];
var i = 2;
const asyncRun = (arr, fn) => {
for (var i = 0; i < arr.length; i++) {
setTimeout(() => fn(i), 1000);
}
console.log(i); // 7
};
asyncRun(baseData, (idx) => console.log(idx));
// 7 7 7 7 7 7 7 7
위 코드를 보면, var i = 2;
로 값을 지정해준 상태에서, asyncRun
함수 안에서 console.log
가 이루어지기 때문에, 값이 2로 나와야 한다.
그러나, for문 내의 var i = 0
을 통해서 값이 재지정되었기 때문에 7이라는 값이 나오게 된다.
setTimeout()
역시 이미 1초보다 훨씬 이전에 처리된 i
의 값으로 처리가 진행되기 때문에 7만 나오게 된다.
const baseData = [1, 2, 3, 4, 5, 6, 100];
var i = 2;
const asyncRun = (arr, fn) => {
for (let i = 0; i < arr.length; i++) {
// var를 let으로 변경
setTimeout(() => fn(i), 1000);
}
console.log(i); // 2
};
asyncRun(baseData, (idx) => console.log(idx));
// 2 0 1 2 3 4 5 6
이와 달리, let
은 블록레벨 스코프를 가져, 함수, if문, while문 등 중괄호로 된 곳내에서만 작동한다.
실행 시 1차적으로 for
문 밖 i의 값이 2로 제대로 출력되는 것을 볼 수 있고, 내부에서도 원래 의도대로 출력되는 것을 볼 수 있다.
3-2. forEach
const baseData = [1, 2, 3, 4, 5, 6, 100];
const asyncRun = (arr, fn) => {
arr.forEach((v, i) => {
setTimeout(() => fn(i), 1000);
});
};
asyncRun(baseData, (idx) => console.log(idx));
// 0 1 2 3 4 5 6
forEach
로 실행할 시에는 따로 영향받는 요소 없이 지정한 시간 뒤에 모두 제대로 실행되는 것을 볼 수 있다.
3-3. 비동기 + 비동기
const baseData = [1, 2, 3, 4, 5, 6, 100];
const asyncRun = (arr, fn) => {
arr.forEach((v, i) => {
setTimeout(() => {
setTimeout(() => {
console.log("cb 2");
fn(i);
}, 1000);
console.log("cb 1");
}, 1000);
});
};
asyncRun(baseData, (idx) => console.log(idx));
/*
cb 1
cb 1
cb 1
cb 1
cb 1
cb 1
cb 1
cb 2
0
cb 2
1
cb 2
2
cb 2
3
cb 2
4
cb 2
5
cb 2
6
*/
비동기 내에 또다른 비동기적인 함수를 넣게 되는 경우에는 우선적으로 들어온 함수를 처리하고, 그 다음 들어온 함수의 순서대로 처리되는 것을 볼 수 있다.
이를 통해, TASK QUEUE에서 QUEUE의 의미를 다시 알 수 있다.