함수가 실행되기 위해선 함수 실행 컨텍스트가 실행 컨텍스트 스택에 푸시되어야 한다. 이는 반대로 보았을 때, 실행 컨텍스트 스택에 함수 실행 컨텍스트가 푸시되었다는 것은 함수의 실행을 의미한다고 말할 수 있다.
단, 자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 가진다는 점을 항상 기억해야한다. 이 말은, 자바스크립트가 한 번에 하나의 함수만을 실행할 수 있다는 말이다. 즉, 자바스크립트는 동시에 2개 이상의 함수를 실행할 수 없다. 현재 실행중인 실행 컨텍스트를 제외한 나머지는 모두 대기중인 컨텍스트들이다. 만약 현재 실행중인 실행 컨텍스트가 완료되어 스택에서 사라지면, 비로소 대기중인 컨텍스트들이 하나씩 실행된다.
이를 싱글 스레드 방식이라고도 한다. 한 번에 하나의 함수만 실행하기 때문에, 만약 함수의 실행에 시간이 필요하다면 작업이 중단하는 현상이 일어나는데, 이를 블로킹이라고 한다.
직원이 한명밖에 없는 카페의 예시를 들어보자
직원 앞에 손님 1,2,3이 줄을 서고 있다.
손님 1이 아이스 아메리카노를 주문한다.
직원이 주문을 받고 음료를 만든다.
직원이 음료를 만들어 손님 1에게 준다.
손님 2가 아이스 카페라떼를 만든다.
직원이 주문을 받고 음료를 만든다.
반복
.
.
.
이처럼 요청이 있을 때 곧바로 결과가 따라오고, 이 요청이 종료될 때 까지 다음 요청이 대기하는 방식을 동기 처리라고 한다.
그러나 다른 예를 생각해보자. 이번 카페는 직원이 많아 음료를 만들고 있을 때 다른 직원이 주문을 받을 수 있다.
직원 1, 2, 3이 있고 손님 1, 2, 3이 있다.
손님 1이 아메리카노를 주문하고, 손님 2는 카페라떼를 주문한다.
직원 1, 2는 음료를 만든다.
직원 3은 늦게 들어온 손님 3의 주문을 받는다. 손님 3은 아이스티를 주문한다.
손님 3의 음료가 먼저 만들어져 나간다.
손님 2의 음료가 나간다.
손님 1의 음료가 나간다.
이처럼 요청이 종료되지 않아도 다음 요청을 바로 실행하는 방식을 비동기 처리 라고 한다. 비동기 처리에선 블로킹이 발생하지 않지만, 실행의 순서가 장담되지 않는다는 단점이 있다. 이벤트 헨들러, 서버에 자원요청 응답, 타이머 등은 비동기 처리의 주요 사례이다.
콜백은 비동기를 위한 대표적인 방법 중 하나이다. 비동기로 순서를 제어하고 싶을 수도 있다. 즉, 블로킹의 단점 없이 동기의 장점을 누리고 싶을 때는 어떻게 해야 할까?
const printString = (string) => {
setTimeout( ()=> {
console.log(string)},
Math.floor(Math.random()*100)+1)}
const printAll = () => {
console.log(printString('a'));
console.log(printString('b'));
console.log(printString('c'));
}
이 경우 console에 찍히는 abc의 순서는 실행마다 랜덤하다. a, b, c를 순서대로 출력하고 싶은 경우, 다시 말해, 순서를 제어하고 싶은 경우에 사용하는 방법이 바로 callback이다.
const printString = (string, callback) => {
setTimeout(()=> {
console.log(string)
callback()},
Math.floor(Math.random()*100)+1);
}
const printAll = () => {
printString('a', () => {
printString('b', () => {
printString('c', () => {})
})
})
};
printAll();
함수는 전과 비슷하지만 다르다. callback 을 하나 더 인자로 받는다. 이때, callback은 다음 순서로 실행하고 싶은 함수가 온다. 이 경우에는 printSpring이라는 함수를 다시한번 callback으로 사용했다.
printAll을 실행하면 먼저 printString이 한번 실행되고 'a'가 출력된다. 중요한건 그 다음으로 callback 인자로 전달받은 printString이 실행된다는 것이다. 이 때는 'b'를 출력하고, 다시한번 두번째 printString의 callback 인자로 printString이 온다. 이 세번째 printString에서는 'c'가 출력이 되고, callback의 인자로 받은 함수는 비어있으므로 종료된다. 출력되는데 걸리는 시간은 랜덤하겠지만, 어쨋건 a,b,c가 순서대로 출력된다.
이처럼 callback은 비동기를 위한 하나의 방법이다.
그러나 위의 코드는 극히 짧은 함수만을 다루었다. 만약 a, b, c 말고도 d, e, f를 쓰고싶다면 코드는 더욱 길고 복잡해진다.
const printString = (string, callback) => {
setTimeout(()=> {
console.log(string)
callback()},
Math.floor(Math.random()*100)+1);
}
const printAll = () => {
printString('a', () => {
printString('b', () => {
printString('c', () => {
printString('d', ()=> {
printString('e', () => {
printString ('f', () => {
})
})
})
})
})
})
};
단 3개만 더 추가했는데도 가독성이 떨어진다. 가독성이 떨어진다는 뜻은 곧 유지보수에 어렵다는 말이고, 이는 개발자가 피해야하는 것이다. 이렇게 callback함수로 인해 코드가 복잡해지고 가독성이 떨어지는 현상을 callback hell이라고 부른다.
이러한 callback 의 문제점을 피하기 위해 ES6에서 도입된 것이 promise이다.
promise 객체는 Promise생성 함수에 new 키워드를 상용해서 만들 수 있다.
const test = new Promise ((resolve, reject) => {
if (/*비동기 처리가 되었을 경우*/) {
resolve('result');}
else { reject('failure reaseon');
}
});
이 때 Promise의 콜백 함수를 executor함수(실행자 함수)라고 하며, new Promise가 만들어 질 때 자동으로 실행된다. 실행자 함수는 resolve와 reject를 인자로 받는다. 이 둘은 자바스크립트에서 제공하는 함수로 신경 쓸 필요없이 실행자 함수를 작성한다. 단, 최소한 resolve와 reject 둘 중 하나는 호출되어야 한다. 성공하면 resolve를 그 결과인 value와 함께 resolve(value), 실패하면 reject를 에러 객체인 reject(error)로 호출한다.
promise 객체는 state와 result라는 내부 프로퍼티를 가진다.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("완료"), 10000);
});
위의 예시에서 실행자 함수는 new Promise를 만들때 바로 실행된다는 점을 다시 확인할 수 있다. promise 상태는 처음에 다음과 같다.
10초 후 콜백함수가 실행되어 resolve("완료")가 실행되면 promise의 상태는 다음과 같이 바뀐다.
다음은 위와는 반대로 reject를 호출하는 예시이다.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("에러 발생!")), 10000);
});
이 때 10호 후의 promise의 상태는 다음과 같다.
then, catch finally는 promise에서 가장 중요하고 기초가 되는 메서드이다. 위의 메서드는 promise 객체의 result와 다른 함수를 연결하는 역할을 한다.
.then은 성공의 결과와 실패의 에러를 둘 다 다룰 수 있다.
promise객체.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(error)를 다룹니다 */ }
);
then의 첫 번째 인수는 실행자함수가 실행되었을 때의 결과를 다루고 두번째 인수는 거부되었을 때를 다룬다.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("완료!"), 1000);
});
promise.then(
result => console.log(result), //
에러가 발생했을 때의 경우를 다룬다. 이는 .then(null, function(error))과 완전히 같다.
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
promise.catch(alert); //
함수의 실행여부에 상관 없이 항상 실행된다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("결과"), 2000)
})
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)); //
위와 같이 finally는 인수가 필요 없고(실행 여부와 상관이 없으니까) finally 뒤에서 result를 바로 다룰 수 있다. 즉 result는 finally를 통과해 전달된다.
그러나 프로미스를 잘못 사용하면 잘못 사용하면 promise hell이 발생할 수 있다. 이를 방지하기 위해 return으로 정리한다.
프로미스 객체를 조금 더 쉽게 사용하기 위해 async와 await가 도입되었다.
async는 함수 앞에 위치한다. async가 함수 앞에 위치하게 되면 해당 함수는 항상 프로미스 객체를 리턴한다.
async function f() {
return 1;
}
f().then(alert); //1
await는 반드시 async 안에서만 작동한다. 문법은 다음과 같다.
let value = await promise
자바스크립트는 await를 만나면 프로미스가 종료될 때 까지 기다린다. await은 프로미스에서 result를 추출한다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
console.log(result); // "완료!"
console.log('안녕핫에ㅛ')
}
f();
async와 await는 promise then 보다 가독성이 좋다.
프라미스가 거부되면 catch로 에러를 핸들링할 수 있다.
async function f() {
let response = await fetch('http://유효하지-않은-주소');
}
// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)
또한 try catch로 다룰 수도 있다.
async function f() {
try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();