자바스크립트는 기본적으로 코드가 위에서 아래로 한 줄씩 차례로 코드가 해석합니다. 이러한 특성을 동기라 부릅니다.
fetch 나 setTimeout 등과 같이 오랜 시간을 요구하는 기능들이 동기와 같은 특성으로 실행된다면 어떠할까?? 코드를 한 줄씩 해석하다 이런 코드를 접하여 2초가 걸리든 10초가 걸리든 다음 코드는 실행이 되지 않을 것이고 웹사이트가 제대로 작동되지 않아 성능과 사용자 경험에 최악일 것입니다.
그래서 도입된 것이 비동기이며 이런 기능들은 비동기적으로 처리가 됩니다. 비동기는 동기와 다르게 코드 완료를 기다리지 않고 다음 코드를 실행하게 됩니다.

기본적으로 자바스크립트의 엔진이 싱글스레드 언어이기 때문입니다. Call Stack이 한 줄의 코드의 실행을 도와주는데 이게 한 개이기 때문입니다. 그래서 한 줄씩 위에서 차례로 동기적으로 실행이 됩니다.
데이터를 임시 저장하는 곳으로 함수나 변수, 함수가 실행될 때 사용하는 값이 저장되는 곳입니다.
MDN에서 이벤트 루프와 함께 작동원리를 설명할 때 대략적으로 변수 등 정보의 저장은 메모리힙에서 관리된다고 설명이 되었지만 일부만 맞는 말입니다.
call stack에서 함수가 정의될 때 변수값, this 바인딩, scope chain 등의 여러 정보들이 담긴 실행 컨텍스트가 저장이 되게 되는데 원시값인 경우에는 call stack에 변수가 저장이 되고 객체인 경우 메모리 힙에 정보를 저장하고 메모리 주소만 call stack에 저장되게 됩니다.

코드의 실행에 따라 함수의 호출 스택이 쌓입니다.
function multiplyNumber(num1, num2) {
return num1 * num2;
// 3. num1 * num2 이 쌓입니다.
// 4. Memory Heap에 num1과 num2를 찾아 연산하게 되어 호출 스택에서 제거 됩니다.
}
function printSquare(x) {
let n = multiplyNumber(x, x);
// 2. multiplyNumber 가 쌓입니다.
// 5. multiplyNumber 호출이 완료되어 제거됩니다.
console.log(n);
// 6. n의 값을 Memory Heap에서 찾고 콘솔을 실행합니다.
}
// 7. printSquare 함수가 모두 실행되어 호출 스택에서 제거됩니다.
printSquare(5); // 1. printSquare가 호출되어 먼저 쌓입니다.

싱글스레드 언어인 자바스크립트에서 비동기는 어떻게 처리할까요?
이 때 나오는 개념이 Event Loop(이벤트 루프)와 Queue(큐)입니다.
콜 스택 처럼 비동기 관련 실행코드(setTimeout, Promise etc..)의 코드가 쌓이게 됩니다.
이벤트루프는 콜스택과 큐의 상태를 보고 콜스택이 비어있으면 큐에 있는 테스크를 콜스택에 전달합니다.

console.log(1); // 1. 콜스택에 쌓이게 되고 콘솔에 1이 찍혀 콜스택에 제거됩니다.
setTimeout(function timeout(){
console.log(2);
// 4. 콜스택에 모두 비어지게 되고 이벤트루프는 이 코드를 콜스택으로 옮깁니다. 그래서 2가 콘솔에 찍히고 콜스택은 비워집니다.
});// 2. setTimeout을 만났습니다. 이벤트루프는 큐에 전달을 합니다.
console.log(3); // 3. 다음코드가 실행됩니다. 1번과 같습니다.
// 콘솔 : 1 3 2
비동기의 문제점이 있습니다. 바로 순서를 보장하지 않는다는 점입니다.
function getDB() {
let data;
// 데이터베이스에서 값을 가져오는 3초 걸린다고 가정 (비동기 처리)
setTimeout(() => {
data = 100;
}, 3000);
return data;
}
function main() {
let value = getDB();
value *= 2;
console.log('value의 값 : ', value);
}
main(); // 메인 스레드 실행
이 경우는 어떻게 실행이 될까요? 200이 찍힌다고 생각하지만 실제로는 NaN이 찍힙니다.
그 이유를 위와같이 다시 설명하겠습니다.
function getDB() {
let data;
setTimeout(() => {
data = 100;
}, 3000);
// 3. setTimeout을 만나게 되고 이벤트 루프는 큐에 저장합니다.
return data;
// 4. 자바스크립트는 다음 코드를 실행하고 data는 선언만 한 상태라 undefined가 return 됩니다.
}
function main() {
let value = getDB();
// 2. getDB가 실행되고 역시 콜스택에 쌓입니다.
// 5. value는 getDB에 undefined를 받게 됩니다.
value *= 2;
// 6. undefined에 숫자를 연산했기에 NaN이 나옵니다.
console.log('value의 값 : ', value);
// 7. 콘솔에 value 값으로 NaN이 찍힙니다.
}
main(); // 1. 메인이 실행되고 콜스택에 쌓입니다.
위의 코드에서 문제는 value 값을 받기도 전에 다른 코드가 실행되어 순서가 보장이 되어야 하는 상황이라 생긴 문제입니다.
그래서 getDB코드 안에 main 코드들을 함수로 직접 전달하여 봅시다.
이런 방법을 콜백이라 부릅니다. 콜백이란 다른 코드를 인수로서 넘겨주는 실행 가능한 코드입니다.
function getDB(callback) {
let data;
setTimeout(() => {
data = 100;
callback(data);
}, 3000);
}
function main() {
getDB(function(value){
value *= 2;
console.log('value의 값 : ', value);
}); // 이렇게 실행할 코드를 직접적으로 전달해줍니다.
}
main();
이렇게 되면 순서를 보장받아 원하는대로 실행이 됩니다. 하지만 비동기 처리할 때 콜백으로 처리하게되면 콜백의 콜백이 필요한 상황에서는 뎁스가 깊어지면서 콜백이 중첩적으로 쌓이게 됩니다. 이 문제는 콜백이 함수의 인자로 쓰이면서 내부에서 처리하면서 가독성을 해치게 되는 이유입니다.

그러면 어떻게 처리를 하면 좋을까요?
그래서 나온 개념이 Promise입니다. Promise 객체를 생성하고 비동기 결과값을 처리할 수 있게 됩니다. 이를 처리하기 위해 .then을 사용하여 성공 값 / 실패 값을 처리할 수 있습니다.
콜백에서는 내부에 처리되고 이게 중첩이 되면 내부가 깊어지기 때문에 뎁스가 깊어지는 단점이 있습니다.
하지만 Promise인 경우 내부에서 처리하지 않고 then을 통해 연쇄어 처리하기 때문에 가독성도 해치지 않고 해석하기 쉬운 코드가 되게 됩니다.
function getDB() {
return new Promise((resolve, reject)=>{
// Promise 객체는 resolve와 reject 콜백을 받습니다.
// 그래서 성공 시 resolve에 값을 넣고
// 에러 등 실패는 reject에 넣어 전달하게 됩니다.
let data;
setTimeout(() => {
data = 100;
}, 3000);
resolve(data);
})
}
function main() {
// getDB는 Promise객체이고 resolve값을 반환받습니다.
let value = getDB()
.then((data)=> data * 2)
// then을 통해 resolve 값을 전달 받게 됩니다.
.then((data)=> {
console.log('value의 값 : ', value)
})
// 이렇게 then을 연쇄적으로 할 수 있으며 이를 then 체이닝이라 부릅니다.
.catch((error)=>{
console.error(error);
})
// then이 성공 값을 컨트롤한다면 catch는 실패 값을 컨트롤 합니다.
}
main();
then 체이닝할 때에도 then 지옥에 빠질 위험이 있습니다. 그래서 나온 개념이 async / await 입니다. 동기적인 코드로 보여 가독성을 좀 더 높힙니다. 다만 async / await는 reject의 처리가 따로 없기 때문에 try catch 문으로 감싸주는 것이 좋습니다.
function getDB() {
return new Promise((resolve, reject)=>{
// Promise 객체는 resolve와 reject 콜백을 받습니다.
// 그래서 성공 시 resolve에 값을 넣고
// 에러 등 실패는 reject에 넣어 전달하게 됩니다.
let data;
setTimeout(() => {
data = 100;
}, 3000);
resolve(data);
})
}
async function main() { // 함수 키워드 앞에 async를 표시합니다.
try{
let value = await getDB();
// Promise 객체를 반환하는 함수 호출에 await 키워드를 붙여줍니다. await은 async 함수 내에서만 사용 가능 합니다.
data *= 2;
console.log('data의 값 : ', data);
}catch(error){
console.error(error);
}
}
main();