Table of Contents
비동기 함수
- 함수의 실행 순서는 실행 컨텍스트 스택(콜 스택)으로 관리한다
- 자바스크립트 엔진은 하나의 실행 컨텍스트 스택만을 갖는다
- 이는 함수를 실행할 수 있는 창구가 단 하나이며, 동시에 2개 이상의 함수를 동시에 실행할 수 없다는 것을 의미한다
- 이처럼 자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작한다
- 싱글 스레드 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행할 경우 블로킹이 발생한다
- 이처럼 현재 실행 중인 태스크가 종료할 때까지 다음 실행될 태스크가 대기하는 방식을 동기(synchronous) 처리라고 한다
- 동기 처리 방식은 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료될 때까지 이후 태스크들이 블로킹되는 단점이 있다
- 반면에 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기(asynchronous) 처리라고 한다
- 비동기 처리를 수행하는 비동기 함수는 전통적으로 콜백 패턴을 사용한다. 하지만 콜백 패턴은 콜백 헬을 발생시켜 가독성을 나쁘게 하고, 비동기 처리 중 발생한 에러의 예외 처리가 곤란하며, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다
setTimeout
,setInterval
, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다- 자바스크립트는 이벤트 루프를 통해 동시성을 지원한다
- 비동기 함수의 콜백 함수 또는 이벤트 핸들러는 태스크 큐에 일시적으로 보관된다
- 이벤트 루프는 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기 중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해서 확인한다
- 만약 콜 스택이 비어있고, 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다
-
이때 콜 스택으로 이동한 함수는 실행된다. 즉, 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다
- 자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작한다
- 브라우저에 있는 태스크 큐와 이벤트 루프가 비동기 처리를 돕는 것이다
프로미스
- ES6에서 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다
- 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다
- 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다
- 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 후, 나중에 완료된다
- 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다
- 예를 들어, 비동기 함수인
setTimeout
함수의 콜백 함수는setTimeout
함수가 종료된 이후에 호출된다 - 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다
- 따라서 비동기 함수의 처리 결과(ex. 서버의 응답)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다
- 이때 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 것이 일반적이다
- 하지만 콜백 함수는 콜백 헬 문제와 에러 처리를 못한다는 문제가 있다
- 이를 극복하기 위해 ES6에서 프로미스가 도입되었다
프로미스의 생성
Promise
생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달 받는데 이 콜백 함수는resolve
와reject
함수를 인수로 전달받는다- 비동기 처리는
Promise
생성자 함수가 인수로 전달받는 콜백 함수 내부에서 수행한다. 만약 비동기 처리가 성공하면 비동기 처리 결과를resolve
함수에 인수로 전달하면서 호출하고, 실패하면 에러를reject
함수에 인수로 전달하면서 호출한다
프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
pending |
비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled |
비동기 처리가 수행된 상태 (성공) | resolve 함수 호출 |
rejected |
비동기 처리가 수행된 상태 (실패) | reject 함수 호출 |
- 생성된 직후의 프로미스는 기본적으로
pending
상태다 - 이후 비동기 처리가 수행되면 처리 결과에 따라 다음과 같이 프로미스의 상태가 변경된다
- 성공:
resolve
함수를 호출해 프로미스를fulfilled
상태로 변경한다 - 실패:
reject
함수를 호출해 프로미스를rejected
상태로 변경한다
- 성공:
- 이처럼 프로미스의 상태는
resolve
또는reject
함수를 호출하는 것으로 결정된다 - 프로미스는
pending
상태에서fulfilled
또는rejected
상태가 되면 더는 다른 상태로 변화할 수 없다 - 프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과도 갖는다
- 성공하면,
resolve()
함수의 첫 번째 인자로 넘겨준 값을 처리 결과 값으로 한다 - 실패하면,
reject()
함수의 첫 번째 인자로 넘겨준 에러 객체를 결과 값으로 한다 - 즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다
프로미스의 후속 처리 메서드
- 프로미스가
fulfilled
또는rejected
가 되면 처리 결과를 가지고 무언가를 해야한다 - 이를 위해 프로미스는 후속 메서드
then
,catch
,finally
를 제공한다 - 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다
- 모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다
Promise.prototype.then
then
메서드는 두 개의 콜백 함수를 인수로 전달받는다- 첫 번째 콜백 함수는 프로미스가
fulfilled
상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 비동기 처리 결과를 인수로 전달받는다 - 두 번째 콜백 함수는 프로미스가
rejected
상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다 then
메서드는 언제나 프로미스를 반환한다. 만약then
메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다
Promise.prototype.catch
catch
메서드는 한 개의 콜백 함수를 인수로 전달받는다catch
메서드의 콜백 함수는 프로미스가rejected
상태인 경우에만 호출된다catch
메서드는then(undefined, onRejected)
과 동일하게 동작한다- 마찬가지로 언제나 프로미스를 반환한다
Promise.prototype.finally
finally
메서드는 한 개의 콜백 함수를 인수로 전달받는다finally
메서드의 콜백 함수는 프로미스의 성공, 실패와 상관없이 무조건 한 번 호출된다- 프로미스의 상태와 상관없이 수행해야 할 처리 내용이 있을 때 유용하다
- 마찬가지로 언제나 프로미스를 반환한다
프로미스의 에러 처리
then
,catch
를 이용해 에러를 처리할 수 있다- 단,
then
메서드의 두 번째 콜백 함수가 첫 번째 콜백 함수에서 발생한 에러를 캐치하지는 않는다 then
이 단 한번만 나온다면 두 번째 콜백함수를 사용해 그 안에서 에러 처리를 해도 괜찮다- 하지만
then
이 여러 번에 걸쳐 체이닝 되는 경우에는 두 번째 콜백 함수를 쓰지 않고,catch
로 에러를 처리하는게 가독성이 좋다
fetch
fetch
함수는 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API다fetch
함수는 프로미스를 지원하기 때문에 비동기 처리가 훨씬 자유롭다fetch
함수는 HTTP 응답을 나타내는Response
객체를 래핑한Promise
객체를 반환한다fetch
함수는Response
객체를 래핑한 프로미스를 반환하므로,then
을 통해 프로미스가 resolve한Response
객체를 전달 받을 수 있다fetch
함수가 반환하는 프로미스는 404와 같은 HTTP 에러가 발생해도 에러를 reject 하지 않고 불리언 타입의ok
상태를false
로 설정한Response
객체를 resolve 한다- 오프라인 등의 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를
reject
한다 - 따라서 HTTP 에러는 프로미스가 resolve한 불리언 타입의
ok
상태를 확인해 명시적으로 처리해야 한다
async/await
- ES8 에서 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await이 도입되었다
- async/await은 프로미스를 기반으로 동작한다
- async/await을 사용하면 프로미스의 then/catch/finally과 같은 후속 처리 메서드를 사용하지 않고도 마치 동기 처리처럼 프로미스를 사용할 수 있다
async
async
는 함수 정의문 앞에 붙는 키워드로, 함수가 비동기적으로 실행되도록 한다. 즉 비동기 함수를 정의할 때 사용하는 키워드이다async
함수는 언제나 프로미스를 반환한다.async
함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다
await
await
키워드는 프로미스가 settled 상태(fulfilled
또는rejected
)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다await
키워드는 반드시 프로미스 앞에서 사용해야 한다await
키워드는 다음 실행을 일시 중지시켰다가 프로미스가 settled상태가 되면 다시 재개한다
에러 처리
- async/await 에서 에러 처리는
try...catch
문을 사용할 수 있다 - 콜백 함수를 인자로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다
const foo = async () => {
try {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data)
} catch (err) {
console.error(err); // TypeError: Failed to fetch
}
}
foo();
catch
문은 HTTP 통신에서 발생한 네트워크 에러뿐 아니라try
코드 블록 내의 모든 문에서 발생한 에러까지 모두 캐치할 수 있다async
함수 내에서catch
문을 사용해서 에러 처리를 하지 않으면async
함수는 발생한 에러를 reject 하는 프로미스를 반환한다- 따라서
async
함수를 호출하고Promise.prototype.catch
후속 처리 메서드를 사용해 에러를 캐치할 수도 있다
const foo = async () => {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
return data
}
foo()
.then(console.log)
.catch(console.error) // TypeError: Failed to fetch