[DAY18] 끝없는 비동기

Jhey·2024년 11월 6일
0

JavsScript

목록 보기
14/18

0. Promise then()

0.1 먼저 실행되는 then

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이 먼저 실행돼야 하지 않나?

✅ Promise는 Task Queue가 아닌 Microtask Queue에 쌓이기 때문이다.



0.2 Microtask Queue

  • 비동기 작업을 처리하기 위한 내부 큐인 Microtask Queue는 먼저 들어온 작업을 먼저 실행(FIFO)하며, 특정 Promise가 준비되었을 때 이 Promise의 .then/catch/finally가 Microtask Queue에 들어간다.
  • 실행할 것이 아무것도 남아있지 않을 때만 Microtask Queue에 있는 작업이 실행되기 시작한다.
  • Task Queue보다 Microtask Queue를 우선적으로 처리한다.

0.3 작동 순서

아래와 같은 코드의 작동 순서는 이렇다.

setTimeout(() => {}, 0);

const promise = new Promise((resolve) => { resolve(); });

promise
	.then()
	.catch();

  1. 전역 컨텍스트가 콜스택에 쌓인다.

  2. setTimeout 함수를 웹 API에 맡긴다.

  3. setTimeout은 0초이기 때문에 바로 Task Queue에 쌓인다.

  4. promise 객체가 콜스택에 쌓인다.

  5. then()이 resolve 반환값을 받고 실행 부분이 Microtask Queue에 쌓인다. promise 객체가 콜스택에서 제거된다.

  6. 전역 컨텍스트가 실행되고 제거된다.

  7. 콜스택이 비었을 때 비로소 Microtask Queue에 있던 then 콜백이 콜스택에 쌓인다. 실행 후 제거된다.

  8. Microtask Queue가 비워진 후 Task Queue에 있던 콜백이 콜스택에 쌓여 실행되고 제거된다.


Promise 바로 실행시키기

setTimeout(() => { b; });

// 바로 실행되는 함수
// 바로 Microtask Queue에 들어가서 바로 실행됨
Promise.resolve().then(() => console.log(a));
// a -> b 출력

1. async/await

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();
};

1.1 await 실행 순서

const getSunIcon = () => {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve("확인");
		}, 1000);
	});
};

const getWeatherIcon = async () => {
	const sun = await getSunIcon();
	console.log(sun);
};

getWeatherIcon();
console.log("end");

1.2 작동 순서

아래와 같은 코드의 작동 순서는 이렇다.

  1. getWeatherIcon콜스택에 쌓인다.

  2. await인지하지 않은 채getWeatherIcon 안에 있는 getSunIcon을 호출한다. 또한 쌓인다.

  3. getSunIcon에서 Promise를 반환하기 때문에 executor를 실행하며 쌓인다.

  4. executor는 setTimeout을 호출하므로 웹 브라우저에게 위임하고 제거된다.

  5. getSunIconPromise 인스턴스 객체를 반환하고 제거된다.

  6. await는 반환된 결과값을 기다린다. getSunIcon이 제거되고 나서야 await을 인지한다.

  7. 그 순간 ROCK이 걸리고 getWeatherIconMicrotask Queue에 들어간다.

    • await 키워드를 인지한 순간 Microtask Queue로 함수 자체를 보낸다.
    • getWeatherIcon콜스택에서 빠져나온다.
  8. 전역 컨텍스트는 자신 위에 아무것도 없다고 생각하고 실행된 후 제거된다.

  9. 콜스택이 비워진다.

  10. Task Queue에 있는 setTimeout 콜백 함수가 먼저 콜스택에 들어간다.

    • getWeatherIconROCK이 걸려서 Task Queue 관련 콜백이 먼저 실행된다. 원래는 Microtask Queue가 먼저 실행되어야 한다.
  11. ROCK이 풀리고 Microtask Queue에 있던 getWeatherIcon콜스택에 들어간다.


🚨 개인적으로 이해가 안 갔던 ROCK이 걸린 후 함수가 Microtask Queue로 이동하는 과정

  1. myFunc() 안에 one()이 콜스택에 쌓인다.

  2. one()의 Promise가 쌓인 후 실행되어 제거된다.

  3. one()이 제거되고 반환값을 받은 후 await임을 인지하고 ROCK에 걸린다.

  4. ROCK에 걸린 후 myFunc이 Microtask Queue에 쌓인다.

  5. 콜스택이 비워진 후 Microtask Queue에서 myFunc이 콜스택으로 쌓이고 이때 아까 실행하지 못한 console.log(res)가 실행된다.

ROCK 상태

  • ROCK 상태는 엄밀히 말하면 Microtask Queue에 걸쳐 있는 상태로, ROCK이 풀릴 때까지 나갈 수 없다.

  • await이 끝나지 않으면 Microtask Queue에 있는 다른 작업들이 대기하게 되고, 그 작업들이 영원히 실행되지 않을 수 있다. await은 비동기 작업의 완료를 기다리는 방식이기 때문에 이런 현상이 발생할 수 있다.

2. 동기를 비동기처럼 사용하기

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();

3. 비동기를 동기로 사용 시 단점 보완

결국 하나의 함수를 기다리고 결과가 나온 후에야 실행되기 때문에 시간이 오래 걸릴 것이다.

보완 방법

  1. 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();
};
  1. Promise.all()을 사용해 한 번에 객체를 받은 후 await을 붙인다.
const getWeatherIcon = async () => {
	const results = await Promise.all([getWaveIcon(), getCloudIcon(), getSunIcon()]);
	console.log(results);
};
  1. Promise.all()을 사용해 한 번에 객체를 받은 후 await로 처리합니다.
const a = await Promise.all([getWaveIcon(), getCloudIcon(), getSunIcon()]);
console.log(a.join(" "));

➡️ 원래는 각각의 비동기 작업이 끝날 때까지 기다린 후 다음 작업이 실행되지만, 이 방법을 사용하면 모든 작업을 동시에 실행시켜 실행 시간을 단축할 수 있다. 그런 다음 await로 모든 반환값을 받아 한 번에 처리하면 효율이 좋아진다.


3. API

HTTP Methods:

  • GET: 데이터를 가져올 때 (기본)
  • POST: 데이터를 추가할 때
  • PUT: 데이터를 업데이트할 때
  • PATCH: 데이터를 부분적으로 업데이트할 때
  • DELETE: 데이터를 삭제할 때
  • OPTIONS: 서버에서 허용하는 HTTP 메서드를 확인할 때

GET 요청 예제

MethodURL설명
GET/posts전체 게시글 100개를 보여줌
GET/posts/1ID가 1번인 게시글을 보여줌
GET/posts/1/commentsID가 1번인 게시글의 댓글을 보여줌
GET/comments?postId=1ID가 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);
	}
};
  • fetchPromise를 반환합니다.

  • fetchresponse 객체를 반환하며, 이를 .json() 메서드를 사용해 JSON 데이터로 변환할 때도 Promise가 반환됩니다.


catch가 걸리는 경우와 걸리지 않는 경우

  • 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를 다루려면 필수로 알아야 하는 지식이니 빈틈없이 공부하자!

출처: await 그림, 그 외 모든 그림

profile
천천히 가더라도 즐겁게 ☁

0개의 댓글

관련 채용 정보