[자바스크립트] 비동기 처리방식을 수행하는 콜백함수, Promise, async/await

2021. 11. 15. 16:31JavaScript/study

1. 동기식 처리와 비동기식 처리란 무엇인가?

1.1 동기식 처리(Synchronous)

동기식 처리란 여러개의 작업 요청이 들어올 경우 요청이 들어온 순서대로 처리하는 방식을 의미합니다. 먼저 들어온 요청 작업이 수행중인 동안 다른 작업 요청들은 대기합니다. 예를 들어 마트에서 계산대가 1대만 있다고 가정할 경우 여려명의 손님들은 순서대로 줄을 서서 계산을 기다릴 것입니다. 즉, 정리하면 동기식 처리란 먼저 시작된 하나의 작업이 끝날 때까지 다른 작업을 시작하지 않고 대기하다가 작업이 완료되면 새로운 작업을 하는 방식입니다.

1.2 비동기식 처리(Asynchronous)

비동기식 처리란 여러개의 작업 요청이 들어올 경우 한 개의 요청이 수행되는 동안 다른 작업 요청들은 대기하지 않고 병렬적으로 각각의 작업 요청들을 수행하게 됩니다. 이러한 병렬 작업은 먼저 수행중인 작업 요청이 뒤에 수행중인 작업 요청보다 늦게 끝날 수도 있습니다. 예를 들어 마트에서 기존 계산대가 1대에서 여러개로 늘어나서 여러명의 손님들은 그동안 대기하지 않고 바로 계산을 수행할 수 있는 것과 같습니다. 즉, 정리하면 비동기식 처리란 동기식 방식과는 다르게 먼저 시작된 작업의 완료 여부와는 상관없이 새로운 작업을 시작하는 방식입니다.

 

2. 자바스크립트 비동기 처리의 종류

자바스크립트에는 아래와 같이 3가지 비동기 처리방식이 존재합니다.

  • 콜백 함수(Callback)
  • Promise
  • Promise 기반 async/await

위의 3가지 처리방식은 비동기적으로 동작하는 부분을 동기적으로 동작하도록 처리해주는 방식입니다.

 

2.1 콜백함수(Callback)

2.1.1 콜백 함수란 무엇인가?

  • 다른 함수의 인자로써 이용되는 함수
  • 어떤 이벤트에 의해 호출되어지는 함수

콜백 함수는 예를 들면, 레스토랑의 자리 예약과 같습니다. 레스토랑의 대기자 명단에 이름과 전화번호를 적은 다음 레스토랑 근처를 돌아다닙니다. 시간이 지난 후 레스토랑에서 문자 메시지로 자리가 났다고 연락이 올 것입니다. 여기서 전화번호를 남기고 근처를 돌아다니다 문자 메시지를 받는 것을 정리하면 아래와 같습니다.

  • 대기자 명단에 이름과 전화번호 남김 -> 서비스 호출
  • 레스토랑 근처를 돌아다님 -> 비동기 수행
  • 문자 메시지가 온 시점 -> 콜백함수 호출 시점

 

2.1.2 콜백 함수를 사용하지 않은 경우

function arrive(){
    console.log("1. 레스토랑 도착");
}
function book(){
    console.log("2. 자리 예약");
}
function wait(){
    setTimeout(function(){
        console.log("3. 자리 비었음");
    },3000);
    
}
function eat(){
    console.log("4. 식사");
}
arrive();
book();
wait();
eat();

처음 의도한 결과

1. 레스토랑 도착
2. 자리 예약
3. 자리 비었음
4. 식사

실제 결과

1. 레스토랑 도착
2. 자리 예약
4. 식사
3. 자리 비었음

처음 의도한 결과와는 달리 "4. 식사"가 먼저 출력된 것을 알 수 있습니다. 이는 setTimeout() 함수를 호출할때 3초간 대기하지 않고 바로 eat() 함수를 호출했습니다. 이는 자바스크립트가 비동기로 수행되기 때문입니다. 따라서 3초간 대기했다 eat() 함수를 호출하기 위해서 콜백 함수를 사용해야 합니다.

 

2.1.3 콜백 함수를 사용한 경우 예제

function arrive(){
    console.log("1. 레스토랑 도착");
}
function book(){
    console.log("2. 자리 예약");
}
function wait(callbackFunc){
    setTimeout(function(){
        console.log("3. 자리 비었음");
        callbackFunc();
    },3000);
    
}
function eat(){
    console.log("4. 식사");
}
arrive();
book();
wait(eat);
1. 레스토랑 도착
2. 자리 예약
3. 자리 비었음
4. 식사

위 예제와 같이 wait() 함수에 인자로써 eat() 함수를 넣음으로써 eat() 함수를 콜백 함수로 수행하게 합니다. 하지만 콜백 함수를 남용하게 되면 콜백 지옥에 빠지는 경우가 생길 수 있습니다.

 

2.1.4 콜백 지옥(Callback hell)이란 무엇인가?

콜백 지옥은 비동기 수행을 동기적인 수행으로 변경하기 위해 콜백 함수를 연속해서 사용할때 발생하는 문제입니다.

 

2.1.5 콜백 지옥 예제

function paraseValue(response, callbackFunc){
    setTimeout(() => {
        console.log("1. " + response);
        callbackFunc("id");    
    }, 3000);
    
}

function auth(id, callbackFunc){
    setTimeout(() => {
        console.log("2. " + id);
        callbackFunc("result");    
    }, 3000);
}

function display(result, callbackFunc){
    setTimeout(() => {
        console.log("3. " + result);
        callbackFunc("text");    
    }, 3000);
}

function func1(){
    paraseValue("response",function(id){
        auth("id", function(result){
            display("result", function(text){
                setTimeout(() => {
                    console.log("4. " + text);
                }, 3000);
            });
        });
    });
}
func1();
1. response
2. id
3. result
4. text

위 예제를 수행하면 3초 간격으로 순서대로 response->id->result->text가 수행되는 것을 볼 수 있습니다. 물론 콜백 함수를 사용하여 비동기적 수행에서 동기적인 수행으로 작동하였으나 func1() 함수의 내용은 콜백 지옥에 빠져 가독성이 떨어지고 수정이 어려워지는 것을 볼 수 있었습니다. 일반적으로 콜백 지옥을 해결하는 방법에는 대표적으로 Promise와 Async을 활용하여 해결할 수 있습니다.

 

2.2 Promise 객체

2.2.1 Promise 객체란 무엇인가?

Promise 객체는 비동기 처리방식 중에 하나입니다. 비동기 수행의 결과로 성공했을 때와 실패했을 때의 처리를 수행할 수 있습니다.

2.2.2 Promise 객체의 상태

Promise 객체는 다음 중 하나의 상태를 가집니다.

  • 대기(pending) : 이행하거나 거부되지 않은 초기 상태
  • 이행(fulfilled) : 연산이 성공적으로 완료됨
  • 거부(rejected) : 연산이 실패함

2.2.3 Promise 사용 방법

new Promise(function(resolve, reject){
	// ...
});

new Promise() 객체를 생성하여 콜백 함수를 선언 할 수 있습니다. function(resolve, reject) 함수가 콜백 함수입니다. 콜백 함수에 대한 설명은 아래와 같습니다.

  • resolve : 결과가 성공했을때 호출하는 함수
  • reject : 결과가 실패했을때 호출하는 함수

new Promise() 객체안에 콜백함수를 호출한 다음 결과가 성공이면 이후 then 메서드를 사용하고 실패라면 catch 메서드를 사용하여 처리합니다.

 

2.2.4 Promise 사용 예제

2.2.4.1 Promise 성공예제(then)

let bye = new Promise((resolve,reject)=>{
    console.log("1. hello");
    setTimeout(() => {
        resolve("2. bye");
    }, 3000);    
});
bye.then((result)=>{
        console.log(result);
        console.log("3. hello again");
    });
1. hello
2. bye
3. hello again

위 예제의 결과를 보면 3초간 대기하고 resolve() 함수를 호출하여 Promise 객체가 이행됨을 알립니다. 그리고 then안의 함수를 호출하여 3초간 대기 이후의 로직을 수행합니다. 즉, resolve 함수를 호출하면 then 함수의 인자로 들어있는 함수가 호출됩니다.

2.2.4.2 Promise 실패예제(catch)

let bye2 = new Promise((resolve, reject)=>{
    console.log("1. hello");
    let val = 1;
    if(val!==1){
        setTimeout(() => {
            resolve("2. bye");
        }, 3000);
    }
    else{
        reject(new Error("fail!!"));
    }
});
bye2.then((result)=>{
            console.log(result);
            console.log("3. hello again");
        })
    .catch((error)=>{
            console.log(error);
        });
1. hello
test.js:129 Error: fail!!
    at test.js:121
    at new Promise (<anonymous>)
    at test.js:112

위의 예제에서 일부로 val변수에 정수 1을 저장하여 실패를 유도하였습니다. reject() 함수를 호출하면 catch 함수의 인자로 들어있는 함수를 호출합니다.

 

2.2.5 Promise 방식이 콜백함수보다 좋은 이유

Promise방식이 콜백함수도 좋은 점은 콜백 지옥의 문제점인 가독성을 높혀주고 수정을 더 간단히 할 수 있게 합니다.

function func1(){
    return new Promise((resolve,reject)=>{
        resolve();
    });
}

function a(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log("call a");
            resolve();
        },3000);
    });
}

function b(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log("call b");
            resolve();
        },3000);
    });
}

function c(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log("call c");
            resolve();
        },3000);
    });
}

func1().then(a)
        .then(b)
        .then(c);
call a
call b
call c

콜백함수를 사용하면 길어지고 복잡해졌을 코드도 위와 같이 Promise 객체를 반환하면 여러 개의 then을 연결하여 간단히 처리할 수 있습니다.

 

2.3 Promise기반 async/await

async 키워드를 적용한 함수(function)은 암시적으로 Promise를 사용하여 결과를 반환합니다.

 

기존 Promise와의 차이점

  • resolve, reject, then, catch 함수를 사용하지 않음

async 함수 적용 형식

async function foo(){
	return "foo";
}

 

async 함수는 항상 Promise를 반환합니다. 만약 async 함수의 반환값이 명시적으로 promise가 아니라면 암묵적으로 promise로 감싸집니다.

// before
async function foo(){
	return "foo";
}

// after
function foo(){
	return Promise.resolve("foo");
}

2.3.1 async 사용 예제

async function foo(){
    return "foo";   // -> Promise.resolve("foo");
}

const result = foo();
console.log(result);
Promise { 'foo' }

위 예제의 실행 결과로 Promise 객체가 반환됩니다. 만약 Promise 객체안의 'foo'를 추출하고 싶다면 await을 같이 사용해야 합니다.

2.3.2 await 사용 예제

async function foo(){
    return "foo";   // -> Promise.resolve("foo")
}

async function func1(){
    const result = await foo(); // -> Promise.resolve("foo").then(()=>undefined);
    console.log(result);    // Expected Output : foo
}
func1();
foo

await 키워드를 사용하는 함수에는 async 키워드가 적용되어 있어야 합니다.

2.3.3 async/await 성공 처리 예제

async function foo(){
    return "foo";
}

async function func1(){
    try{
        const result = await foo();
        console.log("성공 : " + result);
    }catch(e){
        console.log("실패 : " + e.message);
    }
}
func1();
성공 : foo

2.3.4 async/await 실패 처리 예제

async function foo(){
    throw new Error("fail!!");
}

async function func1(){
    try{
        const result = await foo();
        console.log("성공 : " + result);
    }catch(e){
        console.log("실패 : " + e.message);
    }
}
func1();
실패 : fail!!

 

References

[1] https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/
[2] https://velog.io/@change/JavaScript-asyncawait%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C
[3] https://12soso12.tistory.com/64
[4] https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function