JS/Node – 비동기

비동기 쉽게 이해하기

햄버거 주문을 예로 들어 생각해보자


햄버거 가게에서 손님들이 햄버거를 주문하려고 왔다. 그런데 먼저 주문한 A가 주문한 햄버거를 받을 때까지 그 뒤에 있는 B가 햄버거를 주문조차 할 수 없다고 생각해보자
→ “blocking”(하나의 작업이 끝날 때까지 이어지는 작업을 막는 것)

 

손님 B는 손님 A가 주문한 햄버거가 나오고 나서야 원하는 햄버거를 주문할 수 있다. 즉 손님 A의 햄버거 주문 완료 시점과 손님 B의 햄버거 주문 시작 지점이 같다
→ “동기적(synchronous)” 이다(완료 시점과 시작 시점이 같은 상황)

 

효율적인 햄버거 가게 운영을 위해 커피 주문 과정을 다음과 같이 변경해 보자

* 햄버거 주문이 blocking 되지 않고, 언제든지 주문을 받을 수 있다
* 햄버거가 완성되는 즉시 햄버거를 제공한다
- 손님 A의 주문 완료 시점과 손님 B의 주문 완료 시점이 같을 필요가 없다

 

Node.js 의 경우도 효율적인 햄버거 가게라고 생각하면 된다.
→ non-blocking하고 비동기적(asynchronous)으로 작동하는 런타임으로 개발됨
→ 특히 JS의 비동기적 실행은 웹 개발에서 특히 유용

* 백그라운드 실행, 로딩 창 작업
* 인터넷에서 서버로 요청, 응답 받는 과정
* 큰 용량의 파일을 로딩

 

학습 목표

* 중첩된 callback이 발생하는 케이스

* 중첩된 callback의 단점, Promise의 장점
* Promise 사용 패턴의 이해
- `resolve`, `reject`, `then`, `catch`
- Promise에서 인자를 넘기는 방법
- Promise의 3가지 형태
- `Promise.all`

* `async/await` 키워드와 작동 원리 이해

* Node.js의 `fs` 모듈 사용

 

고차 함수와 Callback

고차 함수(HoF) 개념에 대해 다시 집고 넘어가자

* 고차함수는 다른 함수를 인자(argument)로 전달받을 수 있다
- 고차 함수의 인자로 전달되는 함수; 콜백 함수(callback function). 작업 중에 호출하는 경우가 많은 함수

* 고차함수는 다른 함수를 리턴할 수 있다(리턴 값이 함수인 형태)
- `함수를 리턴하는 함수`; `커리 함수`라고도 한다.

* `함수를 인자로 받는 함수`와 `함수를 리턴하는 함수` 모두 고차함수
- 즉, 고차함수는 콜백 함수와 커리 함수의 상위 개념

 

callback 함수를 전달 받은 caller 함수는 함수 내부에서 이 callback 함수를 호출(invoke)할 수 있다.
caller는 “조건”에 따라 callback 함수의 실행 여부를 결정
→ 조건에 따라서 아예 호출 안 할 수도, 여러 번 호출하여 실행할 수도 있는 것
(보통, 특정 작업의 완료 후에 호출하는 경우가 많다)

 


비동기 호출(Asychronous Call)

비동기를 다뤄보기 전에 callback 함수를 잘 집고 넘어가야 한다

콜백 함수

콜백 함수가 사용되는 경우들을 생각해 보자

* callback in action: 반복 실행하는 함수(iterator)
반복문 같은 곳 안에서 콜백 함수가 사용되는 경우를 생각

* callback in action: 이벤트에 따른 함수(event handler)
DOM에서 특정 이벤트에 의해 콜백 함수가 사용되는 경우

→ 헷갈리면 안되는 점: 함수 실행을 연결하는 것이 아니라 함수 자체를 연결해야 하는것

함수명();    ← 함수 실행( 틀린 케이스)
함수명;        ← 함수 자체

 

blocking vs. non-blocking

blocking은 전화 통화, non-blocking은 문자라고 생각하면 이해가 편하다

* 전화: 하던 일을 멈추고 받아야 한다(blocking), 요청에 대한 결과가 동시에 일어남(동기적)

* 문자: 확인 후 나중에 답장 가능하다(non-blocking), 요청에 대한 결과가 동시에 일어나지 않는다(비동기적)

 

동기 vs. 비동기

동기: 요청에 대한 결과가 동시에 일어난다
→ 즉, 그 요청에 대한 응답이 끝나야 다음 요청을 받을 수 있는 것

 

비동기: 요청에 대한 결과가 동시에 일어나지 않는다
→ 그 요청에 대한 응답과 무관하게 다음 요청을 받을 수 있는 것(요청에 blocking이 없다)
→ 비동기의 경우 그 내부 코드에서 callback 함수를 많이 사용한다

 

비동기 함수 전달 패턴

비동기 함수의 전달 패턴들은 크게 2가지로 구분 가능하다

1. callback 패턴

일반적인 콜백 함수 사용한 경우

ex) 
let request= ‘caffelatte’;
orderCoffeeAsync(request, function(response){
    //response → 주문한 커피의 결과
    drink(response);
});


2. 이벤트 등록 패턴

DOM에서 특정 이벤트에 사용한 경우

ex)
let request= ‘caffelatte’;
orderCoffeeAsync(request).onready = function(reponse){
    //response → 주문한 커피의 결과
    drink(response);
};

 

비동기의 주요 사례

* DOM Element의 이벤트 핸들러
- 마우스, 키보드 입력(click, keydown, ...)
- 페이지 로딩(DOMContentLoaded, …)

* 타이머
- 타이머 API(setTimeout, …)
- 애니메이션 API(requestAnimationFrame); 시간 흐름에 따라서 모양이 바뀌는?

* 서버에 자원 요청 및 응답; 가장 많이 사용하는 사례!
- fetch API
- AJAX(XHR)

 

브라우저의 비동기 함수 작동 원리를 알고 싶다면 “event loop” 키워드로 검색해 보자

 

비동기 자바 스크립트

왜 Async 자바스크립트 여야 하느냐?(Why Async)

동기적일 때보다 비동기적인 경우가 더 효율적이니깐(시간적으로 볼 때)

 

→ 비동기적인 경우, 어떤 특정 요청을 해 둔 상태에서 그 요청을 기다리지 않고 다른 요청이나 task를 처리할 수 있다
(유튜브 동영상 로딩을 걸어놓고 다른 댓글들을 보고 댓글을 단다거나 다른 리스트 찾아본다거나 하는 경우를 생각)

 

Callback

Async가 좋은 것은 알겠는데 그러면 순서를 제어하고 싶으면 어떻게 해야 하는가?
→ Callback 을 이용하자

 

가령, A , B, C의 순서대로 출력되도록 하고 싶은데?
→ A가 실행되는 구조 안에 Callback으로 B, C가 실행되게 받고, B가 실행되는 구조 안에 Callback으로
C가 실행되게 받는다면, A, B, C의 순서대로 로직이 동작한다.

 

Callback을 이용한 에러 핸들링 디자인 예

const isErrCheck = (callback) => {
    waitingUntilSomethingHappens();    // 뭔가 일어 날때까지 기다림

    if(isSomethingGood //어떤 조건){
        callback(null, something);    // 에러가 없고, data는  something
    }

    if(isSomethingBad //조건){
        callback(something, null);    // 어떤 오류메시지( something)와 데이터는 없
    }
}


isErrCheck((err, data) => {

    if (err) {
        console.log(“에러 발생”);
        return;
    }
)}

 

그런데 콜백이 너무 많이 반복(callback chain)되면 callback Hell에 빠지게 된다 (코드 가독성이 떨어지게 됨)
→ 그렇다면 어떻게 해야 할까?


→ Promise 를 사용!

Promise, async/await

Promise

기본형태: new Promise( (resolve, reject) => {});

 

단순화해서 살펴 보면,

new Promise()

    resolve()    // ← 다음 행동으로 넘어 가게 됨

    reject()    // ← 에러 핸들링

resolve: 다음 행동(task)으로 넘어가게 함
→ 밖에서 호출해서 사용할때 .then 으로 받는다(.then과 한 쌍)

 

reject: 에러 핸들링하는 부분
→ 밖에서 호출해서 사용할 때 .catch로 받는다(.catch와 한 쌍)

 

아래의 콜백 함수 체인이 사용된 것을 보자

const printString = (string, callback) => {

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


const printAll = () =>    {
    printString(‘A’ , () => {
        printString(‘B’, () => {
            printString(‘C’ , () => {

            })
        })
    }

    printAll();

 

이를 Promise를 이용해 바꿔보면,

const printString = (string) => {

     return new Promise((resolve, reject) => {    // resolve와 reject라는 콜백을 씀

    setTimeout(
    () => {
        console.log(string)
        resolve()    // callback 대신 resolve 가 사용됨
        //callback()
    },
    Math.floor(Math.random() * 100)+1
    )
     })
}


const printAll = () =>    {
    printString(‘A’ )
    .then(() => {    // .then을 사용    ; “이 끝나면” 의 뜻으로 이해
        return printString(‘B’)
    })
    .then(() => {
        return printString(‘C’)
    })
    // reject가 있었다면 .catch를 이용한다

 }

    printAll();

 

이를 도식화 해서 보면

[Promise]

1st task → .then ()  → success task → .then ()  →  success task

     → .catch()    → failure task

 

그런데 Promise 도 너무 과도하게 사용되면 Promise Hell 에 빠질 수 있다

그래서 Promise Chaining 작업을 할 때 `return 처리`를 잘해야 한다!

 

Promise에 대한 추가 보충 내용

Q. new Promise()를 통해 생성한 Promise 인스턴스에는 어떤 메소드가 존재하나요? 각각은 어떤 용도인가요?

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다.
다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서
미래의 어떤 시점에 결과를 제공합니다.

 

Q. Promise.prototype.then 메소드는 무엇을 리턴하나요?
→ 프로미스를 리턴한다

 

Q. Promise.prototype.catch 메소드는 무엇을 리턴하나요?
→ 값을 반환하는 경우: Promise를 리턴
→ 값을 반환하지 않는 경우: undefined를 리턴

 

Promise.prototype.catch 메소드 = Promise.prototype.then(undefined, onRejected)와 같은 뜻

 

Q. Promise의 세 가지 상태는 각각 무엇이며, 어떤 의미를 가지나요?

Promise는 다음 중 하나의 상태를 가집니다.
대기(pending): 이행하거나 거부되지 않은 초기 상태. -> 초기값.
이행이나 거부가 완료가 되었다면 다시 Promise를 반환해주면서 대기 상태로 돌아온다
이행(fulfilled): 연산이 성공적으로 완료됨. .then 이 된거
거부(rejected): 연산이 실패함. .catch 된거

Promise를 사용한 Best Practice
function getData(callback) {
  // new Promise() 추가
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', function(response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

 

async/await

비동기 함수들을 마치 동기적인 프로그램인 것처럼 쓸 수 가 있다.
→ 일반 함수 실행하듯이 `평평하게` 코드 작성 가능

특정 함수 코드 작성 부분

...

const result = async () => {    // async 함수다 라는 뜻

    const one = await 함수A();

    const two = await 함수B();

    const three = await  함수C();

    const four = await 함수D();

    // A, B, C , D의 순서대로 실행됨을 알 수 있음
    // 실제로 돌아가는 것은 Promise와 동일하나 표현에 있어서 동기적으로 표현 가능하게 한 것!
}

result();

 

타이머 API

비동기식 처리에 주로 사용된다

setTimeout(callback, millisecond)

일정 시간 후에 함수를 실행

// * arguments(인자): 실행할 callback 함수, millisecond(콜백 함수 실행 전 기다려야 할 시간; 밀리초)
// * return값(리턴값): 임의의 타이머 ID


setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);
// 123

 

setInterval(callback, millisecond)

일정 시간의 간격을 가지고 함수를 반복적으로 실행

// * 인자: 실행할 callback 함수, millisecond(반복적으로 함수를 실행시키기 위한 간격; 밀리초)
// * 리턴 값: 임의의 타이머 ID

setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
// 345

 

clearInterval(timeId)

반복 실행중인 타이머를 종료

// 인자: 타이머 ID(timeId)
// 리턴값: 없음

const timer = setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// 더 이상 반복 실행되지 않음

setTimeout 에 대응하는 clearTimeout도 존재한다

 


Node.js 모듈 사용하기

Node.js는 많은 API가 비동기로 작성되어 있다
→ Node.js: 비동기 이벤트 기반 자바스크립트 런타임 이기 때문
→ “로컬 환경”에서 자바스크립트를 실행할 수 있는 자바스크립트 런타임

 

브라우저에서 불가능한 몇가지 기능도 가능하다

Node.js 내장 모듈을 사용하는 방법

`fs(file system)` 모듈과 관련해서 살펴 보자
→ 파일을 읽거나 저장하는 기능을 구현하도록 돕는 모듈

 

파일을 읽을 때: readFile 메소드
파일을 저장할 때: writeFile 메소드

 

모든 모듈을 사용할 때는 모듈을 사용하기 위해 불러오는 과정이 필요!

 

ex) 브라우저에서 다른 파일을 불러올 때는 HTML의 `<script> `태그 이용했었음

<script src=”불러오고자하는_스크립트명.js”></script>

 

Node.js 에서는 자바스크립트 코드 “가장 상단”에 require 구문을 이용하여 다른 파일을 불러온다

ex) Node.js 에서 다른 파일을 불러오는 require 구문

const fs = require('fs'); // 파일 시스템 모듈을 불러옵니다
const dns = require('dns'); // DNS 모듈을 불러옵니다

// 이제 fs.readFile 메소드 등을 사용할 수 있습니다!

 

3rd-party 모듈을 사용하는 방법

써드 파티 모듈이란 해당 프로그래밍 언어에서 공식적으로 제공하는 빌트인(built-in) 모듈이 아닌 모든 외부 모듈을 통칭한다.


예를 들면, underscore의 경우 Node.js 공식문서에 없는 모듈이다
→ 써드 파티 모듈

 

이러한 써드 파티 모듈을 다운로드 받기 위해선?
→ npm install 명령어를 사용

 

ex) npm install 명령어를 통한 써드 파티 모듈인 `underscore` 설치

npm install underscore

 

이제 이 써드 파티 모듈을 사용하려면 위에서 배운 바와 같이 require 구문을 사용하면 된다

const 변수명A = require(‘underscore’);

 

fs.readFile을 통한 Node.js 공식 문서 탐구

fs.readFile: 로컬에 존재하는 파일을 불러오는(읽어오는) 메서드
→ 이 메서드를 Node.js를 통해 보는 법을 알아보자

 

fs.readFile(path[, options], callback)

메소드 fs.readFile은 비동기적으로 파일 내용 전체를 읽는데, 이 메소드를 실행할 때는 인자 3개를 넘길 수가 있다
→ 그것이 path , options(생략가능; 말그대로 옵션), callback함수

 

path <string> | <Buffer> | <URL> | <integer>

path 에는 파일 이름을 인자로 넘길 수가 있다. 위의 4가지 종류의 타입을 인자로 넘길 수 있지만, 일반적으로 문자열(<string>) 타입으로 넘긴다

ex) '/etc/passwd' 파일을 불러오는 예시

fs.readFile(‘/etc/passwd’, … , …)

 

options <Object> | <string>

대괄호로 감싼 뜻은 두번째 인자인 options는 넣을 수도 있고, 안 넣을 수 도 있다는 뜻
→ 대괄호는 선택적 인자를 의미함

 

options는 객체 형태(object) 또는 문자열(string)로 인자를 넘길 수 있다. 문자열로 전달할 경우 인코딩을 넘긴다.

ex) 두번째 인자 options에 encoding 정보를 전달함

let options = {
  encoding: 'utf8', // UTF-8이라는 인코딩 방식으로 연다는 뜻
  flag: 'r' // 읽기 위해 엽니다 // 파일시스템과 관련된 flag. fs.readFile의 기본값은 :r
}

// /etc/passed 파일을 옵션을 사용하여 읽습니다.
fs.readFile('/etc/passwd', options, ...)


fs와 관련한 flag를 몇개 살펴 보면

r: 읽기
w: 쓰기
f: 파일이 존재함

정도로 요약 가능

 

callback <Function>

- `err` <Error>
- `data` <string> | <Buffer>

콜백 함수를 전달한다. 이는 파일을 읽고 난 “후”에 비동기적으로 실행되는 함수이다

 

콜백 함수에는 2가지 파라미터(err, data)가 존재하는데,
에러가 발생하지 않으면 errnull이 되며, data에 문자열이나 buffer 라는 객체가 전달된다
`data`는 파일의 내용을 뜻함

 

Q: data에는 문자열이나 buffer가 전달되는데, 어떤 경우에 문자열로 전달이 될까?
A: options가 string이고 그것이 encoding 정보를 담고 있는 경우에 문자열(string)로 전달 된다

 

import { readFile } from 'fs';

readFile('/etc/passwd', 'utf8', callback);
// options가 utf8로 명시되었으므로, callback 함수에서 data에 string이 전달 된다

 

 


복사했습니다!