위 개념들을 정리하기 전에 먼저 비동기와 동기에 대해 한번 짚고 넘어가자.
동기 vs 비동기
동기(Synchronous)
‘동시에 일어난다’라는 사전적 의미를 갖고 있다.
그러나 프로그래밍에서는 동기는 작업이 순차적으로 진행되는 것을 의미한다.
즉, 한 작업이 완료될 때까지 다른 작업이 기다리고 있는 것을 의미한다.
결국 동기적으로 작동하는 프로그램은 성능이 떨어질 가능성이 크다.
(한 작업씩만 처리되기 때문에…)
비동기(Asynchronous)
프로그래밍에서는 비동기는 작업이 독립적으로 실행되고 작업의 완료 여부를 기다리지 않고 다른 작업을 실행할 수 있는 방식을 의미한다.
비동기 방식은 주로 I/O 작업이나 네트워크 요청같이 시간이 오래 걸리는 작업에 유용하다.
작업의 완료여부를 기다리지 않고 다른 작업을 실행할 수 있기 때문에 성능상으로 유리하다.
비동기 방식에는 콜백, 프라미스, async/await 등의 메커니즘이 있다.
비동기의 여러 방식
1. Promise
•
Promise를 표현한 그림
Promise의 세 가지 상태
◦
대기 (Pending) : 비동기 함수가 아직 시작하지 않은 상태
◦
성공 (Fulfilled) : 비동기 함수가 성공적으로 완료된 상태
◦
실패 (Rejected) : 비동기 함수가 실패한 상태
Promise는 비동기 작업의 단위이다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업
});
// Promise의 생성자는 화살표 함수를 인자로 받는다.
// (resolve, reject)를 묶어서 executor라고 부른다.
// executor 내의 resolve 함수를 호출하면 비동기 작업이 성공했다는 뜻
// executor 내의 reject 함수를 호출하면 비동기 작업이 실패했다는 뜻
TypeScript
복사
callback에 비해 간단하다는 Promise 코드인데 생성자 내의 함수, 그리고 그 함수내의 또 함수…
이거 간단한거 맞아…?
new Promise(…) 하는 순간 Promise에 할당된 비동기 작업이 바로 시작된다.
const promise1 = new Promise((resolve, reject) => {
resolve();
});
promise1
.then(() => {
console.log("then!");
})
.catch(() => {
console.log("catch!");
});
// output
// then!
const promise2 = new Promise((resolve, reject) => {
reject();
});
promise1
.then(() => {
console.log("then!");
})
.catch(() => {
console.log("catch!");
});
// output
// catch!
// promise는 비동기 작업을 처리하는 객체
// resolver는 정상적인 결과 값을 반환 (이행)
// reject는 정상적이지 않았던 값을 반환 (거부)
// 메서드
// then() : 이행되었을 때
// catch() : 거부되었을 때
// finally() : 이행되거나 거부되었을 때 항상
TypeScript
복사
비동기로 실행하기 때문에 성공/실패를 알 수 없기 때문에 then과 catch로 후속 동작을 지정한다.
•
SetTimeout과 비교
setTimeout(callback, delay);
위는 delay만큼 기다리는걸 완료한다면, 성공했다는 것이고 callback 함수를 실행한다.
setTimeout(callback, delay);
// Promise로 표현해본다면
function setTimeoutPromise(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay);
});
}
TypeScript
복사
•
Promise chaining
fetch('https://jsonplaceholder.typicode.com2222/todos?_limit=5')
.then(response => {
return response.json()
})
.then(data => {
console.log('data: ', data)
return data.filter(obj => obj.id > 3)
})
// 'https://jsonplaceholder.typicode.com2222/todos?_limit=5' 에서 요청한 데이터를
// then을 통해 json형태로 반환하고
// 그 밑 then에서 id가 3이상인 데이터를 필터한다.
// 이렇게 연속적으로 then 처리를 하는것을 promise chaining이라고 한다.
// 여러 then을 통해 순차적으로 로직을 수행한다.
// 이 Promise chaining도 결국 callback 지옥처럼 길어지면 가독성이 떨어진다.
TypeScript
복사
2. async / await
await 은 async 가 붙은 함수 내에서만 사용할 수 있다.
await 은 피연산자의 값을 반환한다. Promise객체라면 then 메서드를 호출하여 얻은 값을 반환해준다.
async의 리턴값은 항상 Promise 객체이다!
async function example() {
return 42;
}
const result = example();
console.log(result); // Promise { <resolved>: 42 }
// await 없이 반환하면 async 반환값인 Promise 객체가 반환된다.
--------------------------------------------------------------------
async function example() {
return 42;
}
async function main() {
const result = await example();
console.log(result); // 42
}
main();
// await으로 반환값을 받았기 때문에 42가 출력된다.
TypeScript
복사
•
await 의 역할
◦
가장 중요한 것은 Promise 가 resolve되던 reject 되던 끝날 때까지 기다린다.
◦
async 함수 내에서만 사용할 수 있다.
◦
await 은 결국 비동기 작업을 동기작업처럼 수행하겠다는 의미이다.
▪
await 걸지 않겠다는 의미는 결국 비동기 작업 이후의 작업을 정의하지 않겠다는 의미와 일맥상통하다.
•
for 구문 속 await
function setTimeoutPromise(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms);
});
}
// 사원의 데이터를 받아와 나이를 리턴하는 함수
// 1초가 걸린다고 가정해보자
async function fetchAge(id) {
await setTimeoutPromise(1000);
console.log(`${id} 사원 데이터 받아오기 완료!`);
return parseInt(Math.random() * 20, 10) + 25;
}
async function startAsyncJobs() {
let ages = [];
for (let i = 0; i < 10; i++) {
let age = await fetchAge(i);
ages.push(age);
}
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`,
);
}
// 사원의 정보를 받아오는 작업은 비동기이지만
// 끝나고 평균나이를 받아오는 작업은
// 사원의 나이를 전부 다 받아오고나서 동기로 처리되어야 하는 작업이다.
// 10명의 사원의 나이를 받아오는데 await를 사용하여 1초 * 10 = 10초를 기다려야 하네?
// Promise.all 을 사용하여 1초만에 반환하도록 성공
// 도중에 하나만 실패하더라도 위에 코듳터럼 예외를 발생시킨다.
async function startAsyncJobs() {
const ids = Array.from({ length: 10 }).map((_, index) => index);
const promises = ids.map(fetchAge);
const ages = await Promise.all(promises);
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`,
);
}
JavaScript
복사
•
결론
1.
기다리기만 하면 되는 작업은 비동기로 처리할 수 있다. 하지만 흐름 제어는 동기 코드보다 어렵다.
2.
Promise를 생성할 때 resolve, reject 함수를 적절히 호출하는 작업을 넣어주고, 이후 Promise 에 대해 then, catch를 통해 후속 조치를 할 수 있다.
3.
new Promise(…) 구문은 async 함수로 적절하게 변환할 수 있다.
4.
되도록이면 then, catch 구문을 사용하던지 async/await를 사용하던지 일관되게 작성하자.
5.
여러 Promise를 기다리게 된다면 Promise.all을 사용하자
나올 수 있는 async/await
1. async/await 와 Promise 차이점
async/await 는 Promise의 syntactic sugar로 비동기 작업을 동기 코드처럼 작성하게 해주고 Promise와 동일한 결과 반환합니다.
장점 : 가독성이 좋아지고, 복잡한 .then() 체인 대신 try-catch로 에러 처리가 용이해진다.
단점 : await가 순차적으로 실행되므로 병렬 처리가 필요한 경우는 Promise.all과 같은 방법이 있다.
async function fetchMultipleData() {
const [data1, data2] = await Promise.all([
fetchData1(),
fetchData2()
]);
console.log(data1, data2);
}
TypeScript
복사
2. await의 성능 문제와 이를 해결하는 방법을 설명해 주세요.
•
성능 문제 : await는 Promise가 해결될 때가지 대기하기 때문에 서로 의존하지 않은 작업이 await로 순차 실행되면 비효율적이다.
•
해결 방법 : 독립적인 비동기 작업은 Promise.all()로 병렬 처리하거나 Promise.allSettled()를 사용하여 각 작업의 성공/실패 여부와 관계없이 병렬로 처리할 수 있습니다.
3. Promise .finally()는 어떤 역할을 하나요?
.finally() 는 Promise의 성공(.then()) 또는 실패 (.catch()) 여부에 상관없이 항상 실행된다.
리소스 해제, 로딩 종료 등 후처리에 사용된다.