본문 바로가기

자바스크립트

[인간 JS엔진 되기] 2-1. 비동기 - 콜백, promise, async/await

비동기 처리 함수 (콜백, promise, async/await) 유무에 대한 비교

function getDB() {
    let data;
    setTimeout( () => {
        data = 1000;
    }, 3000);
    return data;
}

function getDBCallback(callback) {
    let data;
    setTimeout( () => {
        data = 1000;
        callback(data)
    }, 3000);
}

function getDBPromise() {
    return new Promise((resolve) => {
        setTimeout( () => {
            let data = 1000;
            resolve(data)
        }, 3000);
    })
}

function getDBAwait() {
    return new Promise((resolve, reject)=> {
        setTimeout( () => {
            let data = 1000;
            resolve(data);
        }, 3000);
    });
}

async function main () {
    let data = getDB();
    data+=2;
    console.log('data의 값 : ', data);
    let dataCallback = await getDBCallback(function (data) {
        let value = data+2;
        console.log('dataCallback의 값 : ', value);
    });
    let dataPromise = await getDBPromise();
    dataPromise+=2;
    console.log('dataPromise의 값 : ', dataPromise);
    let dataAwait = await getDBAwait();
    dataAwait+=2;
    console.log('dataAwait의 값 : ', dataAwait);
}
main();
// data의 값 :  NaN
// dataCallback의 값 :  1002
// dataPromise의 값 :  1002
// dataAwait의 값 :  1002

 

 

위 코드는 db에서 데이터를 가져오는 상황을 구현함

getDB 메소드는 setTimeout 비동기 함수만 사용하고 나머지는 각각 콜백, Promise, async/await를 사용하여 비동기 작업을 구현했다

 

getDB 메소드는 setTimeout 비동기함수에 추가적인 비동기 처리 함수 없이 구현해서

작업의 순서가 의도와는 달라졌다. 그 이유는 비동기 함수인 setTimeout 함수가 3초 동안 대기하는 동안 완료될 때까지 기다리지 않고 다음 코드인 console.log()를 실행하였기 때문!

 

비동기 함수를 사용 할 경우 작업의 순서를 맞추는 것이 필수!

이를 해결하기 위해 비동기 처리를 위한 함수들을 적용했다.

콜백 함수 처리

먼저 getDBCallback(callback) 메소드의 경우 콜백 함수를 적용했다.

콜백 함수는 JS의 함수는 일급 객체 특성을 가지고 있는 것을 이용해 함수의 매개변수에 함수 자체를 넘겨, 함수 내에서 매개변수 함수를 실행하는 기법을 이용

비동기 방식은 요청과 응답의 순서를 보장하지 않는다. 따라서 응답의 처리 결과에 의존하는 경우에는 콜백 함수를 이용하여 작업 순서를 간접적으로 끼워 맞출 수 있음

 

 

Promise 함수 처리

getDBPromise 메소드는 Promise 함수를 사용해 비동기 작업을 처리했다.

Promise는 비동기 작업의 성공 또는 실패와 그 결과값을 나타내는 객체

Promise를 사용하면 비동기 작업을 쉽고 깔끔하게 연결할 수 있음

 

async/await 함수 처리

getDBAwait 메소드는 async/await 함수를 이용함, 비동기 논블로킹 동작을 동기적으로 처리 가능

promise.then()메소드를 대체하는 것이 아니라, promise.then() 메소드를 간결하고 명확하게 작성할 수 있게 해줌

 

Callback

아래 콜백함수를 작성하면 동기적으로 실행됨 

 

function increaseAndPrint3(n, callback) {
    const increased = n + 1;
    console.log(increased);
    if (callback) {
    callback(increased); // 콜백함수 호출
    }   
}

increaseAndPrint3(0, n => {
    increaseAndPrint3(n, n => {
        increaseAndPrint3(n, n => {
            increaseAndPrint3(n, n => {
                increaseAndPrint3(n, n => {
                    console.log('끝!');
                });
            console.log('네번째 끝')
        });
        console.log('세번째 끝')
        });
    console.log('두번째 끝')
    });
console.log('첫번째 끝')
});

// 1
// 2
// 3
// 4
// 5
// 끝!
// 네번째 끝
// 세번째 끝
// 두번째 끝
// 첫번째 끝

 

여기에 setTimeout 비동기 함수를 추가하여 비동기적으로 콜백 함수를 실행 할 수 있음

 

function increaseAndPrint(n, callback) {
    setTimeout(() => {
      const increased = n + 1;
      console.log(increased);
      if (callback) {
        callback(increased); // 콜백함수 호출
      }
    }, 1000);
  }

increaseAndPrint(0, n => {
    increaseAndPrint(n, n => {
        increaseAndPrint(n, n => {
            increaseAndPrint(n, n => {
                increaseAndPrint(n, n => {
                    console.log('끝!');
                });
            console.log('네번째 끝')
        });
        console.log('세번째 끝')
        });
    console.log('두번째 끝')
    });
console.log('첫번째 끝')
});




// 1
// 첫번째 끝
// 2
// 두번째 끝
// 3
// 세번째 끝
// 4
// 네번째 끝
// 5
// 끝!

df

 

 

 

Promise

콜백 헬이라고 불리는 지저분한 JS 코드의 해결책

promise : 내용이 실행은 되었지만 결과를 아직 반환하지 않은 객체

Then을 붙이면 결과를 반환함

실행이 완료되지 않았으면 완료된 후에 Then 내부 함수가 실행됨

 

Resolve(성공리턴값) → then으로 연결

Reject(실패리턴값) → catch로 연결

Finally 부분은 무조건 실행됨

 

 

function increaseAndPrint1(n) {
return new Promise((resolve, reject)=>{
    setTimeout(() => {
    const increased = n + 1;
    console.log(increased);
    resolve(increased);
    }, 1000)
})
}

increaseAndPrint1(0)
    .then((n) => increaseAndPrint1(n))
    .then((n) => increaseAndPrint1(n))
    .then((n) => increaseAndPrint1(n))
    .then((n) => increaseAndPrint1(n)); // 체이닝 기법

 

 

 

 

 

Promise vs async await

function findAndSaveUser (Users) {
    Users.findOne({})//findOne이 실행 완료되면
        .then((user)=> {//실행이 성공하면 그 결과가 user, then에서는 이 user를 받아서 실행
            user.name='zero';
            return user.save();
        })
        .then((user)=> {
            return Users.findOne({gender:'m'});
        })
        .then((user) => {
            //생략
        })
        .catch(err)
}

async function findAndSaveUser2(Users) {
    let user = await Users.findOne({});
    user.name = 'zero';
    user = await user.save();
    user = await Users.findOne({gender:'m'});
}

 

 

 

 

자바스크립트 이벤트 루프 동작 과정

싱글 스레드인 JS에서도 작업의 동시 처리를 지원할 수 있는 비결은 바로!

이벤트 루프가 JS 엔진과 브라우저의 웹 API를 연결하여 비동기적인 일 처리를 가능케 하기 때문

다만 모든 JS 코드를 비동기로 처리할 수 있는 것은 아니고 비동기 전용 함수만 처리 할 수 있음

 

만약 setTimeout 이 호출되면 Timer API라는 별도의 스레드에서 타이머 동작이 실행

fetch가 호출되면 Ajax API 스레드에서 네트워크 통신이 실행

 

이벤트 루프는 이 비동기 함수 작업을 Web API에 옮기는 역할을 하고 작업이 완료되면 콜백 큐(Queue)에 적재했다가 다시 JS 엔진에 적재해 수행시키는 일종의 '작업을 옮기는 역할' 만을 함

 

작업을 처리하는 주체는 JS 엔진과 Web API.

그래서 이벤트 루프는 Call Stack에 현재 실행중인 작업이 있는지 그리고 Task Queue에 대기 중인 작업이 있는지 반복적으로 확인하는 일종의 무한 루프만 돌고, 대기 작업이 있다면 작업을 옮겨주는 형태로 동작

 

setTimeOut 이 실행될 때, 내부 동작 과정

function bar() {
  setTimeout(() => {
  	console.log("Second")
  }, 500);
}

function foo() {
  console.log("First");
}

function baz() {
  console.log("Third");
}

bar();
foo();
baz();

 

(1) bar()함수가 호출되고 그안의 setTimeout()함수가 호출되어 Call Stack에 쌓임

(2) setTimeout() 함수의 매개변수에 할당된 콜백 함수를 Timer Web API에 전달 그리고 Timer Web API에서는 백그라운드로 5초를 기다림, 그리고 foo()함수가 호출되고 콘솔창에 First가 출력

왼쪽 (1) / 오른쪽 (2) 

 

 

(3) 5초 대기 시간이 만료되면서, 이벤트 루프는 Timer Web API에서 가지고 있던 콜백 함수를 Task Web API에서 가지고 있던 콜백 함수를 Task Queue로 옮김

(4) baz() 함수가 호출되고 콘솔창에 Third가 출력

 

 

Promise 내부 동작 과정 (Task Queue (Macro)와 Microtask Queue)

Callback Queue는 WebAPI가 수행한 비동기 함수를 넘겨받아 Event Loop가 해당 함수를 Call Stack에 넘겨줄 때까지 비동기 함수들을 쌓아놓는 곳임

위에서 Callback Queue의 종류에는 Task Queue, Microtask Queue 2가지가 있음

Promise 객체의 콜백이 쌓이는 곳은 Microtask Queue.

MicroTask Queue는 그 어떤 곳보다 가장 먼저 우선으로 콜백이 처리되게 됨