개요
프로젝트 진행 도중에 콜백 지옥인 코드를 맛보게 되었다... 가독성이 너무 떨어져서 다음에 기능 수정할 때 눈이 너무 아플 것 같았다! 바로 콜백 지옥에서 벗어나기 위해 async/await과 Promise를 활용했고, 비동기 처리에 있어서 얘네들에 소중함을 절실히 느꼈다. 그리고 얘네들에 소중함을 널리 알리기 위해 정리해보려고 한다.
1. 비동기(Asynchronous)
비동기에 대한 설명들은 많이 보고 들었지만, 가장 이해가 빨랐던 설명은 '카페에서 음료 주문'이였던 것 같다.
일단 개념은
하나의 작업을 시작하고, 그 작업이 끝날 때까지 그 코드에서 머물지 않고(기다리지 않고) 바로 다음 작업을 처리하는 방식이다. 그 뒤 작업이 끝나면 그 결과를 나중에 알려준다.(Non-Blocking)
-> 이 내용을 예시로 설명하면
카페에서 커피 주문을 하고 '커피가 나오길 그 자리에서 기다리지 않고', 진동벨을 받아 자리에 와서 '친구와 떠드는 것'
그리고 진동벨이 울리면 커피를 받으러 간다.
이 예시가 진짜 나한테는 단번에 이해가 되는 내용이였다. 감사합니다
2. 콜백 함수(Callback Function)
콜백 함수는 다른 함수의 인자로 전달되어서 그 함수 안에 특정 시점에 실행되는 함수를 콜백 함수라고 한다.
주로 비동기 작업들(서버 요청, 파일 읽기)이 끝났을 때 실행시킬 코드가 있다! 주로 나는 이때 사용한다.
function order(menu : string, callback: (text : string) => void) {
console.log('{menu} 주문하기');
callback(menu);
}
function getDrink(menu : string){
console.log('{menu} 받으러 가기');
}
order("아아", getDrink);
// 이아 주문하기
// 아아 받으러 가기
order 함수에서 callback이라는 인자로 함수를 받는데 여기서 메뉴를 주문한 후에 들어가는 함수를 실행시킨다!
또 대표적으로 setTimeout이 해당되겠다.
console.log("작업 시작!");
setTimeout(() => {
// 이 함수가 바로 콜백 함수입니다.
console.log("3초 후에 이 메시지가 보입니다!");
}, 3000); // 3000ms = 3초
console.log("setTimeout() 실행 요청");
// 작업 시작!
// setTimeout() 실행 요청
// 3초 후에 이 메시지가 보입니다!
콜백 지옥
콜백 지옥이라는 말은 정말 찰떡으로 지은 것 같다. 진짜 코드 보기만 해도 지옥이 생각난다.
콜백 지옥은 비동기 작업 처리를 위해 콜백 함수를 연달아 중첩해서 사용하는 구조를 말한다.
// 1. 첫 번째 작업
setTimeout(() => {
console.log("작업 1 완료!");
// 2. 첫 번째 작업이 끝나면 두 번째 작업 실행
setTimeout(() => {
console.log("작업 2 완료!");
// 3. 두 번째 작업이 끝나면 세 번째 작업 실행
setTimeout(() => {
console.log("작업 3 완료!");
// 만약 작업이 더 있다면... 계속 안으로 들어갑니다.
}, 1000);
}, 1000);
}, 1000);
이렇게 세개만 중첩되어 있는데도 벌써부터 어질어질 하다.
일단 가독성이 안좋고, 나중에 저 코드에 기능을 추가하거나 로직 수정하려고 할 때, 이해하기가 너무 힘들 것 같다. (= 내 프로젝트 코드가 이랬었다....)
이제 구세주들 두둥등장
3. Promise
Promise는 비동기 작업의 결과(성공, 실패)를 나타내주는 객체이다. 그래서 다음에 3가지 상태를 가지는데,
1) pending (대기)
-> 초기 상태로 비동기 작업이 완료되지 않은 상태
2) fulfilled (이행)
-> 작업이 성공적으로 완료된 상태
3) rejected (거부)
-> 작업이 실패한 상태
3-1. Promise 생성
Promise는 객체 이기 때문에 new Promise()를 통해 객체를 만들어 줘야 한다.
Promise의 콜백 함수는 resolve와 reject라는 두 개의 함수를 인자로 받는다.
: resolve(value)
-> 비동기 작업이 성공했을 때 호출되고, 결과 value를 전달한다.
-> Promise는 fulfilled 상태가 된다.
: reject(error)
-> 비동기 자겅ㅂ이 실패했을 때 호출되고, error 정보를 전달한다.
-> Promise가 rejected 상태가 된다.
const promise = new Promise<string>((resolve, reject) => {
console.log("Promise 작업 시작...");
setTimeout(() => {
if (Math.random() > 0.1) { // 90% 확률로 성공
resolve("작업 성공!");
} else { // 10% 확률로 실패
reject(new Error("작업 실패!"));
}
}, 2000);
});
-> 2초 후에 작업이 끝나고 90% 확률로 성공!, 10% 확률로 실패 로직의 함수이다.
3-2. 후속 처리 메서드
Promise는 then, catch, finally를 통해 나타난 결과에 후속 처리를 할 수 있다.
: then(onFulfilled, onRejected)
-> Promise가 이행 됐을 때 실행해, 성공 결과값을 인자로 받는다.
-> 거부될 때도 실행시킬 수 있긴 한데 보통 catch()를 사용하겠지
: catch(onRejected)
-> Promise가 거부 됐을 때 실행되고, 에러를 인자로 받고 여기서 에러 처리를 해주면 된다.
: finally(onFinally)
-> Promise 성공/실패 여부와 상관없이 무조건 한 번 실행된다.
promise
.then((successMessage) => {
console.log("성공:", successMessage); // 성공: 작업 성공!
})
.catch((error) => {
console.error("실패:", error.message); // 실패: 작업 실패!
})
.finally(() => {
console.log("Promise 작업 끝.");
});
추가적으로 .then() 메서드는 항상 새로운 Promise를 반환하는데, 이러한 특징 때문에 여러 비동기 작업을 순차적으로 실행시킬 수 있다.
function step(taskNumber: number): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
const result = `작업 ${taskNumber} 완료`;
console.log(result);
resolve(result);
}, 1000);
});
}
step(1)
.then((resultFromStep1) => {
// resultFromStep1은 '작업 1 완료'
return step(2);
})
.then((resultFromStep2) => {
// resultFromStep2는 '작업 2 완료'
return step(3);
})
.then((resultFromStep3) => {
// resultFromStep3은 '작업 3 완료'
console.log("모든 작업이 끝났습니다.");
})
.catch((error) => {
console.error("체인 실행 중 에러:", error);
});
4. async/await
위에서 정리한 Promise를 더 간편하게 사용할 수 있도록 해주는게 async와 await이다.
하나씩 알아보자면,
4-1. async
async 키워드를 함수 앞에 붙이게 되면, 이 함수는 자동으로 Promise를 반환하는 비동기 함수가 된다.
그래서, 성공 또는 에러 발생시에 각각에 맞는 Promise가 반환되겠지?
// 이 함수는 Promise<string>을 반환한다.
async function sayHello(): Promise<string> {
return "안녕!";
}
sayHello().then(console.log); // 출력: 안녕!
4-2. await
await 키워드는 Promise가 처리될 때까지 함수의 실행을 일시 중지시키는 역할을 한다. 그리고 async 함수 안에서만 사용할 수 있다!
이 await을 활용하면 굳이 then()을 사용하지 않고, Promise의 결과값을 변수에 직접 할당하는 것처럼 로직을 구성할 수 있다.
function delay(ms: number, taskNumber: number): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
const result = `작업 ${taskNumber} 완료`;
console.log(result);
resolve(result);
}, ms);
});
}
// async 함수 선언
async function runAllTasks() {
console.log("모든 작업 시작");
// await를 사용하여 Promise가 끝날 때까지 기다림
const result1 = await delay(1000, 1);
const result2 = await delay(1000, 2);
const result3 = await delay(1000, 3);
console.log("--- 결과 요약 ---");
console.log(result1);
console.log(result2);
console.log(result3);
console.log("모든 작업이 끝났습니다.");
}
runAllTasks();
이렇게 await을 활용하면 굳이 then()으로 체이닝해서 복잡하게 코드를 구성할 필요 없이, delay 함수가 순서대로 실행되게 된다.
이제 정리를 통해 여러 비동기 처리에 대한 처리를 async/await 과 Promise를 통해 간편하게 해결할 수 있다!
'Language > TypeScript' 카테고리의 다른 글
[TypeScript] 클래스 (3) | 2025.04.01 |
---|---|
[TypeScript] 함수 (0) | 2025.03.31 |
[TypeScript] 변수와 타입 (0) | 2025.03.18 |
[TypeScript] TypeScript란? (0) | 2025.03.17 |