상세 컨텐츠

본문 제목

JavaScript | 비동기 처리 | Promise 객체

프로그래밍 언어/JavaScript

by hyuga_ 2024. 1. 1. 14:55

본문

동기 처리, 비동기 처리

자바스크립트는 기본적으로 싱글 스레드 언어이다. 

  • 따라서 하나의 작업 흐름을 따라 처리한다. 그러므로 이전 작업이 진행 중이라면, 다음 작업을 수행하지 않고 기다린다.
  • 이를 동기적 방식, 혹은 blocking 방식 이라고 부른다. 

 


이렇게 했을 때의 문제점이 있는데, 바로 실행 시간이 긴 하나의 함수를 뒤의 함수가 계속해서 기다리는 경우가 생긴다는 것이다. CPU 스케줄링에서 FIFO 방식의 부작용을 그대로 상상하면 된다. 사용자 경험을 위한 언어인 자바스크립트 특성상 이는 치명적인 단점이다.

  • 따라서 비동기적 방식, Non-blocking 방식이 도입되었다.
  • 싱글 스레드이지만 마치 병렬처리를 하는듯한 착시를 일으키자는 것!
  • 비동기식 프로그래밍에서는 특정 작업이 백그라운드에서 실행되며, 그 작업의 완료를 기다리지 않고 다음 코드로 넘어갈 수 있다. 이를 통해 애플리케이션이 더 효율적으로 동작하고 사용자 경험이 개선될 수 있다.
  • 이때 call back 함수를 통해 함수간 실행의 순서를 보장할 수 있다. 

 

자바스크립트는 비동기 코드를 어떻게 핸들링할까?

  1. 자바스크립트는 비동기 함수를 우선 Web APIs 로 빼놓는다.
  2. 그리고 비동기 함수를 제외한 Call Stack의 함수들을 먼저 실행하고 있는다. 
  3. 비동기 함수의 실행이 종료되면, 비동기 함수가 부르는 콜백 함수를 Callback Queue로 이동시킨다. 
  4. Callback Queue와 콜 스택 사이에는 이벤트 루프가 돌고있는데, 이는 지속적으로 Call Stack을 확인하며 Main 컨텍스트를 제외한 모든 호출된 함수가 종료되었는지를 검사한다. 
  5. 만일 모든 콜 스택이 비었다면, Callback Queue에서 FIFO 방식으로 콜백 함수들을 콜 스택에 넣어준다. 

 

 

 

실제로는 micro task, macro task 등 조금 더 복잡한 구조를 갖고 있지만, 코드를 처리하는 큰 그림은 이렇다고 보면 된다. 

 

 

JS Visualizer 9000

 

www.jsv9000.app

해당 사이트에서 원하는 코드를 시뮬레이션 해볼 수 있다. 

 

 

부작용: 콜백 지옥 (Callback hell)

다수의 비동기 처리를 순서대로 처리하는 경우가 있을 수 있다. 

예를 들어 비동기 함수 A, B, C ... 가 있는데 A의 결과값을 B가 필요로 하고, B의 결과값을 C가 필요로 하고 ... 등 

실제 서비스에서는 그런 상황이 많이 발생할 것이다. 

 

이러한 로직의 경우, A의 콜백함수로 B를 호출하고, B의 콜백함수로 C를 호출하는 식으로 구현할 수 있다.

 

function taskA(a, b, cb) {
    setTimeout(() => {
        const res = a + b;
        cb(res);
    }, 2000);
}


function taskB(a, cb) {
    setTimeout(() => {
        const res = a * 2;
        cb(res);
    }, 1000);
}

function taskC(a, cb) {
    setTimeout(() => {
        const res = a * 2;
        cb(res);
    }, 1500);
}

// 콜백을 중첩해서 다수의 비동기 처리를 순서대로 처리 -> 콜백 지옥 -> Promise 객체 등장 !!
taskA(1, 2, (a_res) => {
    console.log("A:", a_res);
    taskB(a_res, (b_res) => {
        console.log("B:", b_res);
        taskC(b_res, (c_res) => {
            console.log("C:", c_res);
        });
    });
});

console.log("End-of-code");

 

 

 

 

위와 같이 구현 가능하다. 

근데 실제 콜백이 콜백을 부르는 로직이 계속 중첩되면 다음과 같은 무시무시한 형태의 코드가 짜여질 수도 있다. 

 

https://medium.com/gousto-engineering-techbrunch/avoiding-callback-hell-97734e303de1

 

그냥 개발할 맛 뚝 떨어지는 비주얼... 극악의 가독성이다. 

그래서 보다 깔끔하게 코드를 작성하고, 효율적으로 로직을 관리하기 위해 Promise 객체가 도입되었다.

 

Promise

Promise는 자바스크립트에서 비동기 작업을 표현하기 위한 객체이다.

이 객체는 미래의 어느 시점에 결과를 제공하는 작업 그 자체를 나타낸다.

  • 이 결과는 보통 웹 API 요청, 파일 I/O 작업, 어떤 긴 작업의 완료 후에 발생하는 데이터, 오류 메시지 등이다.

 

Promise 객체 구성요소

Promise 객체는 내부적으로 주로 다음과 같은 구성 요소를 가지고 있다. 

 

1. 상태 (State): Promise의 현재 상태를 나타내며, 세 가지 중 하나이다:

  • pending: 초기 상태, 아직 결과가 결정되지 않음.
  • fulfilled: 작업이 성공적으로 완료됨 (성공).
  • rejected: 작업이 실패함 (오류 발생).

비동기로 처리할 특정 작업에 대해서, 저 3가지 상태에 따라 적절한 콜백함수를 호출함으로써 실행 흐름을 컨트롤할 수 있다.

 

 

2. 결과 (Result): Promise의 작업 결과이며, 결과는 다음 두 가지 중 하나이다:

  • undefined: 초기에는 결과가 undefined이다.
  • value 또는 reason: Promise가 fulfilled 또는 rejected 상태일 때, 작업의 결과 값 또는 오류 이유가 된다.

이는 결과값은 Promise 객체 수행 이후 다음 함수에 넘겨주거나 하는 식으로 쓰인다. 

 

 

3. 처리 메서드 (then, catch, finally): Promise에 대한 반응을 정의하는 메서드들이다. 

  • then: Promise가 성공적으로 완료되었을 때 실행할 콜백 함수를 등록한다. 이 메서드는 또 다른 Promise를 반환할 수 있어 체이닝이 가능하다.
  • catch: Promise가 실패하였을 때 실행할 콜백 함수를 등록한다.
  • finally: Promise의 결과와 상관없이 실행될 콜백 함수를 등록한다.

 

 

 

Promise 사용법 예시

/* 
[Promise 객체 생성하기]

1. 비동기 작업 자체인 Promise()를 저장할 asyncTask 상수를 선언
2. new Promise() 생성자를 이용하여 Promise 객체 생성 
3. Promise 생성자에 실행 함수(executor function)를 인자로 넘겨줌

*/
function isPositiveP(number) {
    // 실행자. 비동기작업을 실질적으로 수행
    const executor = (resolve, reject) => { // 결과에 따라 각기 다른 콜백함수 호출
        setTimeout(() => {
            if (typeof number === 'number') {                
                resolve(number >= 0 ? "양수" : "음수"); // 성공 -> resolve
            } else {                
                reject("주어진 값이 숫자형 값이 아닙니다."); // 실패 -> reject
            }
        }, 2000);
    }

    const asyncTask = new Promise(executor);
    return asyncTask;
}

/*
[Promise 객체 사용] 

1. Promise 객체의 비동기 처리 결과를 변수에 담기
2. Promise 객체에 사용 가능한 then, catch 함수로 비동기 처리 핸들링

*/

const res = isPositiveP(10);
res.then((res) => {
    console.log("success:", res);
}).catch((err) => {
    console.log("fail:", err);
});

 

Promise 객체 생성 방법: 

  1. 비동기 작업 자체인 Promise()를 저장할 asyncTask 상수를 선언
  2. new Promise() 생성자를 이용하여 Promise 객체 생성
  3. Promise 생성자에 실행 함수(executor function)를 인자로 넘겨줌

Promise 객체 사용 방법: 

  1. Promise 객체의 비동기 처리 결과를 변수에 담기
  2. Promise 객체에 사용 가능한 then, catch 함수로 비동기 처리 핸들링 (성공했을 경우 then, 에러 처리는 catch를 통해 해준다.)

 

Promise 체이닝

Promise 객체를 체인처럼 연결하는 방식을 사용하면 위에서 언급된 콜백 지옥을 해결할 수 있다. 

 

function taskA(a, b) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const res = a + b;
            resolve(res);
        }, 2000);
    })
}

function taskB(a) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const res = a * 2;
            resolve(res);
        }, 1000);        
    })
}

function taskC(a) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const res = a * 2;
            resolve(res);
        }, 1500);
    })
}


taskA(5, 1)
.then((a_res) => {
    console.log("A result:", a_res);
    return taskB(a_res); 
}).then((b_res) => {
    console.log("B result:", b_res);
    return taskC(b_res);
}).then((c_res) => {
    console.log("C result:", c_res);
})

/* 
(기존 콜백 지옥 함수)

taskA(1, 2, (a_res) => {
    console.log("A:", a_res);
    taskB(a_res, (b_res) => {
        console.log("B:", b_res);
        taskC(b_res, (c_res) => {
            console.log("C:", c_res);
        });
    });
});
*/

 

 

연쇄적인 비동기 작업이 많아질수록 Promise를 이용한 코드가 더 깔끔해진다. 

 

Promise를 이용한 위 코드의 동작 방식은 다음과 같다.  

  1. taskA의 리턴값 = Promise 객체
    • then으로 taskA의 Promise 객체 중 결과값(a_res)을 인자로 주면서 taskB 호출
  2. taskB의 리턴값 = Promise 객체
    • then으로 taskB의 Promise 객체 중 결과값(b_res)을 인자로 주면서 taskC 호출
  3. taskC의 리턴값 = Promise 객체
    • then으로 resolve 함수에 해당하는 console.log만 찍고 끝남. 

 

 

관련글 더보기