minzzl

[Javascript] 비동기처리 callback, promise, aync/await 본문

프로젝트/자바스크립트

[Javascript] 비동기처리 callback, promise, aync/await

minzzl 2022. 10. 9. 15:41
728x90
반응형

Javascript는 기본적으로 hosting을 제외하면 동기적인 언어입니다.

 

여기서 동기적이라는 것은 위에서 아래로 순차적으로 실행되는 것을 의미합니다.

console.log('1');
console.log('2');
console.log('3');
// 1
// 2
// 3

만약 코드를 실행하는데 동기 적으로 처리해야한다면 어떤일이 발생할까요?

짜장면을 배달하고 손님이 다 먹은 후 그릇을 가져오는 로직이 있다고 가정해봅시다.

해당 로직의 경우 손님이 다 먹을 때 까지 기다려야하므로 효율성이 아주 떨어집니다.

 

이렇게 행동해야한다면 너무 비효율적이겠죠? 그래서 이 때 나온 개념이 비동기입니다. 

앞선 예시를 비동기적으로 처리한다면 짜장면 배달 후 손님이 다 먹을 때까지 기다리는 것이아니라, 뒤에 해야할 일들을 우선 적으로 처리 후에 손님이 다 먹은 경우 그릇을 가져오도록 처리할 수 있습니다. 

 

정리해보자면 동기와 비동기의 개념은 다음과 같습니다.

  • 동기 : 특정 코드를 수행 완료 한 후 다음 코드를 실행
  • 비동기 : 특정 코드를 수행하는 도중 다음 코드를 실행 

 

비동기처리에 사용되는 대표적인 예시가 callback 함수인데요, 

Callback 함수

- 함수에서 매개 변수로 넘겨준 함수를 의미

 

매개 변수로 넘겨받은 함수는 때가 되면 나중에 called back 한다는 것이 콜백함수의 개념입니다.

또 다른 함수를 만들 때 인풋(parameters)을 함수로 받아서 사용할 수 있는데, 이 때 인자로 사용되는 함수를 말합니다.

callback 함수는 비동기적일 수도, 동기적일 수도 있는데 이는 이를 파라미터로 하는 함수에 따라달라집니다.

 

아래의 코드는 동기적으로 작동하는 callback 입니다.

function checkGang(count, link, good) {
  count < 3 ? link() : good();
}

function linkGang() {
  console.log('1일 3깡은 기본입니다. 아래 링크를 통해 깡을 시청해주세요');
  console.log('https://youtu.be/xqFvYsy4wE4');
}

function goodGang() {
  console.log('오늘 할당량은 모두 채우셨습니다! :)')
}

checkGang(2, linkGang, goodGang);

 

비동기적으로 작동하는 callback으로는 setInterval에서 사용될 때를 예로 들 수 있습니다. 

const arr = ['무','야','호'];

const printArray = () => {
	console.log(arr.shift());
    if (!arr.length) {
    clearInterval(timer)}
};

const timer = setInterval(printArray, 1000)
console.log('waiting')

/*출력
waiting
무
야
호
*/

 

그렇다면 callback 함수는 왜 필요한 것일까요?

 

우선 callback 함수는 가독성 및 코드 재사용 면에서도 활용할 수 있습니다.

function add(a, b) {
  return a + b;
}

function sayResult(value) {
  console.log(value);
}

sayResult(add(3, 4));

 

해당 코드를 다음과 같이 callback 함수를 이용하여 가독성을 높일 수 있습니다.

function add(a, b, callback) {
  callback(a + b);
}

function sayResult(value) {
  console.log(value);
}

add(3, 4, sayResult);

 

뿐만아니라, callback을 쓰는 이유에는 동기/비동기 처리, 변수의 유효범위와도 밀접한 관련이 있습니다.

 

(1) 비동기 처리

 

우선 자바스크립트는 싱글 스레드 언어입니다.

자바스크립트의 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어하고합니다. 하지만 루프만 독립적으로 실행되지 않고 웹 브라우저나 NodeJS 같은 멀티 쓰레드 환경에서 실행되기 때문에 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아닙니다.

 

싱글 쓰레드로 어떻게 한번에 여러 요청을 처리하는 것일까요?

기존의 동기식 요청은 코드를 한줄 한줄 차례대로 실행합니다. 그래서 하나의 작업에 걸리는 시간에 관계 없이 첫번째 코드가 실행된 후 다음 코드가 실행됩니다. 이렇게 되면 앞의 작업 시간이 길수록 시간 및 자원의 낭비가 심해지는 것이죠. 하나의 요청이 완료될 때 까지 기다리지 않고 동시에 다른 작업을 실행하는 비동기 호출로 이를 극복할 수 있습니다.

 

즉 자바스크립트는 싱글 쓰레드로 동작하며 비동기 작업을 통해 여러 요청을 처리합니다. 

그럼 자바스크립트의 비동기 런타임 과정을 알아봅시다.

 

자바스크립트가 비동기 코드를 어떻게 동작시키는지 알아보기 위해서 일단 자바스크립트 런타임 환경에 대해 알아야합니다. 

간단하게 비동기를 동작하는데 필요한 요소들을 살펴보겠습니다 :)

 

 

 

자바스크립트가 실행될 때는 다음과 같은 요소들이 실행을 도와줍니다.

  • Call Stack: 자바스크립트에서 수행해야 할 함수들을 순차적으로 스택에 담아 처리
  • Web API: 웹 브라우저에서 제공하는 API로 AJAX나 Timeout등의 비동기 작업을 실행
  • Task Queue: Callback Queue라고도 하며 Web API에서 넘겨받은 Callback함수를 저장
  • Event Loop: Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김

코드를 통해 비동기 코드가 동작하는 과정을 살펴보겠습니다.

 

setTimeout(() => console.log('async chanyeong'));
console.log('hello chanyeong');

// hello chanyeong
// async chanyeong

 

 

1.  setTimeout 함수가 실행되며 Call Stack에 setTimeout 함수가 추가됩니다.

 

 

2. setTimeout 함수는 자바스크립트 엔진이 처리하지 않고 Web API가 처리합니다. 

 

3. setTimeout함수는 Web API의 Timeout작업을 요청한 시간이 지나면 callback함수를 Task Queue로 전달한다.

 

 

 

3. 두 번째 라인에 작성한 console.log가 Call Stack에 추가된다. 그리고 Call Stack의 console.log가 실행되며 콘솔에는 'hello chanyeong'이라는 문자열이 출력된다

 

 

4. 자바스크립트의 Event Loop는 Call Stack이 비어있는지 항상 확인하는데 방금 console.log가 실행되며 Call Stack이 비워진 것을 확인한다.

 

5. Call Stack이 비워진 것을 확인한 Event Loop는 Task Queue에 있던 callback함수를 Call Stack으로 옮겨 작업을 수행한다. 콘솔에는 'async chanyeong'이 추가로 출력된 것을 볼 수 있다

 

 

 

 

이 처럼 단일 스레드인 자바스크립트는 비동기 처리 방식을 통해 여러개의 요청을 처리 할 수 있습니다,

그렇다면 싱글 스레드인 자바스크립드가 비동기가 가능한 이유는 무엇일까요?

 

이는 앞에서 설명했던 이벤트 루프 덕분입니다.

이벤트루프는 자바스크립트가 아닌 브라우저에 내장되어 있는 기능 중 하나인데요, 

 

우선 아래는 브라우저 환경을 나타낸 그림입니다. 

출처 :&nbsp;https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5

 

그림에서도 알 수 있듯이 이벤트 루프는 자바스크립트가 아닌 브라우저에 내장되어 있는 기능 중 하나입니다. 

즉 자바스크립트는 싱글 스레드이지만 브라우저에서는 이벤트 루프 덕분에 멀티 스레드로 동작하며 비동기 작업이 가능합니다.

 

용어
메모리 힙: 메모리 할당이 일어나는 곳
콜 스택: 힙에 저장된 객체를 참조하여, 호출 된 코드(함수)의 정보를 저장하고 실행하는 곳
태스크 큐(Task Queue): setTimeout(), setInterval()과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 대기하는 곳이다.

이벤트 루프는 실행 할 함수를 관리하는 역할로 콜 스택과 큐의 함수를 계속 확인 합니다. 만약 콜 스택이 비어있고 큐에 대기 중인 함수가 있다면 순차적으로 큐에 대기중인 함수를 콜 스택으로 이동시킵니다. 이렇게 반복되는 매 순회를 tick 이라고 합니다.

 

callback이 실질적으로 비동기처리 되는 것은 서버와의 통신에서 입니다.

ajax()를 통해 서버와의 통신이 필요하다면, 응답을 받는 데까지의 처리 시간이 필요할 것입니다. 만약 callback을 통한 비동기처리가 없이 동기적으로, 서버에게 요청에 대한 응답을 받지 못하였는데 아래의 코드들이 실행된다면 분명 문제가 발생할 것이 자명합니다. 

 

그러나 callback이 좋다고만 할 수 없습니다. 비동기 호출이 자주 일어나는 프로그램의 경우 콜백 지옥이 발생하기 때문입니다.

콜백 지옥이란 함수의 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들어질 정도로 깊어지는 현상입니다.

class UserStorage {
    loginUser(id, password, onSuccess, onError){
        setTimeout(() => {
            if (
                (id === 'ellie' && password === 'dream') ||
                (id === 'coder' && password === 'academy')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
            }
        }, 2000);
    }

    getRoles(user, onSuccess, onError) {
        setTimeout(() => {
            if(user === 'ellie') {
                onSuccess({name:'ellie', role: 'admin'});
            } else {
                onError(new Error('no access'));
            }
        }, 1000)
    }
}
const userStorage = new UserStorage();
const id = prompt('enter your id')
const password = prompt('enter your password')
userStorage.loginUser(
    id, 
    password, 
    user => {
        userStorage.getRoles(
            user, 
            (userWithRole) => {
                alert(`hello ${userWithRole.name}, you have a ${userWithRole.role} role`)
            },
            (error) => {
                console.log(error)
            }
        )
    },
    error => {
        console.log(error)
    }
)

다음과 같은 콜백 지옥이 발생할 경우 가독성이 떨어짐은 물론, 디버깅에도 어려움을 겪습니다. 

그렇다면 우리는 어떻게 이러한 콜백 지옥을 벗어나 비동기적으로 처리 할 수 있을까요?

 

Promise

- 자바스크립트 비동기 처리에 사용되는 객체

 

Promise는 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있는 기능을 가지고 있습니다. 또한 작업 결과 따라 성공 또는 실패를 리턴하며 결과 값을 전달 받을 수 있습니다.

 

 

Promise 상태 및 처리 흐름

pending(대기) : 처리가 완료되지 않은 상태
fulfilled(이행) : 성공적으로 처리가 완료된 상태
rejected(거부) : 처리가 실패로 끝난 상태

Promise 객체가 비동기 함수의 처리 상태를 보고 완료되었는지 판단하여 성공 여부에 따라 다음 처리를 다르게 수행할 수 있도록 해 줍니다.

 

promise 처리 흐름

 

Promise 예제 1

function arriveAtSchool_tobe() {
    return new Promise(function(resolve){
        setTimeout(function() {
            console.log("학교에 도착했습니다.");
            resolve();
        }, 1000);
    });
}

goToSchool();
arriveAtSchool_tobe().then(function(){
    study();
});

arriveAtSchool_tobe 함수는 Promise 객체를 리턴하는데 Promise는 then이라는 메서드를 가지고 있고 그 메서드 파라미터에 콜백 함수를 대입하면 앞서 resolve()라고 정의했던 구문이 실행이 되는 구조입니다.

구현의 차이는 있지만 아직까진 직접 콜백 함수를 작성한 것과 의미상으로 큰 차이는 없어 보입니다.

 

Promise 예제 2

 

Promise 객체는 2가지의 콜백 함수를 가집니다.
하나는 앞서 언급한 fulfilled 상태에서 실행되는 resolve 함수이고 다른 하나는 rejected 상태일 경우 실행되는 reject 함수입니다.

function arriveAtSchool_tobe_adv() {
    return new Promise(function(resolve, reject){
        setTimeout(function() {
            var status = Math.floor(Math.random()*2);
            if(status === 1) {
                resolve("학교에 도착했습니다.");
            } else {
                reject("중간에 넘어져 다쳤습니다.");
            }
        }, 1000);
    });
}

function cure() {
    console.log("양호실에 가서 약을 발랐습니다.");
}

goToSchool();
arriveAtSchool_tobe_adv()
.then(function(res){
    console.log(res);
    study();
})
.catch(function(err){
    console.log(err);
    cure();
});

성공 시에는 then 메서드가 실행되어 resolve 함수를 통해 넘겨준 문장을 실행하게 됩니다. 만약 실패하여 reject 함수가 실행되는 경우는 catch 메서드를 통해 reject 콜백 함수를 수행하게 됩니다.
reject일 때는 then 메서드가 실행되지 않으므로 성공, 실패를 각각 분리해서 처리할 수 있게 되는 것입니다.
콜백 함수 작성 방법에서도 콜백 함수를 2개 넘겨주면 비슷하게 처리가 가능합니다.

 

Promise 장점

1. Promise 연결하기(체이닝)

add(1,1)
.then(function(res){ // res: 2
    return res + 1; // 2 + 1 = 3
})
.then(function(res){ // res: 3
    return res * 4; // 3 * 4 = 12
})
.then(function(res){ // res: 12
    console.log(res); // 12 출력
});

then 메서드에서 값을 return 키워드를 사용하면 결과 값이 기본 자료형이 아닌 Promise 객체로 반환되기 때문에 이와같은 체인 형식이 가능하게 됩니다.

각각의 함수가 Promise 객체를 리턴하는 비동기 작업이라고 가정한다면 위와 같이 then 메서드를 연속적으로 사용하여 순차적인 작업을 할 수도 있습니다. 즉 비동기적인 작업들을 동기적으로 처리할 수 있습니다.

 

체이닝 기법을 활용함으로써 콜백 함수 사용 시 발생할 수 있는 콜백지옥에서 탈출이 가능합니다.

 

2. Promise 예외처리

goToSchool()
.then(function(){
    return arriveAtSchool();
})
.then(function(){
    return studyHard();
})
.then(function(){
    return eatLunch();
})
.catch(function(){
    leaveEarly();
});

연속적인 then 부분을 실행하다가 어느곳에서든 예외 상황이 발생하더라도 마지막 catch 구문에서 한번에 처리가 가능합니다. 이경우 최초 발생하는 rejected 상태의 작업만 처리하고 구문을 빠져나오게 됩니다.

 

 

뿐만아니라 Promise 객체를 좀 더 쉽게 다룰 수 있게 고안된 문법인 async / await 도 있습니다.

Async / Await

Async

async function greet() {
    return 'hello';
}

greet().then(console.log); // hello
console.log(greet()); // Promise 객체 출력

async 함수에서는 Promise가 아닌 값을 리턴하더라도 resolved promise가 반환됩니다.

Await

앞에 async / await 키워드만 붙여주면 비동기 작업의 순차 처리가 일반 순차 프로그래밍과 동일하게 가능합니다.

function greet() {
    return new Promise(function(resolve){
        setTimeout(function() {
            resolve('hello');
        }, 1000);
    });
}

(async function() {
    var result = await greet(); //resolved 될 때까지 대기
    console.log(result);
})();

promise.then을 사용하는 것보다 훨씬 간결하여 쓰기도 편하고 가독성도 뛰어 납니다. async로 선언된 함수 안에서만 사용이 가능합니다. await 키워드를 사용하더라도 그 작업이 처리되는 동안 다른 모든 프로세스가 중단되는 것은 아니며 엔진이 다른 일을 할 수 있으므로 자원이 비효율적으로 운영되지 않습니다.

 

 

 

 

 

 

 

* 아래의 글들을 참고하여 작성하였습니다.

https://sangminem.tistory.com/479

https://sangminem.tistory.com/284

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98

728x90
반응형

'프로젝트 > 자바스크립트' 카테고리의 다른 글

[Javascript] 이벤트 버블링과 캡처링  (0) 2022.10.09
[Javascript] this  (0) 2022.10.09
[Javascript] JSON  (0) 2022.10.09
[Javascript] Array  (0) 2022.10.09