목차
NodeJS 로 프로젝트를 개발할 때
javascript
export const function = async(parameter) => { ... }
위 코드처럼 async 를 습관적으로 작성했었다. 사실 나는 해당 키워드가 비동기 처리를 해준다 라는 것만 알고, 자세히 들여다보진 않았었다. 하지만, NodeJS에 대해서 정리를 하다보니까 습관처럼 사용하던 async라는 키워드에 대해서 궁금증이 생겼고, 동기/비동기 에 대해서 알아보았다.
동기 (Synchronous) vs 비동기 (Asynchronous)
동기 (Synchronous)
- 한 줄이 끝나야 다음 줄이 실행된다
- CPU 계산처럼 바로 끝나는 작업에 적합하다.
예시 )
const a = 1 + 1; // 바로 계산됨
const b = a * 2; // a가 끝나야 실행
console.log(b);
비동기 (Asynchronous)
- 오래 걸리는 작업을 기다리지 않고 다음 코드로 넘어간다.
- I/O 작업 (네트워크 요청, 파일 읽기, DB) 등에 적합하다.
- 완료 시점에 callback / Promise 를 통해 결과를 알려준다.
예시 )
function getNumber() {
return Promise.resolve(2); // 바로 2를 반환하는 비동기 함수
}
async function run() {
const a = await getNumber(); // a 값이 준비될 때까지 기다림
const b = a * 2; // a가 끝난 뒤 실행
console.log(b); // 4
}
run();
비동기를 습관처럼 사용하는 이유가 뭘까 ?
일단 첫 번째 장점은 성능 향상이다.
비동기를 사용하면 오래 걸리는 작업 때문에 프로그램 전체가 멈추는 현상을 줄일 수 있다.
예를 들어, 내가 카페에 갔는데 앞 손님이 아아 100잔을 테이크아웃 주문했다고 해보자.
그렇다고 해서 내가 앞 손님이 100잔을 다 받을 때까지 기다린다면 너무 비효율적일 것이다.
보통 이런 상황에서는 카페가 아아 100잔을 만드는 동안에도 다른 손님들의 주문을 함께 받는다.
이처럼 여러 작업을 동시에 처리하는 방식을 비동기 처리라고 한다.
또 다른 장점은 병렬적 작업 처리다.
다시 카페 상황을 생각해보면, 아아 100잔과 뒤에 들어오는 주문을 동시에 처리하려고 해도
직원이 한 명뿐이라면 감당하기 어렵다.
그래서 여러 알바생을 고용해 각자 작업을 분담한다면 훨씬 빠르게 주문을 처리할 수 있다.
이처럼 여러 개의 작업을 동시에 나눠서 처리하는 과정을 병렬 처리라고 한다.
콜백 (Callback) & 프로미스 (Promise)
비동기 처리가 끝나면 결과를 알려주는 방법에는 두 가지가 있다 : Callback & Promise
이 두가지 방법에 대해서 알아보도록 하자.
콜백 (Callback)
: 비동기 함수에 작업이 끝나면 실행할 함수를 전달하는 방식이다.
function getData(callback) {
setTimeout(() => {
const data = "서버에서 온 데이터";
callback(data); // 완료되면 callback 실행
}, 2000);
}
getData((result) => {
console.log("결과:", result);
});
// 실행 흐름: (즉시) → 대기 → 2초 뒤 "결과: 서버에서 온 데이터"
사용법이 굉장히 간단하다는 장점이 있지만, 콜백 안에서 또 다른 콜백을 사용하면 콜백 지옥 (Callback Hell) 문제가 발생할 수 있다.
프로미스 (Promise)
콜백 방법 보다는 조금 더 구조화된 방법으로, 비동기 작업이 끝나면 resolve (성공) 또는 reject (실패)를 호출해서 결과를 알려준다.
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = "서버에서 온 데이터";
resolve(data); // 성공 시점
}, 2000);
});
}
getData()
.then((result) => {
console.log("결과:", result);
})
.catch((error) => {
console.error("에러:", error);
});
resolve: Promise 안에서 성공적으로 작업이 끝났음을 알리는 함수reject: Promise 안에서 작업이 실패했음을 알리는 함수then: Promise 안에서 비동기 작업이 끝난 후 (성공을 했을 때), 실행될 동작을 등록하는 함수
async / await
이 방법이 가장 많이 사용하는 방법이다. 나도 이 방법을 거의 공식처럼 사용하기도 했다. 이 방법은 Promise의 진화형으로 생각하면 좋을 것 같다. 위 Promise 코드를 조금 더 동기 코드처럼 볼 수 있도록 해주는 문법이다.
async function main() {
try {
const result = await getData(); // getData는 Promise 반환
console.log("결과:", result);
} catch (error) {
console.error("에러:", error);
}
}
main();
위 코드를 보면, 이 전 Promise의 코드보다 훨씬 더 읽기 쉽다.
그러면 병렬처리는 어떻게 처리할 수 있을까 ?
// 여러 사용자를 병렬(동시에)로 불러오는 함수
async function getMultipleUsersParallel(ids) {
const promises = ids.map((id) => fetchUser(id));
const results = await Promise.all(promises);
return results;
}
이 코드는 비동기 요청을 병렬처리로 처리하는 코드이다.
차근 차근 코드를 설명해보자면,
ids 라는 배열이 있을 때, map을 돌면서 각 id 마다 `fetchUser(id)` 라는 함수를 실행한다.
이때, `fetchUser(id)`는 비동기 함수이기 때문에 Promise 객체를 반환한다.
이후 `Promise.all([Promise 배열])`라는 함수를 이용하여 `promises` 배열에 있는 promise 객체들을 동시에 실행시킨다.
이들이 모두 완료될 때까지 대기 했다가 완료가 되면 모든 결과를 배열로 반환한다.
비동기 처리는 Node.js에서 정말 중요한 개념이다. callback → Promise → async/await 순으로 발전해오면서 코드의 가독성과 유지보수성이 크게 좋아졌다. 특히 async/await 은 겉으로 보기엔 동기 코드처럼 읽히지만, 실제로는 비동기 처리를 해주기 때문에 내가 무심코 습관처럼 써왔던 이유를 이제야 명확히 이해할 수 있었다.