관리 메뉴

Dev Blog

5. Asynchronous & Promise 본문

BootCamp_Codestates/IM Tech Blog

5. Asynchronous & Promise

Yongjae Kim 2020. 12. 21. 10:23

Achievement Goals

1. 어떤 경우에 중첩된 callback이 발생하는지 이해할 수 있다.

2. 중첩된 callback의 단점, Promise 의 장점을 이해할 수 있다.

3. Promise 사용 패턴과 언어적인 특징들을 이해할 수 있다. 

  3-1. resolve, reject의 의미와, then, catch와의 관계를 이해할 수 있다.

  3-2. Promise 에서 인자를 넘기는 방법에 대해 이해할 수 있다.

  3-3. Promise의 세가지 상태를 이해할 수 있다.

  3-4. Promise.all 의 사용법을 이해할 수 있다.

4. async/await keyword에 대해 이해하고, 작동 원리를 이해할 수 있다.

5. node.js의 fs 모듈의 사용법을 이해한다.


목차

 

1. why Async

2. Callback

3. Promise

4. async await


1. Why Async

Synchronous

: 동기적인 처리방식. 특정 코드를 수행 완료한 후 아래줄의 코드를 수행한다.

client가 요청을 하고 server 응답을 받을 때까지 기다림. blocking.

 

Asynchronous

: 비동기적 처리방식. 특정 코드를 수행하는 도중에 아래로 계속 내려가며 수행한다.

client가 요청을 하고 작업을 하다가 server 응답을 받고 관련 업무를 실행. non blocking.

 

blocking code 란?

웹 앱이 브라우저에서 특정 코드를 실행하느라 브라우저에게 제어권을 돌려주지 않는 경우를 blocking 이라고 부릅니다.

사용자의 입력을 처리하느라 웹 앱이 프로세서에 대한 제어권을 브라우저에게 반환하지 않는 현상 입니다.(alert 같은...)

 

비동기 호출 용도?

시간의 흐름(time line)에 따른 애니메이션 따위의 작업을 할 때 => sprint part1

파일 I/O => 영상 스트리밍 => sprint part2

네트워크 요청 (fetch) => sprint part3

 

비동기 API

part1: setTimeout, setInterval, requestAnimationFrame

part2: (node.js)fs.readFile fs.writeFile

part3: (browser)fetch, (node.js)http 모듈, 데이터베이스 접근


2. callback

비동기로 돌아가는 자바스크립트의 순서를 제어하는 방법 입니다.

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

const printAll = () => {
  printString("A", () => { //callback!
    printString("B", () => { //callback!
      printString("C", () => {}) //callback!
    })  
  })
}
printAll() // A, B, C

Callback Error handling Design & Usage

const somethingGonnaHappen = callback => {
    waitingUntilSomethingHappens()

    if (isSomethingGood) {
        callback(null, something) 
        //err = null, something = data => return data! 
    }

    if (isSomethingBad) {
        callback(something, null) 
        //err = something => console.log('ERR!!')
    }
}

somethingGonnaHappen((err, data) => {
    if (err) {
        console.log('ERR!!');
        return;
    }
    return data;
})

Callback Error handling Design Example

in

node.js 모듈인 fs.readFile 메소드를 이용하여 로컬 파일 읽어오기

const fs = require("fs");

const getDataFromFile = function (filePath, callback) {
  // TODO: fs.readFile을 이용해 작성합니다

  fs.readFile(filePath, 'utf8',(err, data) => {
  //fs.readFile(path[, options], callback)
    if(err) {
      return callback(err, null); //err 이라면, err, data => null 
      //만약 err가 true일 경우, data => null => console.log(null)
    } else {
    //만약 err가 false일 경우, data => err => null => console.log(data)
      return callback(null, data); //err이 아니면, err=> null, data => data
    }
  });
};

getDataFromFile('README.md', (err, data) => console.log(data)); //path와 callback 함수

(node.js 모듈 사용법 출처:nodejs.org/api/fs.html)

 

What the CallBack Helllllllllllllllllllllllllll

 

하지만 아래의 const printAll 에서 볼 수 있듯이 printString 을 호출하는 횟수가 많아지면 callback Hell 이 발생할 수 있는데,

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

즉, 가독성이 떨어진다는 점이 단점이라고 할 수 있습니다.

Callback hell is any code where the use of function callbacks in async code becomes obscure or difficult to follow. Generally, when there is more than one level of indirection, code using callbacks can become harder to follow, harder to refactor, and harder to test.
const printAll = () => {
  printString("A", () => {
    printString("B", () => {
      printString("C", () => {
        printString("D", () => {
          printString("E", () => { 
            printString("F", () => { 
              printString("G", () => {
                printString("H", () => {
                ....
              })
            })
          })
        })
      })
    })  
  })
}
printAll() // A, B, C, D, E, F, G, H .....

이를 개선하기 위한 세가지 방법은 아래와 같습니다.

 

1. 동기 함수를 사용

2. 콜백 함수를 분리

3. Promise 패턴 도입

 

그 중에서 Promise 패턴 도입이 콜백 문제를 해결할 수 있는 가장 효과적인 방법입니다.

 

promise 를 이용하여 로컬 파일 읽어오기

const fs = require("fs");

const getDataFromFilePromise = (filePath) => {
  // return new Promise()
  // TODO: Promise 및 fs.readFile을 이용해 작성합니다.

  return new Promise((resolve, reject) => {
    fs.readFile(filePath, "utf8", (err, data) => {
      if (err) {
        reject(err); //.catch() => 실행
      } else {
        resolve(data); //.then() => 실행
      }
    });
  });
};

getDataFromFilePromise("README.md")
.then(data => console.log(data)) //promise 가 성공적일 때 실행되는 resolve 의 인자를 받습니다. 
.catch(err => console.log(err)) //promise 가 실패했을 때 실행되는 reject의 인자를 받습니다. 

3. Promise 로 비동기 함수 처리

Promise?

Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.

제작코드(producing code, 생산자) + 소비코드(consuming code, 소비자) 로 이루어져 있습니다.

Promise는 제작코드와 소비코드를 연결해 주는 특별한 자바스크립트 객체로 일종의 '구독리스트' 라고 할 수 있습니다.

 

Producing Code Part

let promise = new Promise(function(resolve, reject) {
  // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.

  // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result가 'done'이 됩니다.
  setTimeout(() => resolve("done"), 1000);
});

- new Promise에 전달되는 함수는 executor(실행자, 실행 함수) 라고 부릅니다. 
- executor는 new Promise가 만들어질 때 자동으로 실행되는데, 
- 결과를 최종적으로 만들어내는 제작 코드를 포함합니다. 
- executor의 인수 resolve와 reject는 자바스크립트가 자체적으로 제공하는 콜백입니다. 
- executor에선 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 합니다.

    1. resolve(value) — 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출
    2. reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
    
- new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖습니다.

    1. state — 처음엔 "pending"(보류)이었다 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"로 변합니다.
    2. result — 처음엔 undefined이었다, resolve(value)가 호출되면 value로, reject(error)가 호출되면 error로 변합니다.

Consuming Code Part

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

promise
.then(
  result => alert(result), // 1초 후 "done!"을 출력
)
.catch(err => alert(err)) 
.finally(() ==> alert("무슨 일이 일어나든 실행됩니다!!") 


.then : promise 가 이행되었을때 실행되는 함수이고 실행 결과(인자)를 받습니다. 
.catch : promise 의 결과가 실패일 경우 new Error("에러 발생") 인자를 받아 alert 합니다. 
.finally: 결과가 어떻든 마무리가 필요할 때 유용합니다. 단 finally 엔 인수가 없습니다. 

 

특징?

  • 콜백(resolve, reject)은 자바스크립트 Event Loop이 현재 실행중인 콜 스택을 완료하기 이전에는 절대 호출되지 않습니다.
  • 비동기 작업이 성공하거나 실패한 뒤에 then() 을 이용하여 추가한 콜백의 경우에도 위와 같습니다.
  • then()을 여러번 사용하여 여러개의 콜백을 추가 할 수 있습니다. 그리고 각각의 콜백은 주어진 순서대로 하나 하나 실행되게 됩니다.

Promise의 장점?

Promise가 return되는 경우, 비동기 작업이 끝나 resolve나 reject가 호출될 때까지는 정지해 있다가, resolve나 reject가 실행되면 then이나 catch를 호출합니다.

이처럼 비동기 작업을 계속 매달아서 쓸 수 있으므로(Promise chain), 콜백이 안쪽으로 깊어지는 문제가 해결됩니다.

then 내에서 throw하면 자동으로 가장 가까운 catch로 오류가 전달되어 에러처리가 직관적이고 실수할 가능성이 낮습니다.

 

throw?

암시적 try...catch

프라미스 executor와 프라미스 핸들러 코드 주위엔 '보이지 않는 try..catch'가 있습니다.

예외가 발생하면 암시적 try..catch에서 예외를 잡고, 이를 reject처럼 다룹니다.

new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!

new Promise((resolve, reject) => {
  reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!

 

지옥의 콜백 피라미드를

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

콜백함수를 반환된 promise에 promise chain을 형성하여 아래와 같이 연결 시킬 수 있습니다.

//일반형
doSomething() //반환된 promise
.then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
//화살표 함수를 이용한 축약형
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

예제1) .then을 활용하여 데이터 합치기

const path = require('path');
const { getDataFromFilePromise } = require('./02_promiseConstructor');

const user1Path = path.join(__dirname, 'files/user1.json');
const user2Path = path.join(__dirname, 'files/user2.json');
//__dirname: 현재 실행 파일을 포함하고 있는 문서의 절대 경로를 말해줍니다. 

// HINT: getDataFromFilePromise(user1Path) 맟 getDataFromFilePromise(user2Path) 를 이용해 작성합니다
const readAllUsersChaining = () => {
  // TODO: 여러개의 Promise를 then으로 연결하여 작성합니다

  let array = [];

  return getDataFromFilePromise(user1Path)
    .then(data1 => { //data1(user1Path 요소)이 화살표 함수를 통해서 다음 {} 안에 전달된다. 
      return getDataFromFilePromise(user2Path)
      .then(data2 => { //data2 가 화살표 함수를 통해서 다음 {} 안에 전달된다. 
        array.push(data1);
        array.push(data2);
        //user1.json의 내용과 user2.json 내용을 합쳐 객체로 리턴되어야 합니다
        //파일 읽기의 결과가 문자열이므로, JSON.parse 를 사용하여 문자열을 객체로 만듭니다.  
        return array.map(x => JSON.parse(x));
      })
    })
  }

readAllUsersChaining();

 

예제2) Promise 활용과 Error Handling

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔"), 1000);
  });
const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`error!! ${hen} => 🥚`)), 1000);
    //error가 발생한 경우!!
    // setTimeout(() => resolve(`${hen} => 🥚`), 1000);
  });
const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

// getHen()
//   .then((hen) => getEgg(hen))
//   // .then((egg) => cook(egg))
//   .catch(error => {
//     return '🍤';
//   })//에러가 발생할지라도 promise chain이 실패하지 않도록 다른 요소로 대체하는 catch 구문을 사용하여 요리를 완성합니다.
//   .then(cook)
//   .then((meal) => console.log(meal))
//   .catch((meal) => console.log(meal))

getHen()
  .then(getEgg)
  .catch((error) => {
    return "🍞";
  })
  .then(cook)
  .then(console.log)
  .catch(console.log);

3-1. Promise Chaining

promise hell에 빠지지 않기 위해 사용하는 chaining

function gotoCodestates() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('1. go to codestates') }, Math.floor(Math.random() * 100) + 1)
    })
}

function sitAndCode() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('2. sit and code') }, Math.floor(Math.random() * 100) + 1)
    })
}

function eatLunch() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('3. eat lunch') }, Math.floor(Math.random() * 100) + 1)
    })
}

function goToBed() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('4. goToBed') }, Math.floor(Math.random() * 100) + 1)
    })
}

gotoCodestates()
.then(data => {
    console.log(data)
    return sitAndCode()
})
.then(data => {
    console.log(data)
    return eatLunch()
})
.then(data => {
    console.log(data)
    return goToBed()
})
.then(data => {
    console.log(data)
})

/* Helllllll
	
gotoCodestates()	
.then(data => {
    console.log(data)
	
    sitAndCode()
    .then(data => {
        console.log(data)
	
        eatLunch()
        .then(data => {
            console.log(data)
	
            goToBed()
            .then(data => {
                console.log(data)
	
            })
        })
    })
})
*/

3-2 promise.all 을 사용하여 데이터 합치기

const path = require('path');
const { result } = require('underscore');
const { getDataFromFilePromise } = require('./02_promiseConstructor');

const user1Path = path.join(__dirname, 'files/user1.json');
const user2Path = path.join(__dirname, 'files/user2.json');

const readAllUsers = () => {
  // TODO: Promise.all을 이용해 작성합니다

  let promise1 = getDataFromFilePromise(user1Path);
  let promise2 = getDataFromFilePromise(user2Path);
  //Promise.all() 의 결과값이 배열이므로 data.map 을 사용하여 각 요소들을 JSON.parse 
  return Promise.all([promise1, promise2]).then(data => data.map(data => JSON.parse(data)))
}

readAllUsers()

4. Async await

 

Async / Await 이란?

  • callback이나 promise와 같이 비동키 코드를 작성하는 새로운 방법입니다.
  • Java와 같이 동기적으로 코딩할 수 있습니다.
    (동기적 코딩이란, 위에서 아래 흐름대로 순차적으로 진행된다는 말이다.)

사용방법?

  • 함수를 선언할때 async라는 단어를 붙여줍니다.
  • await이라는 단어는 async로 정의된 함수에서만 사용되며, 비동기 함수를 call할때 앞에 붙여 사용합니다.
  • function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환합니다.
async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

함수를 호출하고, 함수 본문이 실행되는 도중에 (*)로 표시한 줄에서 실행이 잠시 중단되었다가 
프라미스가 처리되면 실행이 재개됩니다. 이때 프라미스 객체의 result 값이 변수 result에 할당됩니다. 
따라서 위 예시를 실행하면 1초 뒤에 '완료!'가 출력됩니다.

await는 말 그대로 프라미스가 처리될 때까지 함수 실행을 기다리게 만듭니다. 
프라미스가 처리되면 그 결과와 함께 실행이 재개되죠. 프라미스가 처리되길 기다리는 동안엔 
엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않습니다.

 

Async / Await이 Callback이나 Promise보다 나은 점

  • 자바와 같이 동기적 코드 흐름으로 개발이 가능하다.
  • 코드가 간결해지고, 가독성이 높아진다.
  • 응답데이터로 들어오는 변수(관례적으로 많이 사용되는 data, response)를 없앨 수 있다.
  • try / catch로 에러를 핸들링할 수 있다.
  • error가 어디서 발생했는지 알기 쉽다.
// promise 연속 호출
function getData() {
    return promise1()
        .then(response => { //promise1을 통해 전달된 인자.
            return response;
        })
        .then(response2 => { //promise1을 통해 전달된 인자.
            return response2;
        })
        .catch(err => {
            //TODO: error handling
            // 에러가 어디서 발생했는지 알기 어렵다.
        });
}

// async / await 연속 호출
async function getData() {
    const response = await promise1();
    const response2 = await promise2(response);
    return response2;
}

 

기본 포맷

function fetchItems() {
  return new Promise(function(resolve, reject) {
    var items = [1,2,3];
    resolve(items)
  });
}

async function logItems() {
  var resultItems = await fetchItems();
  console.log(resultItems); // [1,2,3]
}

logItems(); // Array(3) [ 1, 2, 3 ]

 

예제

 

예제1)

function gotoCodestates() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('1. go to codestates') }, Math.floor(Math.random() * 100) + 1)
    })
}

function sitAndCode() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('2. sit and code') }, Math.floor(Math.random() * 100) + 1)
    })
}

function eatLunch() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('3. eat lunch') }, Math.floor(Math.random() * 100) + 1)
    })
}

function goToBed() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve('4. goToBed') }, Math.floor(Math.random() * 100) + 1)
    })
}

const result = async () => {
    const one = await gotoCodestates();
    console.log(one)

    const two = await sitAndCode();
    console.log(two)

    const three = await eatLunch();
    console.log(three)

    const four = await goToBed();
    console.log(four)
}

result();

예제2) async, await 활용하여 데이터 합치기

const path = require('path');
const { getDataFromFilePromise } = require('./02_promiseConstructor');

const user1Path = path.join(__dirname, 'files/user1.json');
const user2Path = path.join(__dirname, 'files/user2.json');

async function readAllUsersAsyncAwait () {
  // TODO: async/await 키워드를 이용해 작성합니다

    const result = [];

    await getDataFromFilePromise(user1Path).then(JSON.parse).then(data1 => result.push(data1));
    await getDataFromFilePromise(user2Path).then(JSON.parse).then(data2 => result.push(data2));

    // await getDataFromFilePromise(user1Path).then(data1 => result.push(JSON.parse(data1)));
    // await getDataFromFilePromise(user2Path).then(data2 => result.push(JSON.parse(data2)));
    // return result.map(x => JSON.parse(x))
    //Promise.all() 의 결과값이 배열이므로 data.map 을 사용하여 각 요소들을 JSON.parse 객체화 한다. 

    return result;
}

readAllUsersAsyncAwait();

(출처:medium.com/@constell99/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-async-await-%EA%B0%80-promises%EB%A5%BC-%EC%82%AC%EB%9D%BC%EC%A7%80%EA%B2%8C-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%9E%88%EB%8A%94-6%EA%B0%80%EC%A7%80-%EC%9D%B4%EC%9C%A0-c5fe0add656c)

promise.all 사용법

 

 

 

이벤트 루프?

자바스크립트는 코드 실행, 이벤트 수집과 처리, 큐에 놓인 하위 작업들을 담당하는 이벤트 루프에 기반한 동시성(concurrency) 모델을 가지고 있습니다.

 

youtu.be/8aGhZQkoFbQ

 


스프린트 리뷰 내용 정리

동기와 비동기의 차이?

동기: blocking. alert(그래서 웬만해서는 사용하지 않음)

비동기: non-blocking, 로딩만 보여도 논블라킹!

 

Promise.all

유어클래스 질문들에 대한 답을 다 할 수 있어야 함.

 

Promise.all 의 사용법을 이해했는가?

둘다 fulfilll (성공) 된 상태일 경우에 다음 .then이 불리고 아닐 경우 .catch 가 불려진다.

Promise.allSettled() => 둘 중에 하나만 성공해도 진행 가능하려면?

 

파일 다루고 서버 만들고 데이터 다루고... node 환경에서 가능

 

json() => response 객체로 검색. body 의

JSON.parse

 

 

 

Comments