본문 바로가기

TIL ( CODESTATES)

callback, promise, async & await 비동기 흐름 제어

callback function

 

: 함수가 인자로 다른 함수를 전달하는 것을 의미한다.

function greeting(name){
  alert('Hello ' + name);
}

function userInput(callback) { // 인자로 다른 함수를 받는다.
  let name = prompt('Please enter your name.');
  callback(name);
}

userInput(greeting)

위 함수를 실행하면 

 

 

please enter your name 이라는 입력창이 나와서 name을 입력할 수 있다.

 

 

코드에서 보면

let name = prompt('Please enter your name.')

이라는 부분을 실행한 것이다.

 

 

그리고 name으로 Kate를 입력하면

callback(name) 즉, greeting(name)을 실행해서 

Hello Kate 을 출력하는 것을 확인할 수 있다.

 


위에서 본 callback의 예제는 syncronous callback으로 즉시 실행된 것을 확인할 수 있다.

 

하지만 이런 콜백은 하나의 동기적인 동작이 완료되면 계속해서 코드를 실행하는 방식으로 잘 사용되는데 이것이 바로 asyncronous callback 이다.

좋은 예로, 한 promise가 성공하거나 실패한 이후 그 promise 의 마지막에 .then()으로 chaining을 해 줄 수 있는데 이런 구조는 fetch() 와 같은 현대의 web API들에서 널리 쓰인다.

 

 

여기서부터는 비동기 흐름을 제어하는 세 가지 중요한 패턴에 대해 공부하려고 한다. 

 

const printString = (string) => {
    setTimeout( 
        () => {
            console.log(string)
        },
        Math.floor(Math.random() * 100) +1 
    )
}

const printAll = () => {
    printString("A")
    printString("B")
    printString("C")
}
printAll()
// A,B,C의 순서가 그냥 랜덤을 나옴

 

먼저, string 타입을 인자로 받아서 setTimeout을 랜덤으로 실행하는 printString이라는 함수가 있다고 가정한다.

printString() 을 세 번 실행하는 printAll()함수를 실행해 보면 A,B,C가 순서대로 나오지 않고 랜덤한 것을 확인할 수 있었다.

 

 

이러한 비동기의 흐름을 제어하는 첫 번째 방법이 callback 이다.

 


비동기 흐름을 제어하는 세 가지 패턴

1. CALLBACK

 

const printString = (string, callback) => {	// 두 번째 인자로 callback을 받는다.
    setTimeout( 
        () => {
            console.log(string)
            callback()	// 콜백함수를 호출한다.
        },
        Math.floor(Math.random() * 100) +1 
    )
}

const printAll = () => {
    printString("A", () => { // 'A'를 찍는 이 함수의 콜백은 여기부터 마지막 printString함수까지를 포함한다.
        printString("B", () => {
            printString("C", () => {})
        })
    })
}
printAll()
// A,B,C를 순서대로 찍는다.

 

callback 함수로 비동기를 핸들링할 때의 특징은

  • 에러를 다룰 수 있다.
  • 에러를 넘겨주는 함수가 있으면 callback을 처리할 때마다 에러를 처리해야 하는 문제가 있다.
  • callback hell (혹은 callback pyramid) 가 발생하여 가독성이 떨어진다.

 


 

2. PROMISE

 

const printString = (string) => { 
    return new Promise((resolve,  reject) => { // instance를 리턴한다.  
        setTimeout( 
            () => {
                console.log(string)
                resolve()	//promise는 resolve, reject 라는 인자를 받는 callback을 실행한다.
            },
            Math.floor(Math.random() * 100) +1 
        )
    })
    
}

const printAll = () => {
    printString("A")
    .then( () => {  // callback을 인자로 받지 않고 .then 으로 이어갈 수 있다.
        return printString("B")
    })
    .then( () => {
        return printString("C")
    })
}
printAll()
//A,B,C를 순서대로 찍는다.

 

  • 인자로 callback을 받지 않고 promise의 instance를 리턴해준다.
  • (promise 자체가 class 같은 거라서 new Promise로 인스턴스를 생성할 수 있다.)
  • promise가 resolve, reject 를 인자로 받아서 콜백을 실행한다.

 

callback 함수를 전달하던 고전적인 방식과는 다른  promise 의 장점

 

  • 기존 callback 하는 방식에서는 err를 넘겨주는 함수가 있을 때, 콜백 처리 시 마다 에러도 처리해 주었는데
    promise에서는 .then의 마지막 체인에서 .catch로 핸들링하도록 한다.
  • 콜백은 자바스크립트 event loop이 현재 실행중인 콜 스택을 완료하기 전에는 절대 호출되지 않는다.

  • 비동기 작업이 성공하거나 실패한 뒤에 then()을 이용해 추가한 콜백의 경우에도 현재 실행중인 콜 스택을 완료하기 전에는 절대 호출되지 않는다.

  • then()을 이용해 여러개의 콜백을 추가할 수도 있는데 각각의 콜백은 순서대로 실행된다.

  • Chaining을 통해 비동기 작업을 순차적으로 실행해야 하는 경우를 제어할 수도 있다.

 

단점으로는 promise hell이 형성될 수 있다.

예를 들어 아래처럼 promise화된 함수 4개가 있다고 가정한다.

function gotoCodestates() {
    return new Promise((resolve, reject) => {
        setTimeout( () => { resolve('1. go to Codestates')}, 100)
    })
}

function sitAndCode(){
    return new Promise((resolve, reject) => {
        setTimeout( () => { resolve('2. sit and code')}, 100)
    })
}
function getAJob(){
    return new Promise((reslove, reject) => {
        setTimeout( () => { resolve('3. I got a job!!')}, 100)
    })
}

 

위 함수를 순차적으로 실행하는 코드를 아래와 같이 만들면

 gotoCodestates()
 .then(data => {
     console.log(data)
     sitAndCode()
     .then(data=> {
         console.log(data)
         getAJob()
         .then(data => {
             console.log(data)
         })
     })
 })

 

promise hell이 발생하여 가독성이 떨어진다.

이런 문제를 수정하기 위해 함수 안에서 함수를 호출하지 않고 리턴하는 방식으로 promise chaining을 할 수도 있다.

 

 - Promise Chaining

gotoCodestates()
.then(data => {
    console.log(data)
    return sitAndCode(); // return을 통해 다음 비동기를 넘긴다.
})
.then(data => {
    console.log(data)
    return getAJob();
})

이렇게 promise chianing 방식을 활용하면 콜백도 피할 수 있고 가독성도 좋아진다.

 

 

 * More about Chaining...

순차적으로 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우가 있다. 이 때 chaining을 이용하여 해결할 수 있다.

 

then() 함수는 새로운 promise를 반환하고 각각의 promise는 체인 안에서 서로 다른 비동기 단계의 완료를 나타낸다.

이 때 중요한 것은 반환값이 반드시 있어야 한다는 것이다. 그렇지 않으면 콜백 함수가 이전 promise의 결과를 받을 수 없다.

 

- Chaining after a catch

chain에서 작업이 실패하더라도 새로운 작업을 수행하는 것이 가능한데 이는 매우 유용하다.

new Promise((resolve, reject) =>
  console.log('studying asyncronous');
  resolve();
})
.then( () => {
  throw new Error('something failed');
  console.log('may not be printed');
})
.then( () => {
  console.log('Do next, whatever happened before');
});



// output
studying asyncronous
Do next, whatever happened before

// somethid failed 에러가 rejection을 발생시켜서 'may not be printed' 는 출력되지 않았다.

 

 


3. ASYNC & AWAIT

 

*node 버전이 높으면 바로 사용할 수 있다고 한다.

*async function 내부에서만 사용할 수 있다. (즉, async 함수임을 반드시 명시해 주어야 쓸 수 있다.)

 

 

Syntax

[returnValue] = await expression;

 

  • expression : Promise 혹은 기다릴 어떤 값
  • returnValue : Promise에 의해 만족되는 값. Promise가 아닌 경우네는 그 값 자체를 반환한다.

await 문은 Promise가 fulfill 되거나 reject 될 때까지 async 함수의 실행을 일시정지하고,

  • Promise가 fulfill 되면 async 함수를 일시 정시한 부분부터 실행한다.
  • Promiserk reject 되면 await문은 reject된 값을 throw한다.

 

 

 

await로 비동기 함수들을 동기적인 프로그래밍인 것처럼 쓸 수 있다. 즉, async await을 이용해 promise를 일반 함수처럼 사용할 수 있다.

 

위 promise화된 함수들을 다시 예로 들어서, 

function gotoCodestates() {
    return new Promise((resolve, reject) => {
        setTimeout( () => { resolve('1. go to Codestates')}, 100)
    })
}

function sitAndCode(){
    return new Promise((resolve, reject) => {
        setTimeout( () => { resolve('2. sit and code')}, 100)
    })
}
function getAJob(){
    return new Promise((reslove, reject) => {
        setTimeout( () => { resolve('3. I got a job!!')}, 100)
    })
}

async과 await을 써서 아래처럼 적어줄 수 있다.

const result = async () => {
    const one = await gotoCodestates();
    console.log(one)
    const two = await sitAndCode();
    console.log(two)
    const three = await getAJob();
    console.log(three)
}
result();

각 함수는 'await' 해 준 부분에서 실행을 잠시 정지하고 기다렸다가 promise가 resolve 되면(result() 를 찍으면) 실행된다.

 

각 함수의 delay가 gotoCodestates()에서는 100, sitAndCode에서는 300, getAJob()에서는 400으로 지정하더라도 A,B,C를 순서대로 찍는다.

 

 

javascript.info

 

Async/await

 

javascript.info

 

 

참고해서 볼 자료: 캡틴판교