1. 콜백이란?
콜백 함수는 자바스크립트에서 비동기 작업을 처리하는 가장 기본적인 방법이다.
콜백은 다른 함수에 인자로 전달되는 함수로, 특정 작업이 완료된 후 실행된다.
자바스크립트의 함수가 일급 객체(first-class citizen)이기 때문에 가능한 패턴이다.
콜백 함수의 특징
- 함수를 다른 함수의 인자로 전달한다
- 특정 이벤트가 발생하거나 작업이 완료되면 호출된다
- 실행 순서가 보장되어 함수의 실행 흐름을 제어하는 데 사용된다
1.1. 비동기 작업에서의 콜백 패턴
비동기 프로그래밍에서 콜백은 작업이 완료된 후 실행되어야 할 코드를 지정하는 데 사용된다.
ex)
// 비동기 콜백 예제
console.log("요청 시작");
setTimeout(function() {
console.log("타이머 완료 후 실행");
}, 2000);
console.log("다음 작업 진행");
출력 결과:
요청 시작
다음 작업 진행
타이머 완료 후 실행
setTimeout에 전달된 콜백 함수는 2초 후에 비동기적으로 실행된다.
1.3. 콜백 지옥(Callback Hell)
중첩된 비동기 작업을 처리할 때, 콜백 함수가 여러 단계로 중첩되는 현상이 발생할 수 있다.
이를 콜백 지옥이라고 한다.
ex)
getUserData(userId, function(userData) {
getArticles(userData.name, function(articles) {
getComments(articles[0].id, function(comments) {
... // 중첩된 콜백
}, function(error) {
handleError(error);
});
}, function(error) {
handleError(error);
});
}, function(error) {
handleError(error);
});
콜백 지옥의 문제점
1) 가독성 저하: 코드가 오른쪽으로 깊게 들여쓰기되어 읽기 어렵다.
2) 유지보수 어려움: 로직 변경 시 중첩된, 콜백 구조 전체를 수정해야 할 수 있다.
3) 에러 처리 복잡성: 각 단계마다 에러 처리를 반복해야 하는 경우가 많다.
4) 디버깅 어려움: 스택 트레이스가 복잡해져 오류 발생 위치를 파악하기 어렵다.
1.4. 콜백 지옥 개선 방법
1) 함수 분리: 콜백 함수를 분리하여 명명된 함수로 만들기
2) 모듈화: 관련 기능을 모듈로 분리하여 관리
3) Promise 사용: 콜백 대신 Promise 기반 코드로 변환
4) async/await 사용: Promise 기반 비동기 코드를 더 동기적인 스타일로 작성
2. Promise의 기본 개념
Promise는 비동기 작업의 최종 완료(또는 실패)와 그 결과값을 나타내는 객체이다.
ES6(ES2015)에서 표준으로 도입되어 콜백 패턴의 여러 문제를 해결한다.
2.1. Promise의 상태
Promise는 다음 세 가지 상태 중 하나를 가진다.
1) 대기(pending): 초기 상태, 이행되거나 거부되지 않은 상태
2) 이행(fulfilled): 작업이 성공적으로 완료됨
3) 거부(rejected): 작업이 실패함
Promise가 이행되거나 거부되면 '처리됨(settled)'이라고 하며, 이 상태에서는 더 이상 다른 상태로 변경되지 않는다.
2.2. Promise 생성
Promise 객체는 new Promise() 생성자로 만들며, 실행 함수를 인자로 받는다.
ex)
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = Math.random() > 0.5;
setTimeout(() => {
if (success) {
resolve("작업 성공!"); // 이행(fulfilled) 상태로 변경
} else {
reject(new Error("작업 실패!")); // 거부(rejected) 상태로 변경
}
}, 2000);
});
이 Promise는 2초 후 50% 확률로 성공 또는 실패한다.
- resolve 함수: Promise를 이행 상태로 변경하고 결과값 반환
- reject 함수: Promise를 거부 상태로 변경하고 에러 반환
2.3. Promise 사용
Promise 객체는 .then(), .catch(), .finally() 메서드를 통해 비동기 작업의 결과를 처리한다.
ex)
myPromise
.then(result => {
console.log(result); // "작업 성공!"
return "다음 단계";
})
.then(nextResult => {
console.log(nextResult); // "다음 단계"
})
.catch(error => {
console.error(error.message); // "작업 실패!"
})
.finally(() => {
console.log("무조건 실행됨"); // 성공하든 실패하든 항상 실행
});
- .then(): Promise가 이행되었을 때 실행할 콜백 등록
- .catch(): Promise가 거부되었을 때 실행할 콜백 등록
- .finally(): Promise가 처리되었을 때(이행 또는 거부) 실행할 콜백 등록
3. Promise 체이닝
Promise의 가장 유용한 기능 중 하나는 여러 비동기 작업을 순차적으로 연결할 수 있는 체이닝이다.
3.1. 기본 체이닝
.then()은 새로운 Promise를 반환하므로, 여러 .then()을 연결할 수 있다.
ex)
fetchUser(userId)
.then(user => {
console.log("사용자 정보:", user);
return fetchArticles(user.name);
})
.then(articles => {
console.log("작성한 글:", articles);
return fetchComments(articles[0].id);
})
.then(comments => {
console.log("댓글:", comments);
})
.catch(error => {
console.error("오류 발생:", error);
});
각 .then()에서 Promise를 반환하면 자동으로 다음 .then()에서 그 Promise의 결과를 받을 수 있다.
이를 통해 콜백 지옥을 평평한 구조로 변환할 수 있다.
3.2. 값 변환
Promise 체인에서는 이전 단계의 결과를 변환하여 다음 단계로 전달할 수 있다.
ex)
fetch('https://api.example.com/data')
.then(response => response.json()) // 응답을 JSON으로 변환
.then(data => {
return {
processed: true,
count: data.length,
items: data.map(item => item.name)
};
})
.then(transformedData => {
console.log(transformedData);
});
각 .then()에서 반환된 값은 자동으로 Promise로 래핑되어 다음 .then()으로 전달된다.
3.3. 에러 처리와 복구
.catch()를 체인 중간에 배치하여 에러 처리 후 체인을 계속 이어갈 수 있다.
ex)
fetchData()
.then(data => {
if (!data) throw new Error("데이터 없음");
return processData(data);
})
.catch(error => {
console.warn("데이터 처리 중 오류:", error);
return getDefaultData(); // 에러 발생 시 기본 데이터로 복구
})
.then(result => {
// 정상 데이터나 기본 데이터로 작업 진행
displayResult(result);
});
이 패턴을 사용하면 에러가 발생해도 애플리케이션이 계속 동작할 수 있다.
4. Promise의 정적 메서드
4.1. Promise.resolve() 와 Promise.reject()
즉시 이행 또는 거부 상태의 Promise를 생성한다.
ex)
// 즉시 이행되는 Promise
const fulfilledPromise = Promise.resolve("이미 완료된 데이터");
// 즉시 거부되는 Promise
const rejectedPromise = Promise.reject(new Error("즉시 실패"));
fulfilledPromise.then(data => console.log(data));
rejectedPromise.catch(error => console.error(error.message));
일반 값을 Promise로 변환하거나, 동기 코드와 비동기 코드를 일관되게 처리할 때 유용하다.
4.2. Promise.all()
여러 Promise를 병렬로 실행하고, 모두 이행되었을 때 결과를 배열로 반환한다.
하나라도 거부되면 전체가 거부된다.
ex)
const promises = [
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/comments')
];
Promise.all(promises)
.then(responses => Promise.all(responses.map(res => res.json())))
.then(([users, posts, comments]) => {
console.log("사용자:", users);
console.log("포스트:", posts);
console.log("댓글:", comments);
})
.catch(error => {
console.error("하나 이상의 요청 실패:", error);
});
모든 API 요청이 완료된 후에만 데이터를 처리해야 할 때 유용하다.
4.3. Promise.allSettled()
모든 Promise가 처리될 때까지 기다리고, 각각의 결과 상태를 객체 배열로 반환한다.
ES2020에서 도입되었다.
ex)
const promises = [
fetch('https://api.example.com/endpoint-1'),
fetch('https://api.example.com/endpoint-2'),
Promise.reject(new Error("항상 실패"))
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} 성공:`, result.value);
} else {
console.log(`Promise ${index} 실패:`, result.reason);
}
});
});
일부 실패가 전체 작업을 중단시키지 않아야 할 때 유용하다.
4.4. Promise.race()
여러 Promise 중 가장 먼저 처리되는 것의 결과를 반환한다.
ex)
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("시간 초과")), 5000);
});
const dataPromise = fetch('https://api.example.com/data');
Promise.race([dataPromise, timeoutPromise])
.then(response => response.json())
.then(data => console.log("데이터:", data))
.catch(error => console.error("오류:", error.message));
4.5. Promise.any()
여러 Promise 중 가장 먼저 이행되는 것의 값을 반환한다.
모든 Promise가 거부되면 AggregateError가 발생한다.
ES2021에서 도입되었다.
ex)
const promises = [
fetch('https://api-1.example.com/data').then(r => r.json()),
fetch('https://api-2.example.com/data').then(r => r.json()),
fetch('https://api-3.example.com/data').then(r => r.json())
];
Promise.any(promises)
.then(firstSuccessfulData => {
console.log("가장 빠른 응답:", firstSuccessfulData);
})
.catch(error => {
console.error("모든 API 요청 실패:", error);
});
여러 소스에서 동일한 데이터를 가져오고 가장 빠른 응답을 사용하려는 경우 유용하다.
5. 콜백과 Promise 비교
5.1. 콜백 패턴의 단점
- 가독성: 중첩된 콜백은 코드의 가독성을 크게 저하시킨다
- 에러 처리: 각 콜백에서 개별적으로 에러를 처리해야 한다
- 제어 흐름: 복잡한 비동기 흐름을 관리하기 어렵다
- 코드 재사용: 콜백 기반 코드는 재사용이 어려울 수 있다
5.2. Promise의 장점
- 예측 가능성: Promise는 한 번만 처리되며 상태가 변경된 후에는 불변
- 통합 에러 처리: .catch()를 통한 중앙화된 에러 처리
- 조합 가능성: Promise.all() 등을 통한 비동기 작업 조합
- 타이밍 보장: 이미 처리된 Promise에 등록한 콜백도 적절히 호출됨
'학습 > JavaScript' 카테고리의 다른 글
JavaScript 비동기 3 : async/await (0) | 2025.04.11 |
---|---|
JavaScript 비동기 1 : 이벤트 기반 (0) | 2025.04.09 |
프로토타입과 Class (0) | 2025.04.08 |
var, let, const (0) | 2025.04.07 |
Javascript와 C++ (0) | 2024.12.15 |