console.log('Start!');
setTimeout(() => {
console.log('Timeout!');
}, 0);
Promise.resolve('Promise!').then(res => console.log(res));
console.log('End!');
➡️ 위 코드를 실행하면 Start => End => Promise => Timeout 순으로 출력된다.
❓ 왜 같은 Promise는 setTimeout보다 먼저 실행될까? Task Queue에 쌓이는 순서는 분명 setTimeout, Promise 순서일 텐데, 먼저 들어온 setTimeout이 먼저 실행돼야 하지 않나?
- 비동기 작업을 처리하기 위한 내부 큐인 Microtask Queue는 먼저 들어온 작업을 먼저 실행(FIFO)하며, 특정 Promise가 준비되었을 때 이 Promise의
.then/catch/finally
가 Microtask Queue에 들어간다.- 실행할 것이 아무것도 남아있지 않을 때만 Microtask Queue에 있는 작업이 실행되기 시작한다.
- Task Queue보다 Microtask Queue를 우선적으로 처리한다.
아래와 같은 코드의 작동 순서는 이렇다.
setTimeout(() => {}, 0);
const promise = new Promise((resolve) => { resolve(); });
promise
.then()
.catch();
전역 컨텍스트가 콜스택
에 쌓인다.
setTimeout 함수를 웹 API
에 맡긴다.
setTimeout은 0초이기 때문에 바로 Task Queue
에 쌓인다.
promise 객체가 콜스택
에 쌓인다.
then()이 resolve 반환값을 받고 실행 부분이 Microtask Queue
에 쌓인다. promise 객체가 콜스택에서 제거된다.
전역 컨텍스트가 실행되고 제거된다.
콜스택
이 비었을 때 비로소 Microtask Queue
에 있던 then
콜백이 콜스택
에 쌓인다. 실행 후 제거된다.
Microtask Queue
가 비워진 후 Task Queue
에 있던 콜백이 콜스택
에 쌓여 실행되고 제거된다.
setTimeout(() => { b; });
// 바로 실행되는 함수
// 바로 Microtask Queue에 들어가서 바로 실행됨
Promise.resolve().then(() => console.log(a));
// a -> b 출력
Promise를 반환하기 위한 sugar syntax. async와 await를 사용하면 Promise를 좀 더 편하게 사용할 수 있다.
➡️ 함수 앞에 async
를 붙이면 Promise
를 반환한다.
return
이 없으면 내부적으로 undefined
를 반환한다.
async function fetchNumber() {
return 1; // Promise 반환
}
➡️ 화살표 함수로 async
를 작성할 수도 있다.
const fetchNumber = async () => {
return 1;
};
➡️ await
를 사용하면 함수 호출 비동기 처리 결과를 기다린다.
resolve 또는 완료된 반환값을 기다린다.
const icon = async () => {
const sun = await getIcon();
};
const getSunIcon = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("확인");
}, 1000);
});
};
const getWeatherIcon = async () => {
const sun = await getSunIcon();
console.log(sun);
};
getWeatherIcon();
console.log("end");
아래와 같은 코드의 작동 순서는 이렇다.
getWeatherIcon
이 콜스택
에 쌓인다.
await
를 인지하지 않은 채로 getWeatherIcon
안에 있는 getSunIcon
을 호출한다. 또한 쌓인다.
getSunIcon
에서 Promise
를 반환하기 때문에 executor
를 실행하며 쌓인다.
executor
는 setTimeout을 호출하므로 웹 브라우저에게 위임하고 제거된다.
getSunIcon
은 Promise
인스턴스 객체를 반환하고 제거된다.
await
는 반환된 결과값을 기다린다. getSunIcon
이 제거되고 나서야 await
을 인지한다.
그 순간 ROCK
이 걸리고 getWeatherIcon
은 Microtask Queue
에 들어간다.
await
키워드를 인지한 순간 Microtask Queue
로 함수 자체를 보낸다.getWeatherIcon
이 콜스택
에서 빠져나온다.전역 컨텍스트
는 자신 위에 아무것도 없다고 생각하고 실행된 후 제거된다.
콜스택
이 비워진다.
Task Queue
에 있는 setTimeout 콜백 함수가 먼저 콜스택
에 들어간다.
getWeatherIcon
은 ROCK
이 걸려서 Task Queue 관련 콜백이 먼저 실행된다. 원래는 Microtask Queue
가 먼저 실행되어야 한다.ROCK
이 풀리고 Microtask Queue
에 있던 getWeatherIcon
이 콜스택
에 들어간다.
myFunc()
안에 one()
이 콜스택에 쌓인다.
one()
의 Promise가 쌓인 후 실행되어 제거된다.
one()
이 제거되고 반환값을 받은 후 await
임을 인지하고 ROCK에 걸린다.
ROCK에 걸린 후 myFunc
이 Microtask Queue에 쌓인다.
콜스택이 비워진 후 Microtask Queue에서 myFunc
이 콜스택으로 쌓이고 이때 아까 실행하지 못한 console.log(res)
가 실행된다.
ROCK 상태는 엄밀히 말하면 Microtask Queue
에 걸쳐 있는 상태로, ROCK이 풀릴 때까지 나갈 수 없다.
await
이 끝나지 않으면 Microtask Queue
에 있는 다른 작업들이 대기하게 되고, 그 작업들이 영원히 실행되지 않을 수 있다. await
은 비동기 작업의 완료를 기다리는 방식이기 때문에 이런 현상이 발생할 수 있다.
Promise도 사용하다 보면 콜백처럼 "콜백 지옥"이 형성된다. 이럴 때 async/await를 사용하면 비동기를 동기처럼 작동시킬 수 있다.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getSunIcon = async () => {
await delay(1000);
return "";
};
const getWaveIcon = async () => {
await delay(1000);
return "";
};
const getCloudIcon = async () => {
await delay(1000);
return "";
};
// Promise then으로 풀어보기
// 여기서도 "콜백 지옥"이 발생함
const getWeatherIcon = () => {
getSunIcon()
.then((sun) => {
return getWaveIcon().then(() => {
return getCloudIcon().then(() => {});
});
});
};
// async/await으로 풀어보기
// await를 사용하면 함수를 기다려주므로 해결되면
// 그다음 줄로 넘어가기 때문에 비동기를 동기처럼 사용 가능
const getWeatherIcon = async () => {
const wave = await getWaveIcon();
const cloud = await getCloudIcon();
const sun = await getSunIcon();
};
getWeatherIcon();
결국 하나의 함수를 기다리고 결과가 나온 후에야 실행되기 때문에 시간이 오래 걸릴 것이다.
async
함수 실행을 먼저 해서 Promise
객체를 받은 후 await
을 붙인다.const getWeatherIcon = async () => {
const wave1 = getWaveIcon();
const cloud1 = getCloudIcon();
const sun1 = getSunIcon();
const wave = await getWaveIcon();
const cloud = await getCloudIcon();
const sun = await getSunIcon();
};
Promise.all()
을 사용해 한 번에 객체를 받은 후 await
을 붙인다.const getWeatherIcon = async () => {
const results = await Promise.all([getWaveIcon(), getCloudIcon(), getSunIcon()]);
console.log(results);
};
Promise.all()
을 사용해 한 번에 객체를 받은 후 await
로 처리합니다.const a = await Promise.all([getWaveIcon(), getCloudIcon(), getSunIcon()]);
console.log(a.join(" "));
➡️ 원래는 각각의 비동기 작업이 끝날 때까지 기다린 후 다음 작업이 실행되지만, 이 방법을 사용하면 모든 작업을 동시에 실행시켜 실행 시간을 단축할 수 있다. 그런 다음 await
로 모든 반환값을 받아 한 번에 처리하면 효율이 좋아진다.
HTTP Methods:
GET 요청 예제
Method | URL | 설명 |
---|---|---|
GET | /posts | 전체 게시글 100개를 보여줌 |
GET | /posts/1 | ID가 1번인 게시글을 보여줌 |
GET | /posts/1/comments | ID가 1번인 게시글의 댓글을 보여줌 |
GET | /comments?postId=1 | ID가 1번인 게시글의 댓글을 보여줌 (다른 방식) |
API 호출 방법
fetch API
: 브라우저 내장 API ✅
axios
: 외부 라이브러리로 설치 필요
fetch 예제
// Promise 방식
const getApi = () => {
const promise = fetch("~~", { method: "GET" }); // GET 요청일 경우 생략 가능
promise.then(() => console.log("성공"));
};
// async/await 방식
const getApi = async () => {
try {
const response = await fetch("~~");
if (!response.ok) throw new Error("오류입니다.");
const data = await response.json();
} catch (e) {
console.error(e);
}
};
fetch
는 Promise
를 반환합니다.
fetch
는 response
객체를 반환하며, 이를 .json()
메서드를 사용해 JSON 데이터로 변환할 때도 Promise
가 반환됩니다.
catch가 걸리는 경우
fetch
는 웹 API이기 때문에 웹 브라우저 또는 웹 엔진에서 발생하는 에러만 잡을 수 있습니다.catch
가 즉시 호출됩니다.catch가 걸리지 않는 경우
fetch
입장에서는 에러가 아닙니다. fetch
는 단순히 접근할 수 있으면 문제없다고 간주합니다.response.ok
를 사용해 확인합니다.if (!response.ok) throw new Error("오류입니다.");
response.ok; // 이 값으로 오류를 파악합니다.
if (!response.ok) throw new Error("오류입니다.");
return response.json();
콜백 함수부터
Promise
,async/await
의 탄생 배경을 알게 되어 흥미로웠다. 컨텍스트를 배우면서 자연스럽게 비동기까지 이어지니 자동 복습이...😇 결국 API를 다루려면 필수로 알아야 하는 지식이니 빈틈없이 공부하자!