[비동기] 콜백 & Promise & async

수툴리 양·2021년 6월 26일
0
post-thumbnail

비동기

자바스크립트는 single-threaded 프로그래밍 언어이다. 그렇기 때문에 동기적이다. 동기적 synchronous이란 의미는 다음의 동치와 같다.
synchronous === one call stack === one piece of code at a time
하지만 우리는 이를 비동기적으로도 작동시킬 수 있다.
이 포스트에서 자바스크립트에서 코드를 비동기적(asynchronous)으로 작성하기 위한 3가지 방법을 학습 및 리뷰할 것이다: 콜백, Promise 객체, async&await

  • 콜스택(Call Stack) is basically a data structure which records where in the program we are.
    우선 콜 스택은 우리가 브라우저 등에서 '실행'시킨 즉 함수의 호출, 'invoke'관점에서 작동할 기능들이 들어오는 '스택'이다. 스타벅스에서 주문 후 "음료 제조 중"에 들어간 것이다.
    If we step into a func, we push sth on the stack,
    return from a func, we pop off the top of the stack
    • 'maximum call stack size exceeded': 코드를 작성하면서 이와 같은 에러메시지를 본 적이 있다. 콜백 안의 콜백으로 마치 엘레베이터 내 양쪽벽면이 거울로 둘러쌓여 무한대의 거울상이 생기는 것과 같다. 브라우저는 사용자에게 당신이 같은 콜백을 수천번, 수만번을 호출했을 리가 없으니 이를 끝내겠다고 오류로 뱉는 것이다.

  • blocking vs Non-blocking: 브라우저가 느려지는 이유는?
    만약 동기적으로 작동하는 코드베이스로 인해 콜 스택에 들어온 함수가 내부 콜백에 setTimeout으로 타이머가 걸려있어, 우리는 브라우저의 다른 부분을 클릭하거나 해당 페이지로 넘어가거나 등등 다른 수행을 할 수 없는 이러한 경우 콜 스택이 blocking 된 것이다.
    리액트 등으로 만드는 컴포넌트형 웹앱이 대세라고 하지만? 아직까지 한국에서는 페이지 전체를 reRender 하는 형태가 꽤나 남아있다.
    🔗 관련 글: 지메일이 핫메일을 이긴 진짜 이유 (Ajax가 가져온 유저 인터페이스의 혁신)
    개발자로서 코드의 최적화를 위한 사고는 "브라우저가 왜 느려질까?"라는 질문부터 시작된다.
    *setTimeout에서 걸어주는 타이머는 minimum을 걸어준 것이지, 0으로 한다고 바로 실행된다, 이런 의미로 활용하는 것은 아니다.

  • Event Loop 🔄

    이벤트 루프는 심플하게 딱 한가지의 역할을 한다. task queue에 쌓인 함수를 콜스택이 비었을 때 이제 실행하라고 넘겨주는 것이다.
    "queue"인 만큼 먼저 들어온 대기 기능(함수)가 먼저 나가는 선입선출이다.
    🔗 참고 영상: What the heck is the Event Loop? | JSConf EU

call stack ⇪(실행)
Event Loop 🔄 콜스택이 비었을 때 task queue에 대기하고 있던 실행할 내용을 콜스택으로 넘겨줌
task queue↩︎

💡 Question
Node.js는 single-thread인가?
그렇다면 Event Loop는 왜 필요한가?



❖ 콜백함수

자바스크립트에서 함수는 객체이다,
함수는 변수를 담은 채로 함수를 리턴할 수 있고(closure),
리턴할 함수를 인자로서 받아 다른 함수에 전달할 수도 있고,
변수에 할당할 수 있다.

함수가 인자로 콜백함수를 받아서 들어오는 배열의 각 요소나 객체의 key/value에 함수를 적용하여 그 결과를 성공 시 또는 실패 시(error) 결과를 되돌려 리턴해주는 것이라 'callback'이란 네이밍의 의미를 따져볼 수 있다.
따라서 콜백함수 안에 콜백함수, 또 그 안에 콜백함수..가 있을 수 있다.

callback hell case 🧻
예제로 다음과 같은 과정의 코드가 있을 경우,
1. 사용자에게 id, pw 받아오기
2. 로그인 시도
3. 로그인 성공 시 id 받아오고
4. 역할 받아오기(admin 등)
5. 성공적으로 받아온다면 사용자의 object를 갖게 되는 것

// class객체는 생략함
const userStorage = new UserStorage(); // class 만들고 서버와 통신
const id = prompt('enter your id'); // 사용자가 입력
const password = prompt('enter your pssword'); // 사용자가 입력
userStorage.loginUser(id, password, (user) => { // ⓐ콜백 사용자가 입력한 user정보를 다음 콜백에 넘김
    userStorage.getRoles(user, (userWithRole) => { // ⓑ콜백 이 콜백은 사용자의 Role정보를 받아오는 함수
        alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`)
    }, (error) => { // 에러가 날 경우 에러 핸들링(ⓑ콜백)
        console.log(error)
    })
}, (error) => { // 에러 핸들링(ⓐ콜백)
    console.log(error)
})

드림코딩by엘리 유튜브 무료강의 예제 참고

예제는 콜백 안에서 콜백을 한번 더 전달하는 두 겹의 중첩뿐이긴 하지만,
이렇게 콜백 안에서 콜백을 전달하고, 또 전달, 또 전달하게.. 되면 콜백지옥이 되는데, 이러한 코드 문제점: 가독성 현저히 낮고, 로직 파악이 불편하며 디버깅도 어려움, 유지보수에 꽝이다.
따라서 병렬적으로 작성할 수 있고, 네트워크와 효율적으로 통신할 수 있도록 Promise객체와 async & await를 사용하여 비동기 코드를 작성한다. (곧 비동기적으로 코드를 작성해야 하는 이유)


콜백함수를 인자로 받는 map, filter, forEach 등 배열 메소드
아래 gif 이미지에서 forEach를 async하게 만들어 실행하는 경우를 보여주고 있다.

앞서 말한 blocking 현상과 non-blocking을 여기서 확인할 수 있다.
비동기적으로 코드를 작성하면, 코드가 실행되는 사이사이마다 브라우저는 render할 수 있다.(render queue가 초록색으로 깜빡이는 것)



❖ Promise

Promise는 비동기를 간편하게 처리할 수 있도록 도와주는 "object"이다.

☝🏻 예를 들어 오픈될 수강에 대한 이메일 공지 알림받고싶어 이메일 구독 신청하는 경우, 뒤늦게 사전공지창을 발견했을 때 이메일 등록 시 수업은 이미 오픈되었으니 기다리지 않아도 바로 메일로 공지가 옴 : 이미 성공적으로 처리된 promise인 것!

promise 객체는 promise객체를 리턴한다. (처음 배울 때 코드를 직접 작성하는 기술,적인 면에서는 중요한 포인트라고 생각한다.)

const getDataPromise = path => {
return new Promise((resolve, reject) => {
  fs.readFile(path, "utf8", (err, data) => {
    if(err) {
    reject(err);
    }
    resolve(data);
  })
})
};

수행 결과 값이 있으면 resolve 콜백인자로 넘겨주고, .then 메소드로 resolve 콜백인자 또는 리턴 값을 콜백에 또 넘긴다.
콜백 결과 리턴에 실패 시, 즉 에러가 있으면 에러를 reject 콜백인자로 넘겨준다.
*.try/ .catch 메소드로 에러를 잡음.

*여기서 말하는 콜백은 콜백함수와 같이 작동하는 기능을 말함.
프로미스는 콜백을 쓰지 않고 비동기로 깔끔한 코드를 작성하도록 하는 객체이다.

Promise is a JavaScript object for asynchronous operation.

❗️ Two points

  1. 상태 state* : pending -> fulfilled (수행 성공 완료) or rejected
    :프로세스가 operation을 수행하고 있는 중인지 / 기능을 다 수행하여 성공했는지 실패했는지
    (정해진 장시간의 기능을 수행 후 기능이 정상적으로 수행되었으면 성공 메시지 + 처리된 결과값 전달하고(resolve), 수행 중 에러가 발생하면 에러를 전달(reject)한다.) *resolve 와 reject는 executor라고 부른다.
    리턴하는 것이 없으면 'pending' 상태

  2. producer(정보 제공) vs consumer(소비)
    : promise객체를 만든다. 이것이 producer이고, 이 promise 인스턴스를 사용하는 .then/ .catch/ .finally 메소드를 consumer로 본다.
    .then 등의 메소드를 통해 여러 다른 비동기를 묶어서 병렬적으로 처리가 가능한 것이다.

예제

// 1. producer
const promise = new Promise((resolve, reject) => { // 두가지 콜백이 또 인자로 들어옴 (resolve, reject : executor)
  // 파일을 읽거나(fs.readfile) 네트워크와 통신하는 등의 시간이 걸리는 작업 → 비동기적으로 처리(곧 비동기가 필요한 이유!)
    console.log('doing something...');

    setTimeout(() => {
        resolve('success'); // 수행 성공 시 resolve 콜백 호출(, 인자 전달)
    //  reject(new Error('no network'))
    }, 2000)
})
// 2. consumer
promise
    .then((value) => { // promise 값이 잘 수행되었으면 어떤 값(value)을 받아와서 (인자를 넘겨줘서) 콜백을 수행할 것
  // 여기서 value는 위 producer 코드의 resolve콜백을 통해 전달한 'success'
        console.log(value);
    }) // ! then은 성공한 수행결과값을 전달하거나, promise를 전달할 수 있음, 여기서는 똑같은 promise를 리턴함
    .catch(error => { // ! 그 리턴된 promise에 catch를 또 호출할 수 있음
        console.log(error);
    })
    .finally(() => { // 성공실패 여부 상관없이 마지막에 호출 가능. 인자 따로 받지 않아도 ok
        console.log('finally');
    });
// 이렇게 consumer(메소드)를 사용하여 promise를 chaining

드림코딩by엘리 유튜브 무료강의 참고

  • new 키워드로 Promise 인스턴스가 만들어지는 '순간', 콜백이 executing됨
    즉, 생성 즉시 실행 (hoisting?된다)
const getHen = () =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve('🐓'), 1000);
    });
const getEgg = hen =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${hen} => 🥚`), 1000); // ⓐ
    //  setTimeout(() => reject(new Error(`${hen} => 🥚`)), 1000); ⓑ
    });
const cook = egg =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${egg} => 🍳`), 1000);
    });

  • resolve로 넘겨준 것이 없어도, 따로 반환return하는 것을 지정해주면 메소드로 chaining 가능
getHen() //
    .then(getEgg)
    .catch(error => { // !직전에서 발생한 에러를 캐치하고 싶을 때는 바로 다음에 catch
        return '🥖 ' 
    }) // ! getEgg가 성공하지 않아도, return값을 지정하여 대신 전달해줘서 실패하지 않음(error뜨지 않음)
  // 위 producer 코드에서 ⓐ를 주석 처리, ⓑ를 주석해제한 다음
 // 이 consumer코드 실행 시 콘솔 출력값은 " 🥖  => 🍳  "
    .then(cook)
    .then(console.log)
    // 에러 핸들링하고 있지 않기 때문에 마지막에 걸어줌
    .catch(console.log)
// 콘솔 출력값 "🐓 => 🥚 => 🍳 " 

드림코딩by엘리 유튜브 무료강의 참고

tip +

getHen()
  .then(hen => getEgg(hen)) // .then(getEgg)로 줄일 수 있음. 한 가지 인자만 받아오는 경우엔.
  .then(egg => cook(egg))
  .then(meal => console.log(meal));


❖ async & await

Promise의 경우에도 chaining이 길어지면 복잡하다.
Promise를 좀 더 간결하게, 비동기를 동기적으로 실행되는 것처럼 (코드를) 보이게 만들어주는 async&await 조합을 사용
async와 await는 synctatic sugar이자 유용한 API이다.

*경우에 따라 promise가 적합할 때가 있고, async & await를 써야할 때가 있다.

함수 앞에 async를 붙이고, async가 붙은 함수 내에서
await를 쓸 수 있다.

예제

function fetchUser() { // 서버에서 userdata를 받아오는 함수
// do network request in 10secs...
  return new Promise((resolve, reject) => {
      resolve('received'); 
  }) 
}
// * async
async function fetchUser() {
  return 'received';
}
const user = fetchUser(); // ! 함수 선언된 곳으로 함수의 블럭 수행 한줄씩.. 10초 동안 머무름
console.log(user); // 동기적이라 위 함수호출 수행 완료 후 넘어오는 것
// 비동기적인 처리를 안하면 사용자정보를 가져오는 데 10초가 걸리는데
// 이 이후 웹페이지에 표시되는 UI 를 로드하는 코드가 있다면
// 사용자는 10초 동안 텅텅 빈 브라우저 화면을 봐야함
// * await
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function getApple() {
  await delay(1000); // delay(3000) 호출 ('3초가 지나는 수행') 이 끝날 때까지 기다려
//  throw 'error'; // < catch 
  return '🍎';
}

async function getBanana() {
  await delay(1000); // .then 역할
  return '🍌';
}

async function pickFruits() {
          const apple = await getApple(); // ! delay(1000)
        const banana = await getBanana(); // ! delay(1000) 이 각 각 걸려있어(서로 상관없어 비동기적으로 이루어져도 됨)
    return `${apple} + ${banana}`;
}

pickFruits().then(console.log)

드림코딩by엘리 유튜브 무료강의 참고

  • Promise.all
    위 코드와 같이 사과를 받는 함수와 바나나 받는 함수와 서로 상관없이 동시다발적으로 이루어진다면? Promise.all이라는 api 사용
    모든 프로미스객체가 병렬적으로 받아짐. (배열 등의 형태로 넘겨주고 리턴함)
function pickAllFruits() {
    return Promise.all([getApple(), getBanana()]) // 배열로
        .then(fruits => fruits.join(' + '));
    // ! 모든 프로미스들이 병렬적으로 다 받아질 때까지 기다림
    // 배열 형태 등이 넘겨짐
}

→ Promise.all을 async & await와 함께 사용하여 효율적 코드 작성.
*Promise.race 도 있음, 가장먼저 수행결과값을 리턴하는 프로미스만 전달됨



💡 Question
다음 코드의 실행 순서는?

const p = new Promise((resolve, reject) => {
  console.log("p");
  setTimeout(() => {
    resolve("res");
    console.log("res");
  }, 2000);
});
p.then((val) => {
  console.log(val + 'ⓐ'); // ⓐ
});
console.log("here");
p.then((val) => {
  console.log(val + 'ⓑ'); // ⓑ
});
/* 출력 값
p 
here 
res 
resⓐ 
resⓑ
*/
process⇪call stackTask QueueWeb APIs
5console.log(val + 'ⓑ')
4console.log(val + 'ⓐ')
3console.log("res")
anonymous*2000ms after
p.then ⓑ
2console.log("here")
p.then ⓐ
setTimeout(anonymous)
1console.log("p")
promise

*중요한 것은 비동기는 동기가 끝나고 작동한다는 것

노션노트 참고

profile
developer; not kim but Young

0개의 댓글