비동기 방식 - Callback Hell, Promise, Async/await

dev_jubby·2022년 9월 22일
0

Node.js

목록 보기
7/8

비동기 방식을 알아보고, 콜백 지옥을 해결하기 위한 방법인 Promise와 Async/await을 알아본다.



💡 동기(Synchronous) or 비동기(Asynchronous) 방식 ?

동기(Synchronous) 방식은 하나가 끝날 때까지 다음작업을 수행할 수 없는 방식으로, 데이터의 요청과 결과가 한 자리에서 동시에 일어나는 것이다.

비동기(Asynchronous) 방식은 동시에 일어나지 않는 것으로, 요청 후 응답을 기다리지 않고 다른 활동을 한다.



💡 콜백 (Callback) 함수?

함수가 끝난 뒤 실행되는 함수로, 함수를 만들 때 parameter로 함수를 받아서 사용한다.

콜백함수를 사용해서 코드를 작성해보았다.

console.log("start");

// cb: callback function
function login(id, pw, cb) {
    setTimeout(() => {
        console.log( '정보없음' );
        cb(id);
    }, 0);
}


const user = login("kim", "1234", user => {
    console.log(user + "님 반갑습니다.");
});


console.log("finish");

위 코드를 실행하면 아래와 같은 결과가 나올 것 같지만 그렇지 않다.

start
정보없음
kim님 반갑습니다.
finish

실제 결과는 이렇게 나온다.

start
finish
정보없음
kim님 반갑습니다.

setTimeout()는 비동기적으로 호출되는 함수이기 때문이다. 따라서 실행되는 과정은 다음과 같다

  • 처음 console.log("start"); 를 실행한다.
  • login()을 실행하는데, 비동기적으로 실행되는 setTimeout()를 만나, 바로 다음 코드를 처리한다.
  • 그래서 console.log("finish")를 실행한 후에야 비동기적 코드가 값을 반환하게 된다.


💡 콜백 지옥(Callback Hell)?

콜백지옥(callback hell)이란 비동기 처리에 함수의 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기가 깊어지는 현상으로, 가독성이 떨어지고 코드 수정하기가 어려워진다.


다음 예제를 보자

function call( name, cb ){
    setTimeout( function(){
        console.log( name );
        cb( name );
    }, 1000 );
}
function back( cb ){
    setTimeout( function() {
        console.log( "back" );
        cb( "back" );
    }, 1000 );
}
function hell( cb ){
    setTimeout( function() {
        cb( "callback hell" );
    }, 1000 );
}

call( 'Lee', function( name ){
    console.log( name + "반가워" );
    back( function( txt ) {
        console.log( txt + "을 실행했구나" );
        hell( function( message ){
            console.log( "여기는 " + message );
        } );
    });
});

실행 결과

Lee
Lee반가워
back
back을 실행했구나
여기는 callback hell

콜백 함수가 3번 반복되면서 코드의 들여쓰기가 계속해서 깊어지는 것을 확인할 수 있다. 이와 같은 복잡도 증가와 예외처리의 어려움을 해결하기 위해 만들어진 것이 바로 프로미스(Promise) 다.



💡 프로미스(Promise) ?

Promise는 비동기 처리 작업에서 성공과 실패를 분리해 메소드를 수행한다. 즉 Promise는 상태를 갖는다.

  • resolve(value) : 작업이 성공적으로 끝난 경우
    👉 프로미스를 실행한 곳의 then으로 들어간다.
  • reject(error) : 작업이 성공적이지 않은 경우(에러 발생 시)
    👉 프로미스를 실행한 곳의 catch로 들어간다.
const func1 = new Promise( ( resolve, reject ) => {
    flag = true;
    if (flag) resolve("성공");
    else reject("실패");
})

//func1 실행값이 then에 들어온다.
func1.then( value => {
    return value + "1";
}).then( result => {
    //첫번째 then에서 리턴된 값이 여기 온다.
    console.log(result);
}).catch( err => {
    console.log( err );
});

실행 결과

성공1

promise의 상태

  • Pending : Promise를 수행 중인 상태
  • Fulfilled : Promise가 reslove 된 상태
  • Rejected : Promise가 지켜지지 못한 상태, Reject 된 상태
  • Settled : fulfilled 혹은 rejected로 결론이 난 상태

위에서 보았던 callback Hell 예제를 Promise를 사용해서 아래처럼 바꿀 수 있다.

function call(name) {
    return new Promise( function (resolve, reject) {
        setTimeout( function() {
            console.log(name);
            resolve(name);
        }, 1000);
    })
}

function back() {
    return new Promise( function (resolve, reject) {
        setTimeout( function() {
            console.log("back");
            resolve("back");
        }, 1000);
    })
}

function hell() {
    return new Promise( function (resolve, reject) {
        setTimeout( function() {
            resolve("promise");
        }, 1000);
    })
}


call('Lee')
	.then( function(name) {
      	console.log( name + "반가워");
        return back();
	})
  	.then( function(txt) {
      	console.log( txt + "을 실행했구나");
      	return hell();
	})
    .then( function( message ) {
  		console.log( "여기는 " + message);
  	});

실행 결과

Lee
Lee반가워
back
back을 실행했구나
여기는 promise

다른 예제를 한 번 더 보자

function login(id, pw, cb) {
    setTimeout(() => {
        console.log("사용자 입장");
        cb(id);
    }, 3000);
}

function getVideo(id, cb) {
    setTimeout(() => {
        cb( ['아이언맨1', '아이언맨2']);
    }, 2000);
}

function getDetail( video, cb ) {
    setTimeout(() => {
        cb("비디오 제목은 : " + video);
    }, 1000);
}

login('lee', '1234', user => {
    console.log("user님 환영");
    getVideo( user, (videos) => {
        console.log( videos );
        getDetail(videos[0], title => {
            console.log(title);
        });
    });
});

실행 결과

사용자 입장
user님 환영
[ '아이언맨1', '아이언맨2' ]
비디오 제목은 : 아이언맨1

Callback 방식으로 작성하니 역시 들여쓰기가 깊어진다.
위 코드를 Promise 방식으로 바꿔보자

function login(id, pw) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            console.log("사용자 입장");
            resolve(id);
        }, 3000);
    })
}

function getVideo(id) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            resolve( ['아이언맨1', '아이언맨2']);
        }, 2000);
    })
}

function getDetail(video) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            resolve("비디오 제목은 : " + video);
        }, 1000);
    })
}

login( 'Lee', '1234' )
    .then( function(user) {
        console.log('user님 환영');
        return getVideo(user);
    })
    .then( function(videos) {
        console.log( videos );
        return getDetail( videos[0] );
    })
    .then( function(title) {
        console.log( title );
    });

실행 결과

사용자 입장
user님 환영
[ '아이언맨1', '아이언맨2' ]
비디오 제목은 : 아이언맨1


💡 async & await ?

프로미스 기반 코드를 좀 더 쓰기 쉽고 읽기 쉽게 하기 위해 나온 것으로, Javascript의 비동기 처리 패턴 중 가장 최근에 나온 문법이다.(ES2017)

async

  • 함수 앞에 붙여 Promise를 반환한다.
  • 프로미스가 아닌 값을 반환해도 프로미스로 감싸서 반환한다.

await

  • '기다리다'라는 뜻을 가진 영단어
  • 프로미스 앞에 붙여 프로미스가 다 처리될 때까지 기다리는 역할을 하며, 결과는 그 후에 반환한다.

위 프로미스 예제를 async/await로 변환해보자

function login(id, pw) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            console.log("사용자 입장");
            resolve(id);
        }, 3000);
    })
}

function getVideo(id) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            resolve( ['아이언맨1', '아이언맨2']);
        }, 2000);
    })
}

function getDetail(video) {
    return new Promise( function (resolve, reject ) {
        setTimeout( function() {
            resolve("비디오 제목은 : " + video);
        }, 1000);
    })
}

async function exec() {
    let user = await login('Lee', '1234');
    console.log('user님 환영');
    let videos = await getVideo(user);
    console.log( videos );
    let title = await getDetail(videos[0]);
    console.log(title);
}
exec();

실행결과

사용자 입장
user님 환영
[ '아이언맨1', '아이언맨2' ]
비디오 제목은 : 아이언맨1
profile
신입 개발자 쥬비의 기술 블로그 입니다.

0개의 댓글