프로미스(Promises)

프로미스(Promise)는 자바스크립트에서 비동기 처리를 위해 사용되는 객체이다.

프로미스(Promise)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체이다.
이는 비동기 처리를 동기적으로 처리할 수 있게 해주며, 콜백 함수의 단점을 보완한다.

프로미스(Promise)의 상태

프로미스(Promise)는 세 가지 상태를 가진다.

  1. 대기(Pending): 초기 상태, 비동기 처리 로직이 아직 완료되지 않은 상태
  2. 이행(Fulfilled): 비동기 처리가 성공적으로 완료되어 프로미스(Promise)가 결과 값을 반환한 상태
  3. 거부(Rejected): 비동기 처리가 실패하거나 오류가 발생한 상태

Promise 생성자와 Executor 함수의 기본 구조

프로미스(Promise)는 new Promise() 생성자를 통해 생성된다.
Promise를 생성할 때는 다음과 같은 구조를 사용한다:

1
2
3
4
5
6
7
8
const myPromise = new Promise(
		// 여기서부터 executor 함수 시작 ↓
		(resolve, reject) => {
		    // 이 함수가 executor 함수입니다
		    // 여기서 비동기 작업을 수행합니다
		}
		// executor 함수 끝 ↑
);

이때 Promise 생성자에 전달되는 함수를 “executor” 함수라고 부른다.
이 함수는 resolve와 reject 두 가지 함수를 매개변수로 받는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Promise의 상태 변화 예시
function checkOrderStatus(orderNumber) {
    return new Promise((resolve, reject) => {
        console.log('주문 상태 확인 중…'); // Pending 상태
        
        setTimeout(() => {
            const order = findOrder(orderNumber);
            
            if (order) {
                resolve(order);  // Fulfilled 상태
            } else {
                reject('주문을 찾을 수 없습니다.');  // Rejected 상태
            }
        }, 1000);
    });
}

중요한 특징들

  1. 한 번만 상태 변경 가능

    1
    2
    3
    4
    5
    
    const promiseExample = new Promise((resolve, reject) => {
        resolve('첫 번째 resolve');  // 이것만 적용됨
        resolve('두 번째 resolve');  // 무시됨
        reject(new Error('에러'));   // 무시됨
    });
    
  2. 에러 처리

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    const promiseWithError = new Promise((resolve, reject) => {
        try {
            // 위험한 작업 수행
            const result = riskyOperation();
            resolve(result);
        } catch (error) {
            // 에러 발생 시 자동으로 reject 처리
            reject(error);
        }
    });
    
  3. 비동기 작업 래핑

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    function readFileAsync(filename) {
        return new Promise((resolve, reject) => {
            // 기존의 콜백 기반 API를 Promise로 래핑
            fs.readFile(filename, 'utf8', (error, data) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(data);
                }
            });
        });
    }
    

프로미스(Promise)의 사용

프로미스(Promise)는 then(), catch(), finally() 메서드를 통해 처리된다.

  1. then(): 프로미스(Promise)가 이행되었을 때 실행될 콜백 함수를 등록
  2. catch(): 프로미스(Promise)가 거부되었을 때 실행될 콜백 함수를 등록
  3. finally(): 프로미스(Promise)의 성공 또는 실패와 상관없이 실행될 콜백 함수를 등록
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
promise
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    console.log('작업 완료');
  });

프로미스 체이닝 (Promise Chaining)

프로미스(Promise)는 then() 메서드를 연속적으로 호출하여 여러 비동기 작업을 순차적으로 처리할 수 있다.
이를 프로미스 체이닝이라고 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 주문부터 배달까지의 전체 프로세스
function processOrder(orderDetails) {
    validateOrder(orderDetails)
        .then(validatedOrder => {
            // 주문 확인 후 결제 처리
            return processPayment(validatedOrder);
        })
        .then(paymentResult => {
            // 결제 완료 후 주방에 전달
            return sendToKitchen(paymentResult.orderId);
        })
        .then(kitchenConfirmation => {
            // 음식 준비 완료 후 배달 시작
            return startDelivery(kitchenConfirmation.orderId);
        })
        .then(deliveryInfo => {
            console.log('배달이 시작되었습니다:', deliveryInfo);
        })
        .catch(error => {
            // 어느 단계에서든 발생한 에러를 처리
            console.error('주문 처리 중 오류 발생:', error);
        });
}

프로미스(Promise)의 장점

  1. 콜백 지옥을 해결하여 코드의 가독성을 향상시킨다.
  2. 비동기 작업의 순서를 보장하고 에러 처리를 용이하게 한다.
  3. 여러 비동기 작업을 병렬로 처리할 수 있는 메서드(Promise.all, Promise.race 등)를 제공한다.

병렬로 처리할 수 있는 메서드

Promise.all() 사용하기

여러 Promise를 동시에 처리하고 싶을 때 사용한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 여러 재료의 재고를 동시에 확인
function checkIngredients(dish) {
    const ingredients = dish.requiredIngredients;
    
    const stockChecks = ingredients.map(ingredient => 
        checkStock(ingredient)
    );
    
    return Promise.all(stockChecks)
        .then(results => {
            // 모든 재료의 재고 확인이 완료됨
            console.log('모든 재료가 준비되었습니다');
            return true;
        })
        .catch(error => {
            // 하나라도 재고가 없으면 실패
            console.error('재료 부족:', error);
            return false;
        });
}
Promise.race() 활용

여러 Promise 중 가장 먼저 완료되는 것만 처리하고 싶을 때 사용한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 가장 빨리 응답하는 배달 기사 찾기
function findAvailableDriver(deliveryAddress) {
    const drivers = ['김기사', '이기사', '박기사'];
    
    const driverPromises = drivers.map(driver => 
        checkDriverAvailability(driver, deliveryAddress)
    );
    
    return Promise.race(driverPromises)
        .then(firstAvailableDriver => {
            console.log(`배달 기사가 배정되었습니다: ${firstAvailableDriver}`);
            return firstAvailableDriver;
        })
        .catch(error => {
            console.error('사용 가능한 배달 기사를 찾을 수 없습니다.');
            throw error;
        });
}

실제 활용 예시

API 호출 처리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function fetchUserData(userId) {
    return fetch(`/api/users/${userId}`)
        .then(response => {
            if (!response.ok) {
                throw new Error('사용자 정보를 가져올 수 없습니다.');
            }
            return response.json();
        })
        .then(userData => {
            console.log('사용자 데이터:', userData);
            return userData;
        })
        .catch(error => {
            console.error('에러:', error);
            throw error;
        });
}

파일 업로드 처리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function uploadFile(file) {
    return new Promise((resolve, reject) => {
        const formData = new FormData();
        formData.append('file', file);
        
        fetch('/api/upload', {
            method: 'POST',
            body: formData
        })
        .then(response => response.json())
        .then(result => {
            if (result.success) {
                resolve(result.fileUrl);
            } else {
                reject(new Error(result.error));
            }
        })
        .catch(error => reject(error));
    });
}

타임아웃 처리

1
2
3
4
5
6
7
8
function fetchWithTimeout(url, timeout = 5000) {
    return Promise.race([
        fetch(url),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('요청 시간 초과')), timeout)
        )
    ]);
}

프로미스(Promise)와 async/await

ES2017에서 도입된 async/await 문법은 프로미스를 기반으로 동작하며, 비동기 코드를 더욱 직관적으로 작성할 수 있게 해준다.
async 함수는 항상 프로미스를 반환하며, await 키워드는 프로미스가 처리될 때까지 함수의 실행을 일시 중지한다.

1
2
3
4
5
6
7
8
async function asyncFunction() {
  try {
    const result = await someAsyncOperation();
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

참고 및 출처