콜백 지옥이란, 비동기 프로그래밍에서 각 스레드의 순서를 지키기 위해 각 콜백 함수를 중첩해서 호출할 경우에 발생 되는 가독성 저해 문제인데요.
다음의 코드는 setTimeout 메서드와 콜백 함수를 각각 하나씩 보관하고 있는 세 개의 작업 메서드를 중첩해서 차례대로 실행시키는 예제입니다.
로직은 firstTask가 먼저 실행되면서 setTimeout 메서드를 실행하고, 1초 뒤에 콘솔 출력과 함께 전달 받은 메서드(secondTask)를 실행합니다.
그 다음 secondTask 또한 firstTask와 같은 절차를 반복한 뒤 콜백 함수인 thirdTask를 실행하고요.
이후 thirdTask가 실행되어 콘솔을 출력한 뒤 콜백함수(모든 작업 완료를 출력하는 함수)를 실행하면서 로직은 끝납니다.
function firstTask(callback) { setTimeout(function () { console.log("첫 번째 작업 완료"); callback(); }, 1000); } function secondTask(callback) { setTimeout(function () { console.log("두 번째 작업 완료"); callback(); }, 1000); } function thirdTask(callback) { setTimeout(function () { console.log("세 번째 작업 완료"); callback(); }, 1000); } firstTask(function () { secondTask(function () { thirdTask(function () { console.log("모든 작업 완료"); }); }); });
위 코드의 장점이라고 한다면, 아무래도 비동기 스레드들에 순서를 줄 수 있다는 장점이 있는 반면, 가독성이 떨어진다는 단점이 있죠. 또 늘어나는 비동기 스레드가 있다면 그만큼 코드는 더 넓어질테니 보기에도 좀 그렇겠죠.
그래서 이 문제를 해결할 수 있는 방법으로 나온 것이 Promise 객체라고 합니다.
작성법은 아래의 코드처럼 Promise 인스턴스를 하나 생성하는 것으로 시작이 됩니다.
let promiseInstance = new Promise();
그리고 excutor 함수를 Promise 인스턴스의 매개변수로 전달해 주어야 하는데요. 굳이 함수의 틀을 아래와 같이 지어주지 않고 임의로 지어도 상관 없으나, Promise 객체에 함수를 전해줄 때는 첫번째 매개변수는 성공을 의미하는 함수이고, 두번째 매개변수는 실패를 의미하는 함수를 의미하게 된다는 것만 기억하시면 됩니다.
function excutor(resolve, reject){ }
그리고 해당 함수가 실행에 성공하면, 즉 코드가 정상적으로 실행된 후 resolve를 만나면 resolve 안에 있는 값을 return 받아 then 키워드로 다음 동작을 지정할 수 있고, 정상적으로 실행되지 못했을 때에는 뒤에 정의했을 reject를 만나 reject 안에 있는 값을 return 받은 후 catch 키워드로 다음 동작을 지정할 수 있습니다.
let promiseInstance = new Promise(function executor(resolve, reject) { setTimeout(function() { // 성공과 실패를 랜덤하게 결정 let success = Math.random() > 0.5; // 50% 확률로 성공 또는 실패 if (success) { console.log("작업 성공"); resolve("성공 메시지"); // 작업이 성공했음을 알림 } else { console.log("작업 실패"); reject("실패 메시지"); // 작업이 실패했음을 알림 } }, 1000); }); promiseInstance .then(function(result) { console.log("Then 블록:", result); // '성공 메시지' 출력 }) .catch(function(error) { console.error("Catch 블록:", error); // '실패 메시지' 출력 });
말로 설명하면 이해하기가 어려우니 코드로 살펴보도록 하죠.
// 실행할 함수 안에 Promise 인스턴스를 생성하고 그 안에 비동기 함수를 정의한 뒤 성공, 실패의 경우를 분기로 지어 각각 resolve와 reject가 실행되게 합니다. function firstTask() { return new Promise(function (resolve) { setTimeout(function () { console.log("첫 번째 작업 완료"); resolve(); }, 1000); }); } function secondTask() { return new Promise(function (resolve) { setTimeout(function () { console.log("두 번째 작업 완료"); resolve(); }, 1000); }); } function thirdTask() { return new Promise(function (resolve) { setTimeout(function () { console.log("세 번째 작업 완료"); resolve(); }, 1000); }); } // firstTask가 실행된 뒤 Promise 성공, 실패 여부를 가지고 있는 객체를 반환 받으면 처리 결과에 따라 then이나 catch가 실행될텐데요. // 성공(resolve)가 실행되면 두번째 함수를 실행하고 그 결과(프로미스 객체)를 반환하고, 실패하면 오류를 출력하고 다음 코드는 실행되지 않습니다. firstTask() .then(function (result) { return secondTask(result); }) .catch(function (error) { console.error("firstTask 오류:", error); }) .then(function (result) { return thirdTask(result); }) .catch(function (error) { console.error("secondTask 오류:", error); }) .then(function () { console.log("모든 작업 완료"); }) .catch(function (error) { console.error("thirdTask 오류:", error); });