비동기 처리(asynchronous processing)는 자바스크립트 및 TypeScript에서 필수적인 개념으로, 시간 소요가 긴 작업(예: 네트워크 요청, 파일 읽기 등)을 처리할 때 사용됩니다. 프로그램이 해당 작업을 기다리지 않고 다른 작업을 수행할 수 있게 함으로써 효율성을 높입니다. 이번 글에서는 TypeScript를 사용한 비동기 처리 방식으로 콜백, 프로미스, 그리고 async/await
에 대해 설명하겠습니다.
1. 콜백 (Callback)
1.1 콜백의 정의
콜백은 비동기 작업이 완료된 후 실행할 함수를 다른 함수에 전달하는 방식입니다. 이 방식은 비동기 작업의 완료 여부에 따라 호출되는 함수를 지정합니다.
1.2 콜백의 예시 (TypeScript)
import { readFile } from "fs";
readFile("example.txt", "utf8", (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
위 예시는 Node.js에서 파일을 비동기적으로 읽고, 작업이 완료되면 콜백 함수가 호출됩니다. 이 경우 프로그램은 파일을 읽는 동안 다른 작업을 수행할 수 있습니다.
- 단일 비동기 작업 처리: 파일 읽기, 네트워크 요청 등 단일 비동기 작업을 처리할 때 사용합니다.
- 에러 처리: 첫 번째 매개변수로 에러 객체를 받아 에러 처리를 할 수 있습니다.
- 간단한 비동기 로직: 복잡하지 않은 비동기 로직을 구현할 때 사용합니다.
- 이벤트 기반 프로그래밍: 이벤트 리스너와 같이 특정 이벤트 발생 시 실행될 로직을 정의할 때 사용합니다.
- 라이브러리 호환성: 콜백을 사용하는 오래된 라이브러리와 함께 작업할 때 유용합니다.
단, 여러 비동기 작업을 연속적으로 처리해야 하는 경우 콜백 지옥을 피하기 위해 프로미스나 async/await를 사용하는 것이 좋습니다.
1.3 콜백 지옥 (Callback Hell)
콜백 방식은 간단하지만, 중첩된 콜백이 많아지면 코드가 복잡해지고 유지보수가 어려워집니다. 이를 흔히 "콜백 지옥"이라고 부릅니다.
doSomething((result: any) => {
doSomethingElse(result, (newResult: any) => {
doThirdThing(newResult, (finalResult: any) => {
console.log("Final result: " + finalResult);
});
});
});
위 코드는 콜백이 중첩되어 복잡해지며, 가독성이 떨어집니다. 이러한 콜백 지옥은 다음과 같은 상황에서 특히 문제가 될 수 있습니다:
- 복잡한 비동기 워크플로우: 여러 단계의 비동기 작업이 순차적으로 실행되어야 하는 경우
- API 호출 체인: 여러 API를 연속적으로 호출해야 하는 경우
- 데이터베이스 작업: 여러 쿼리를 순차적으로 실행해야 하는 경우
- 파일 시스템 작업: 여러 파일을 순차적으로 읽거나 쓰는 경우
- 애니메이션 시퀀스: 여러 애니메이션을 순차적으로 실행해야 하는 경우
이러한 상황에서 콜백 지옥을 피하고 코드의 가독성과 유지보수성을 높이기 위해 프로미스(Promise)나 async/await와 같은 더 현대적인 비동기 처리 방식을 사용하는 것이 좋습니다.
1.4 콜백의 장단점
콜백은 비동기 프로그래밍에서 널리 사용되는 패턴이지만, 장단점이 있습니다.
1.4.1 장점
- 간단성: 구현이 간단하고 직관적입니다.
- 유연성: 다양한 상황에 적용할 수 있습니다.
- 비동기 처리: 비동기 작업을 효과적으로 처리할 수 있습니다.
- 이벤트 기반 프로그래밍: 이벤트 리스너 등에 적합합니다.
1.4.2 단점
- 콜백 지옥: 중첩된 콜백으로 인해 코드가 복잡해질 수 있습니다.
- 에러 처리의 어려움: 여러 콜백에 걸친 에러 처리가 복잡할 수 있습니다.
- 제어 흐름 예측 어려움: 복잡한 비동기 로직에서 실행 순서를 예측하기 어려울 수 있습니다.
- 디버깅의 어려움: 콜백 체인에서 문제를 찾기 어려울 수 있습니다.
이러한 장단점을 고려하여 적절한 상황에 콜백을 사용하는 것이 중요합니다.
2. 프로미스 (Promise)
2.1 프로미스의 정의
프로미스는 비동기 작업의 결과를 나타내는 객체로, pending
, fulfilled
, rejected
세 가지 상태를 가집니다. 프로미스는 콜백보다 구조화된 방법으로 비동기 작업을 처리합니다. 다음은 프로미스의 세 가지 상태에 대한 상세한 예제입니다:
2.1.1 Pending (대기 중)
const pendingPromise = new Promise((resolve, reject) => {
// 비동기 작업이 아직 완료되지 않음
});
console.log(pendingPromise); // Promise {<pending>}
2.1.2. Fulfilled (이행됨)
const fulfilledPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("성공!");
}, 1000);
});
fulfilledPromise.then((result) => console.log(result)); // 1초 후 "성공!" 출력
2.1.3. Rejected (거부됨)
const rejectedPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("실패!"));
}, 1000);
});
rejectedPromise.catch((error) => console.error(error)); // 1초 후 Error: 실패! 출력
이러한 상태 변화를 통해 프로미스는 비동기 작업의 완료 또는 실패를 효과적으로 처리할 수 있습니다.
2.2 프로미스의 예시 (TypeScript)
const promise = new Promise<string>((resolve, reject) => {
const success = true;
if (success) {
resolve("작업 성공");
} else {
reject("작업 실패");
}
});
promise
.then((result) => {
console.log(result); // '작업 성공' 출력
})
.catch((error) => {
console.error(error); // '작업 실패' 출력
});
프로미스는 then
과 catch
메서드를 사용해 성공과 실패를 처리하며, 콜백 방식보다 가독성이 뛰어납니다.
2.3 프로미스 체이닝 (Promise Chaining)
여러 비동기 작업을 순차적으로 처리할 때 프로미스 체이닝을 사용하면 중첩 문제를 해결할 수 있습니다.
function doSomething(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});
}
function doSomethingElse(result: string): Promise<string> {
return new Promise((resolve) => {
console.log("두 번째 작업 시작:", result);
setTimeout(() => resolve(result + ", 두 번째 작업 완료"), 1000);
});
}
function doThirdThing(result: string): Promise<string> {
return new Promise((resolve) => {
console.log("세 번째 작업 시작:", result);
setTimeout(() => resolve(result + ", 세 번째 작업 완료"), 1000);
});
}
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log("최종 결과: " + finalResult);
})
.catch((error) => {
console.error(error);
});
이 코드의 출력 결과는 다음과 같습니다:
$ node promise-chaining.ts
두 번째 작업 시작: 첫 번째 작업 완료
세 번째 작업 시작: 첫 번째 작업 완료, 두 번째 작업 완료
최종 결과: 첫 번째 작업 완료, 두 번째 작업 완료, 세 번째 작업 완료
$
프로미스 체이닝을 사용하면 논리적 흐름을 유지하면서 비동기 작업을 순차적으로 처리할 수 있습니다.
3. async/await
3.1 async/await의 정의
async/await
는 프로미스를 기반으로 하여 비동기 코드를 동기 코드처럼 작성할 수 있게 해주는 문법입니다. async
키워드를 사용하면 함수는 자동으로 프로미스를 반환하고, await
키워드는 해당 프로미스가 해결될 때까지 기다립니다.
3.2 async/await의 예시 (TypeScript)
async function fetchData(): Promise<void> {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
위 예시에서 await
키워드를 사용해 비동기 작업의 완료를 기다리고, 그 결과를 바로 사용할 수 있습니다. 코드가 매우 간결하고 가독성이 좋아집니다.
3.3 async/await의 장점
- 가독성: 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 뛰어납니다.
- 에러 처리:
try/catch
구문을 사용하여 에러를 직관적으로 처리할 수 있습니다. - 코드 유지보수성: 복잡한 비동기 로직도 간단하게 작성할 수 있습니다.
3.4 async/await의 병렬 처리
여러 비동기 작업을 병렬로 처리할 때는 Promise.all
을 활용할 수 있습니다.
async function fetchMultipleData(): Promise<void> {
try {
const [data1, data2] = await Promise.all([
fetch("https://api.example.com/data1"),
fetch("https://api.example.com/data2"),
]);
const result1 = await data1.json();
const result2 = await data2.json();
console.log(result1, result2);
} catch (error) {
console.error(error);
}
}
fetchMultipleData();
Promise.all
은 여러 비동기 작업을 병렬로 실행하고, 모든 작업이 완료되면 결과를 반환합니다.
4. 콜백, 프로미스, async/await 비교
특성 | 콜백 | 프로미스 | async/await |
---|---|---|---|
가독성 | 낮음 | 중간 | 높음 |
에러 처리 | 비직관적 (콜백 내에서 처리) | catch 로 처리 |
try/catch 로 처리 |
코드 중첩 문제 | 콜백 지옥 발생 가능 | 체이닝으로 해결 가능 | 동기 코드처럼 처리 가능 |
사용 시기 | 초기 비동기 처리 방식 | ES6에서 도입 | ES8에서 도입 |
결론
TypeScript에서 비동기 처리는 콜백에서 시작해, 프로미스와 async/await
으로 발전했습니다. 각 방식은 서로 다른 시나리오에 적합하며, 특히 async/await
는 비동기 코드를 동기식으로 처리할 수 있어 가독성과 유지보수성에서 매우 유리합니다. 상황에 맞는 비동기 처리 방식을 선택하여 효율적으로 코드를 작성하는 것이 중요합니다.
'JavaScript & TypeScript' 카테고리의 다른 글
require와 import의 차이점: JavaScript 모듈 시스템 비교 (2) | 2024.11.05 |
---|---|
JavaScript 변수 선언의 모든 것: var, let, const의 차이점과 올바른 사용법 (1) | 2024.10.29 |
JavaScript의 내장함수 - every() (0) | 2024.10.12 |
JavaScript의 내장 함수 - match() (1) | 2024.10.11 |
JavaScript의 내장 함수 - test() (0) | 2024.10.09 |