Javascript

자바스크립트 개발자를 위한 Promise 완벽 가이드: 콜백 지옥 탈출하기

드리프트2 2024. 12. 28. 11:05

자바스크립트 개발자를 위한 Promise 완벽 가이드: 콜백 지옥 탈출하기

콜백 함수 지옥에 빠져 허우적대는 당신! 브라우저가 버벅거리며 긴 작업에 힘겨워하는 모습에 좌절한 적 있으신가요?

 

그런 여러분을 위해 Promise가 등장했습니다! 마치 새끼손가락 걸고 약속하는 것처럼 든든한 JavaScript Promise의 세계를 함께 알아볼까요?

 

자바스크립트의 Promise는 현실의 약속보다 훨씬 믿음직스러울지도 몰라요!

 

이번 포스팅에서는 Promise가 무엇이며, JavaScript에서 어떻게 작동하는지 자세히 살펴보겠습니다.

비동기 작업이란 무엇일까요?

Promise에 대해 알아보기 전에 비동기 작업에 대한 개념부터 짚고 넘어가는 것이 좋겠죠?

 

간단히 말해서, 비동기 작업은 컴퓨터가 즉시 완료하지 않는 모든 작업입니다.

 

다시 말해, 작업이 미래의 어느 시점까지 완료될 것으로 예상되지 않는다는 뜻입니다.

 

비동기 작업은 JavaScript에서 특히 중요한데요. 메인 실행 스레드를 차단하지 않고 오래 걸리는 작업을 수행할 수 있게 해주기 때문입니다.

 

반대로, JavaScript에서 블로킹 작업이 오래 걸리면 작업을 실행하는 브라우저 창이 응답하지 않게 될 수 있습니다.

동시성과 병렬성은 다릅니다!

Go와 달리 JavaScript는 Web Workers가 도입되기 전까지 병렬 프로그래밍에 대한 기본 지원이 없었습니다.

 

따라서 대부분의 JavaScript 코드는 기본적으로 단일 스레드입니다.

 

하지만 Rob Pike가 Go에 대해 이야기할 때 상기시켜 주었듯이, 코드가 병렬로 작동할 수 없다고 해서 동시성 스타일로 작성할 수 없다는 의미는 아닙니다.

 

동일한 원칙이 JavaScript에도 적용됩니다.

 

비동기 코드를 작성할 때마다 우리는 동시성을 사용하고 있는 것입니다.

 

JavaScript는 어떤 종류의 비동기 작업을 처리할 수 있을까요?

비동기 작업은 웹 개발에서 일상적인 작업에 매우 일반적으로 사용됩니다.

몇 가지 예를 아래 목록에 정리해 보았습니다.

  • 서버에서 데이터 다운로드 (fetch 함수 사용)
  • 서버에 파일 업로드 (XMLHttpRequest 생성자 함수 사용)
  • 대화형 애니메이션 수행 (requestAnimationFrame 함수 사용)
  • 나중에 실행되도록 코드 예약 (setTimeout 또는 setInterval 함수 사용)
  • 사용자 입력 처리 (DOM 요소에 대한 이벤트 리스너 사용)
  • 사용자 위치 정보 가져오기 (Geolocation.getCurrentPosition 함수 사용)
  • 데이터베이스에 쓰기 (IndexedDB API 사용)

콜백 함수는 어떻게 사용될까요?

콜백 함수는 다른 함수에 인수로 전달되고 나중에 해당 함수에 의해 호출되는 함수입니다.

 

즉, 콜백 함수는 작업이 완료될 때 나중에 "콜백"되는 함수입니다.

 

따라서 비동기 작업이 시작되면 작업이 완료될 때 호출될 콜백 함수가 전달될 수 있습니다.

 

콜백 함수에는 작업에서 생성된 결과가 인수로 제공되며, 이를 사용하여 전역 상태 또는 DOM을 업데이트할 수 있습니다.

 

반면에 결과가 없으면 콜백 함수는 인수 없이 호출됩니다.

 

아래에 표시된 setTimeout 함수는 비동기적으로 실행되며, 콜백은 완료되는 즉시 실행됩니다.

 

setTimeout(() => {
        console.log("the timeout has completed");
}, 500);

 

setTimeout 함수의 두 번째 인수는 콜백 함수가 호출되기 전에 경과해야 하는 시간(밀리초)입니다.

 

따라서 이 경우 0.5초(500ms) 동안 기다립니다.

 

콜백 지옥이란 무엇일까요?

콜백은 비동기 작업을 처리하는 강력한 방법이지만, 중첩된 콜백을 읽고 이해하고 유지 관리하기 어려운 상황인 콜백 지옥으로 이어질 수도 있습니다.

 

다음은 콜백 지옥의 가상적인 예입니다.

getUserFromDatabase("Mark Jones", (user) => {
        getDocumentsWrittenByUserFromDatabase(user, (documents) => {
                findOldDocuments(documents, (oldDocuments) => {
                        deleteDocumentsFromDatabase(oldDocuments, (success, error) => {
                                if (success) {
                                        console.log("Mark's old documents have been successfully removed from the database.")
                                } else {
                                        throw error;
                                }
                        });
                });
        });
});

 

위 예제에서 함수가 실제로 어떻게 작동하는지는 중요하지 않습니다.

 

여러 콜백 함수를 서로 중첩하여 사용할 때 중첩이 얼마나 복잡하고 혼란스러워질 수 있는지 보여주기 위한 예시일 뿐입니다.

 

중첩된 함수가 10개 또는 20개라면 어떤 일이 일어나고 있는지 파악하기가 얼마나 어려울지 상상해 보세요!

 

Promise는 중첩을 전혀 사용할 필요가 없기 때문에 이 문제에 대한 훌륭한 해결책을 제공합니다.

 

Promise란 무엇일까요?

Promise는 비동기 작업의 최종 완료 또는 실패와 그 결과 값을 나타내는 객체입니다.

 

Promise는 비동기 작업을 더 잘 처리할 수 있도록 JavaScript(2015년에 출시된 ES6)에 도입되었습니다.

 

Promise는 동기 코드처럼 보이는 비동기 코드를 작성할 수 있는 방법을 제공하여 코드를 훨씬 쉽게 읽고 유지 관리할 수 있도록 합니다.

 

Promise는 어떤 상태일 수 있을까요?

Promise는 다음 세 가지 상태 중 하나일 수 있습니다.

  • 대기 중(pending)
  • 이행됨(fulfilled)
  • 거부됨(rejected)

Promise가 처음 생성되면 대기 중 상태입니다.

 

대기 중 상태에서 Promise는 비동기 작업이 완료될 때까지 기다립니다.

 

작업이 성공적으로 완료되면 Promise는 이행됨 상태로 전환되고 작업 결과로 해결됩니다.

 

작업이 실패하면 Promise는 거부됨 상태로 전환되고 오류와 함께 거부됩니다.

 

Promise는 어떻게 생성할까요?

Promise를 생성하려면 JavaScript에서 객체를 생성하는 일반적인 방법인 new 키워드를 사용하여 Promise 생성자를 호출하면 됩니다.

 

Promise 생성자는 단일 인수를 사용하며, 이 인수 자체는 두 개의 인수(관례상 resolve 및 reject라는 이름)를 사용하는 함수입니다.

 

이 함수는 Promise에 필요한 유일한 콜백입니다.

 

resolve 함수는 비동기 작업이 성공적으로 완료될 때 호출되는 반면, reject 함수는 작업이 어떤 식으로든 실패할 때 호출됩니다.

간단한 예는 다음과 같습니다.

const promise = new Promise((resolve, reject) => {
        const result = Number.parseInt("deadbeef", 16);

        if (Number.isNaN(result)) {
                reject(new Error("parsing failed"));
        } else {
                resolve(result);
        }
});

 

promise 내에서 문자열에서 정수를 구문 분석하는 예제 작업을 수행하지만, 이는 비동기식이 아닌 동기식으로 완료되므로 promise 내에 있는 것이 실제로 이점이 되지는 않습니다.

 

데모 목적으로 사용했을 뿐입니다.

 

구문 분석 작업이 실패하면 result 변수는 NaN과 같아지므로 이를 확인하고 필요한 경우 문제를 설명하는 Error와 함께 reject 함수를 호출합니다.

 

그렇지 않으면 성공적으로 구문 분석한 숫자와 함께 resolve 함수를 호출합니다.

 

Promise는 어떻게 사용할까요?

Promise가 생성되면 이를 사용하여 비동기 작업의 결과를 처리할 수 있습니다.

 

then 및 catch 메서드를 사용하여 Promise에 중첩되지 않은 콜백 시퀀스를 연결하여 이를 수행합니다.

 

then 메서드는 promise가 성공적으로 이행될 때 호출되고 catch 메서드는 promise가 거부될 때 호출됩니다.

 

이전에 생성한 promise를 사용한 예는 다음과 같습니다.

promise.then((result) => {
        console.log(result); // 3735928559 출력
}).catch((error) => {
        console.error(error); // "Error: parsing failed" 출력
});

 

이 promise가 실행되면 then 메서드만 실행될 것으로 예상해야 합니다.

 

"deadbeaf"가 16진수로 구문 분석되어 10진수 3735928559와 같다는 것을 알고 있기 때문입니다.

 

하지만 예기치 않은 오류가 발생할 경우를 대비하여 catch 함수를 정의하는 것이 항상 유용합니다.

 

Promise는 어떻게 연결할까요?

Promise를 연결하여 여러 비동기 작업을 순차적으로, 즉 하나씩 수행할 수 있습니다.

 

하나의 promise가 완료되면 결과가 체인의 다음 promise로 전달되고 이 프로세스는 무기한 계속될 수 있습니다.

 

promise를 연결하는 이 기능 덕분에 동기 코드처럼 보이는 비동기 코드를 쉽게 작성할 수 있습니다.

 

다음은 promise를 연결하는 방법의 예입니다.

const promiseOne = new Promise((resolve) => {
        setTimeout(() => {
                resolve("foo");
        }, 1000);
});

const callbackOne = (value) => {
        return new Promise((resolve) => {
                setTimeout(() => {
                        resolve(value + "bar");
                }, 1000);
        });
};

const callbackTwo = (result) => {
        console.log(result);
};


promiseOne.then(callbackOne).then(callbackTwo);

 

먼저 promise와 두 개의 콜백 함수를 생성합니다. 각 함수는 promise의 then 메서드에 전달됩니다.

 

초기 promise는 1초(1,000밀리초) 지연 후에 해결되고 문자열 "foo"로 해결됩니다.

 

첫 번째 콜백 함수는 값을 입력으로 받아 입력 값과 문자열 "bar"를 연결하여 해결되는 새 promise를 반환합니다.

 

두 번째 콜백 함수는 두 번째 promise에서 받은 값을 출력합니다.

 

그런 다음 then 메서드를 사용하여 모든 것을 연결합니다.

 

Promise에서 오류는 어떻게 처리할까요?

promise의 가장 좋은 점 중 하나는 체인의 어느 부분에서 발생하는 오류든 처리하는 특히 우아한 방법을 제공한다는 사실입니다.

 

promise가 거부되면 오류는 catch 메서드에 의해 포착될 때까지 promise 체인을 따라 전파됩니다.

 

따라서 여러 콜백 함수가 아닌 단일 위치에서 오류를 처리할 수 있습니다.

 

아래 예제는 promise 내에서 발생하는 오류를 처리하는 방법을 보여줍니다.

const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
                reject(new Error("failure"));
        }, 1000);
});

promise.catch((error) => {
        console.error(error);
});

 

먼저 1초 지연 후에 항상 오류와 함께 거부되는 promise를 생성합니다.

 

그런 다음 promise에 catch 메서드를 연결하여 오류를 처리하고 콘솔에 출력합니다.

 

catch 메서드 뒤에 다른 then 메서드가 있으면 오류가 처리되었으므로 체인이 계속됩니다.

 

all 함수를 사용하여 여러 Promise를 한 번에 실행할 수 있을까요?

마지막으로 Promise.all 함수를 살펴보겠습니다.

 

이 함수는 여러 비동기 작업을 동시에 수행하고 모든 작업이 완료될 때까지 기다렸다가 계속할 수 있기 때문에 특히 유용합니다.

 

아래 예제는 Promise.all 함수를 사용하여 여러 웹 주소에서 동시에 데이터를 다운로드하는 방법을 보여줍니다.

const urls = [
        "https://jsonplaceholder.typicode.com/users/1",
        "https://jsonplaceholder.typicode.com/users/2",
        "https://jsonplaceholder.typicode.com/users/3",
        "https://jsonplaceholder.typicode.com/users/4"
];

const promises = urls.map((url) => {
        return fetch(url).then((response) => {
                return response.json();
        });
});

Promise.all(promises)
        .then((results) => {
                console.log(results);
        })
        .catch((error) => {
                console.error(error);
        });

 

먼저 데이터를 다운로드할 URL 배열을 선언합니다.

 

(사용 중인 특정 URL은 테스트 목적으로 더미 JSON 데이터를 제공합니다.)

 

그런 다음 map 메서드를 사용하여 브라우저의 기본 fetch 함수를 사용하여 각 URL에서 데이터를 가져오는 promise 배열을 생성합니다.

 

Promise.all 함수는 promise 배열을 입력으로 받아 배열의 모든 promise가 해결되면 결과 배열로 해결되는 새 promise를 반환합니다.

 

마지막으로 then 메서드를 사용하여 결과를 출력합니다. 이 경우 JSON 객체 배열입니다.

 

입력 promise 중 하나라도 거부되면 Promise.all은 새 promise를 거부합니다.

 

그러나 각 promise에 대해 개별적으로 오류를 처리해야 하는 경우 map에서 반환된 각 promise에 대해 Promise.all에 전달하기 전에 catch 메서드를 사용해야 합니다.