1. 비동기 프로그래밍
- 비동기 프로그래밍은 시간 소모가 큰 작업(예: 네트워크 요청, 타이머 대기)을 효율적으로 처리하기 위한 방법
- 비동기 작업은 JavaScript 엔진이 아닌 이벤트 루프를 통해 관리되며, 프로그램 실행이 멈추지 않고 계속 진행될 수 있게 함
(이벤트 루프에 대한 글 참고: https://yiyj1030.tistory.com/592)
2. 콜백 헬(hell)
비동기 처리 후에 후속 처리에 대한 작업을 어떻게 해줘야할까?
function fetchData() {
let data;
setTimeout(() => {
data = "Hello, World!";
}, 1000);
return data;
}
const result = fetchData();
console.log(result); // undefined
위에 처럼 해봤자 data를 얻어낼 수 없음. result는 undefined로 나옴.
이를 해결하기 위해선 비동기 함수에게 콜백 함수를 넘겨주는 방식이 필요.
function fetchData(callback) {
let data;
setTimeout(() => {
data = "Hello, World!";
callback(data);
}, 1000);
return data;
}
fetchData((result) => {
console.log(result) // Hello, World!
});
하지만 콜백을 넘겨주는 방식은 자칫하단 콜백 지옥을 만들어낼 수 있음
function fetchData(callback) {
setTimeout(() => {
const data = "Hello, World!";
callback(data); // 데이터를 콜백 함수로 전달
}, 1000);
}
// 콜백 중첩
fetchData((result1) => {
console.log("First:", result1);
fetchData((result2) => {
console.log("Second:", result2);
fetchData((result3) => {
console.log("Third:", result3);
});
});
});
api를 사용하는 상황에서도 아래와 같은 콜백 헬을 만날 수 있다.
get(`${url}/posts/1`, (userId) => {
get(`${url}/users/${userId}`, (userInfo) => {
get(`${url}/grades/${userInfo.grade}/benefits`, (benefits) => {
console.log(`User ${userId} Benefits: ${benefits}`);
});
});
});
3. Promise와 then, catch, finally
- 콜백 헬 문제를 해결하기위해 ES6에서 도입됨.
- Promise는 비동기 작업의 상태를 관리하는 객체
- 작업 진행중: Pending, 작업 성공: Fulfilled, 작업 실패: Rejected
Prmoise 동작 원리
- Promise 객체 생성 시 콜백함수를 인자로 전달해준다.
- 해당 콜백함수는 또 2개의 인자를 갖고 있는데 각각 성공 시 호출 될 함수(resolve)와 실패 시 호출할 함수(rejected)이다.
- 비동기 처리를 하는 코드는 이 콜백함수의 내부에 존재하며 첫번째 인자로 전달된 함수(resolve)가 실행되면 Promise의 상태를 fulfilled로 바꾸고, 두번째로 전달된 함수(reject)가 실행되면 Promise 상태를 rejected로 바꾼다. 그리고 각 resolve, reject 함수의 인자로 받은 결과(아래 예시에서 Json 파싱 결과 또는 에러)를 처리 결과로 저장한다.
const get = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 첫번째 인자이므로 Promise 상태를 resolved로 변경함
// 처리 결과로 JSON.parse(xhr.responseText)가 저장됨
resolve(JSON.parse(xhr.responseText));
} else {
// 두번째 인자이므로 Promise 상태를 rejected로 변경함
// 처리 결과로 Error 객체가 저장됨
new Error(xhr.status);
}
}
});
};
Then, Catch, Finally 사용법
- then: 상태가 fulfilled가 되었을 때 인자로 전달받은 콜백함수를 실행(Promise의 처리 결과를 콜백함수 인자(data)에 자동으로 주입)
- catch: 상태가 rejected가 되었을 때 실행할 콜백함수를 넘겨줌(Promise의 처리 결과를 콜백함수 인자(error)에 자동으로 주입)
- finally: fulfilled/rejected에 상관없이 1번 실행됨
get("https://jsonplaceholder.typicode.com/posts/1")
.then((data) => console.log("Success:", data))
.catch((error) => console.error("Error:", error))
.finally(() => console.log("Operation complete"));
참고로 순서가 중요한데, 아래를 보자.
const promise = new Promise((reject, resolve) => {
resolve("Hello")
});
promise
.then((result) => console.log(`resolve: ${result}`))
.catch((result) => console.log(`error: ${result}`))
//결과는 error: Hello가 나옴.
//왜냐면 resolve 함수가 실행되었지만 이 함수는 어쨌든 두번째 인자로 전달되었던 함수임
//Promise는 두번째 인자로 전달받은 함수가 실행되면 상태를 'rejected'로 바꿈
4. 마이크로태스크큐(Microtask Queue)
이벤트 루프의 태스크큐와 같은 역할을 하지만 특별히 then과 같은 프로미스 후속처리를 위한 별도의 큐다.
일반 태스크큐보다 우선순위가 높음.
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise callback");
});
console.log("End");
결과: Promise call이 먼저 호출됨.
Start
End
Promise callback
Timeout callback
5. async와 await
Promise 기반의 비동기 코드를 더 직관적이고 동기적인 방식으로 작성할 수 있도록 함.
기본 사용법
- 함수에 async 키워드를 추가해주고 비동기 처리 함수에 await 키워드를 추가
- await 키워드가 붙으면 비동기함수가 처리를 완료할때까지 코드가 대기(그동안 이벤트루프에 의해 이 async 함수 밖의 다른 작업이 실행됨)
- 처리가 완료되면 결과가 반환되고 나머지 코드를 이어서 실행
async function fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await response.json();
console.log(data.title);
}
fetchData();
에러 처리 예시 - try, catch 사용
프로미스 상태(fulfilled, rejected)를 await가 기다리고 있다가
fullfilled면 처리 결과를 반환하고 rejected면 에러를 발생시키고 에러를 catch 블록으로 전달.
(참고로 구현방식은 제너레이터를 쓰지만 너무 딥해서 생략)
async function fetchData() {
try {
const response = await fetch("https://invalid-url");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error occurred:", error);
} finally {
console.log("Operation complete");
}
}
fetchData();
실행 순서
async function example() {
console.log("Before await");
const result = await new Promise((resolve) => {
setTimeout(() => resolve("Async result"), 2000); // 2초 후에 resolve 호출
});
console.log("After await:", result);
}
example();
console.log("Outside async function");
출력 결과
Before await
Outside async function
After await: Async result
1. example 함수 호출:
console.log("Before await") 실행 → 출력: "Before await".
2. await로 비동기 작업 대기:
Promise가 2초 후에 resolve되므로, 아래 코드는 대기 상태.
JavaScript는 이벤트 루프를 통해 다른 작업을 계속 처리.
3. console.log("Outside async function") 실행:
await는 이벤트 루프를 통해 대기 상태로 넘어가기 때문에, example 함수의 실행은 잠시 중단.
"Outside async function" 출력.
4. 2초 후:
Promise가 resolve되며, await 대기 상태가 해제.
console.log("After await:", result) 실행 → 출력: "After await: Async result".
댓글