본문 바로가기

개발인생다반사/TIL(Today i learned)

TIL 211013 - [JS/Node] 비동기 (내용이 많음 주의!)

Achievement Goals

  • 어떤 경우에 중첩된 callback이 발생하는지 이해할 수 있다.
  • 중첩된 callback의 단점, Promise의 장점을 이해할 수 있다.
  • Promise 사용 패턴을 이해할 수 있다.
    • resolve, reject의 의미와, then, catch와의 관계를 이해할 수 있다.
    • Promise에서 인자를 넘기는 방법을 이해할 수 있다.
    • Promise의 세 가지 상태를 이해할 수 있다.
    • Promise.all 의 사용법을 이해할 수 있다.
  • async/await keyword에 대해 이해하고, 작동 원리를 이해할 수 있다.
  • Node.js의 fs 모듈의 사용법을 이해할 수 있다.

Chapter 1 - 고차함수 리뷰

고차 함수 : 함수를 리턴하는 함수, 다른 함수를 인자로 전달 받는 함수.

커리 함수 : 함수를 리턴하는 함수. (하스켈 커리의 이름을 따서 특별히 커리 함수라고 함)

콜백 함수 : 고차 함수의 인자로 전달 되는 함수.

 

고차 함수는 커리 함수와 콜백 함수의 상위 개념.

콜백 함수를 전달 받은 caller 함수는 함수 내부에서 콜백 함수를 호출(invoke) 할 수 있다.

caller 함수는 조건에 따라 콜백 함수의 실행 여부를 결정 할 수 있다. 아예 호출하지 않을 수도 있고, 여러 번 실행할 수도 있다.

특정 작업의 완료 후에 호출하는 경우를 자주 접할 수 있다.

Chapter 2 - 비동기(Asynchronous)

1. 비동기 호출(Asynchronous call)

커피를 주문한다고 가정. 커피 주문이 완료되어야 새로운 주문을 받을 수 있다면 커피 주문은 동기적으로 발생한다. 이처럼 시작 시점과 완료 시점이 동일한 상황을 동기적이라고 한다. 반대로 시작과 완료 시점이 다른 상황을 비동기적이라고 한다. 

 

카페에서 커피 주문이 완료되지 않으면 새로운 주문을 받을 수 없는 상태를 blocking이라고 한다. 반대로 커피 주문 완료 여부에 상관없이 새로운 주문을 받을 수 있는 상태를 non-blocking 이라고 한다.

 

Node.jsnon-blocking하고 Async하게 작동하는 런타임 환경으로 개발하게 되었다.

※ 런타임 환경(runtime environment) : 컴퓨터가 실행되는 동안 프로세스나 프로그램을 위한 소프트웨어 서비스를 제공하는 가상 머신의 상태

(Node.js에는 475,000개의 모듈이 있다고 한다.)

 

동기는 요청에 대한 응답이 동시에 일어난다. 비동기는 요청에 대한 응답이 동시에 일어나지 않는다.
동기는 요청에 대한 결과물을 받아야지만 다음 동작이 이루어지는 방식이다.
비동기는 요청에 대한 결과물을 받지 않아도 다음 동작이 이루어지는 방식이다.

 

[ 동기 VS 비동기]

동기(sync) : caller 함수가 작업완료를 끝까지 신경 쓴다. 요청에 대한 결과가 동시에 일어난다.

비동기(Async) : caller 함수는 호출만 하고 작업완료는 콜백 함수가 신경 쓴다. 요청에 대한 결과가 동시에 일어나지 않는다. 콜백 함수가 결과값을 반환하면 설정한 기능을 수행한다.

 

동기적인 실행 코드

더보기
function waitSync(ms) {
  let start = Date.now();
  let now = start;
  while(now - start < ms) {
    now = Date.now();
  }
}

function drink(person, coffee) {
  console.log(`${person}는 ${coffee}를 마십니다.`)
}

function orderCoffeedSync(coffee) {
  console.log(`${coffee}가 접수되었습니다.`);
  waitSync(4000);
  return coffee;
}

let customers = 
    [
      {
        name: 'Steve',
        request: '카페라떼'
      },
      {
        name: 'John',
        request: '아메리카노'
      }
    ];

customers.forEach(function(customer) {
  let coffee = orderCoffeedSync(customer.request);
  drink(customer.name, coffee);
})

 

JavaScript의 비동기적 실행(Asynchonous execution)이라는 개념은 웹 개발에서 특히 유용하다. 아래는 비동기적으로 실행되어야 더욱 효율적인 작업목록이다.

 

  • 백 그라운드 실행, 로딩 창 등의 작업
  • 인터넷에서 서버로 요청을 보내고, 응답을 기다리는 작업
  • 큰 용량의 파일을 로딩하는 작업

 

비동기 처리는 왜 필요한가?

 

[ blocking VS non-blocking ]

blocking: 콜백 함수가 작업을 완료할 때까지 제어권을 가지고 있고 작업을 마친 이후 콜러 함수에 제어권을 넘겨주는 상태. 즉시 return값을 넘겨 준다.

non-blocking: 콜백 함수의 작업 완료 여부에 상관없이 콜러 함수에게 제어권을 넘겨 준다.

 

 

♠ 콜백함수 전달 방법

JS에서 함수는 일급 객체. 일급 객체 함수의 특징은 인자로 받을 수 있다는 것이다. 

콜백함수는 콜러함수에서 의해 호출이 되어 진다.

 

일반적인 함수는 매개변수가 함수에 전달되고 함수의 바디에서 일정한 연산을 하고

그 결과값을 반환하면 함수의 한 사이클이 종료된다.

그런데 콜백함수에서

함수를 콜백으로 다른 함수의 인자처럼 사용할 경우에는 오직 함수이름만 전달해 주면 된다.

함수 뒤에 () 함수 호출 연산자를 붙여줄 필요가 없다.

setInterval(callback, 1000)

 

비동기 주요 사례

♠ 메소드 체이닝

메소드가 객체를 반환하게 되면 메소드의 반환 값인 객체를 통해 또 다른 함수를 호출할 수 있다. 이러한 패턴을 메소드 체이닝이라고 한다. 우리는 이미 고차함수와 내장 객체를 가지고 메소드 체이닝을 하고 있었다.

let ages = data
  .filter((animal) => {
    return animal.type === 'dog';
}).map((animal) => {
    return animal.age * 7
}).reduce((sum, animal) => {
    return sum + animal.age;
});
// ages = 84

참고1 : JavaScript — Learn to Chain Map, Filter, and Reduce

참고2: JavaScript에서 커링 currying 함수 작성하기

참고3: 메소드 체이닝 패턴

참고4: .this

2. 자바스크립트 비동기

원래 JS는 코드를 동기적으로 처리하는 언어이다.

참고: Event Loop (이벤트 루프)

 

웹 브라우저 동작원리

스택 : 실행되는 명령어들이 쌓이는 공간. 코드를 실행해주는 공간.

JS는 single threaded Language. 한번에 한 명령코드만 실행이 가능

힙: 스택에서 감지되는 변수들의 실제 값이 저장된 공간

 

그런데 setTimeout(console.log(2+2), 1000)은 스택에서 바로 실행되지 않기 때문에 대기실로 보낸다.

대기실로 보내야 하는 항목들 : Ajax(서버와 통신), 이벤트리스너, 타이밍 함수

 

대기실에 있다가 실행되게된 함수들은 콜백 큐(이벤트 큐)라는 곳에 저장하게 된다.

콜백 큐에서 스택으로 하나씩 올려 보낸다. (올려보내는 조건: 스택이 비어 있을 때만)

 

스택 -> 대기실 -> 콜백큐 -> 스택 / 이벤트 루프

출처: Youtube, 우리밋_woorimIT
출처: Youtube, 우리밋_woorimIT

비동기로 호출한다는 것은 순서를 제어할 수 없다는 의미.

비동기 함수의 문제점인 순서 제어 불가를 해결하는 방법.

비동기를 순서대로 제어하고 싶을 때 callback을 사용한다.

 

3. CALLBACK

Callback design = function(arr조건, callback)

  function asyncWork() {
    setTimeout(() => {
      console.log('첫번째');
      setTimeout(() => {
        console.log('두번째');
        setTimeout(() => {
          console.log('세번째');
        }, 3000 * Math.random());
      }, 3000 * Math.random());
    }, 3000 * Math.random());
  }
  asyncWork();

(callback의 단점) callback 함수를 통해서 순서를 제어하였지만 callback함수가 또 다시 callback함수를 호출하고 그 함수가 또 다시 callback 함수를 호출하면서 코드의 가독성이 현저히 떨어지게 되었다. 

 

바로 비동기 함수들을 동기적 순서로 나열했기 때문임.

동기적으로 setTimeout을 차례대로 실행을 시키지만 백그라운드에서 개별적으로 실행되는 비동기 코드를 인지하지 못함.

 

Callback hell(콜백 지옥). 이러한 코드를 콜백 지옥이라고 함. 만약 서버와 다수의 비동기 통신이 필요한 함수라면, 단일 통신을 하나 하나 호출할 수 밖에 없는 위의 코드는 문제가 더욱 심각해 진다.

 

4. Promise, async / await

Promise 객체는 콜백 헬을 벗어나기 위한 도구

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

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다.

 

(1) Using promises

promise는 함수에 콜백을 전달하는 대신에 콜백을 첨부한다.

 

iamAsync() 함수가 있다. 이 함수는 2가지 콜백 함수를 받는다.

iamAsync(whour, malecallback, femalecallback)

function malecallback(value) {
 console.log('i am male.')
}

function femalecallback(value) {
 console.log('i am female.')
}

iamAsync(whour, malecallback, femalecallback);

전달인자 whour에 따라 male콜백, female콜백 함수를 실행하게 된다.

promise를 사용하면 콜백을 전달하지 않고 콜백을 붙여서 사용할 수 있게 된다.

iamAsync(whour).then(malecallback, femalecallback);

조금 더 간단하게 써보자면

const promise = iamAsync(whour);
promise.then(malecallback, femalecallback);

위와 같은 것을 비동기 함수 호출이라고 부른다.

(참고: JS 비동기처리에 새롭게 접근하기)

 

(2) promise 특징

  • Promise는 매개 변수 resolve와 reject 중 반드시 하나 이상을 구현 부에서 호출해야 한다. 이는 resolve 또는 reject 함수가 해당 promise의 상태를 pending(대기), fullfilled(이행), rejected(거부)로 변경시키기 때문이다. 뒤에서 다루겠지만 await는 promise의 상태를 인식하여 성공 또는 실패 여부를 판단한다.
  • 만약 resolve, reject 중 아무것도 호출되지 않으면 해당 Promise는 pending 상태로 남게되고 다음 코드(.then과 동일)는 실행되지 않는다.
  • 비동기 작업이 성공 혹은 실패한 뒤 then()을 이용하여 추가 콜백도 동일하다.
  • then()을 여러번 사용하여 여러개의 콜백을 추가 할 수 있다. 그리고 각각의 콜백은 주어진 순서대로 실행된다.

(3) promise Chaining

보통 하나 이상의 비동기 작업을 순차적으로 실행해야 하는 경우를 본다. 순차적으로 각각의 작업이 이전의 단계의 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행하는 경우를 의미한다. 이러한 상황에서 promise chain을 이용할 수 있다.

 

즉, 다수의 비동기 작업을 순차적으로 실행해야 하는 경우 promise 체이닝을 이용하여 해결할 수 있다.

then() 함수는 새로운 promise를 반환한다.

 

아까 예시로 들었던 iamAsync를 살펴보자.

const promise1 = iamAsync(whour);
const promise2 = promise1.then(malecallback, femalecallback);

promise2는 iamAsync() 뿐만 아니라 malecallback 혹은 femalecallback의 완료를 의미한다.

아니면 malecallback 이나 femalecallback은 promise를 반환하는 비동기 함수일 수도 있다.

이 경우 promise2에 추가된 콜백은  malecallback 혹은 femalecallback의해 반환된 promise 뒤에 대기하게 된다.

 

 

프로미스 헬을 방지 하기 위해서 return 처리를 잘해주면 된다.

 

콜백 헬 방지 위해 프로미스 사용

프로미스 헬 방지 위해 return문 잘 처리 혹은 async 사용.

 

(4) async / await

await는 promise와 동작원리 / 순서는 모두 동일하다.

다만 실행하는 함수 앞에 await만 기재하면 작동하기 때문에 코드의 가독성이 높아진다.

const one = await gotoCodestates();

promise로 반환되는 결과값을 변수 one에 할당해주었다. 

함수 정의, 변수 할당, 변수를 가져다가 마음대로 사용이라는 코드의 작성 원리를 담아내었다.

await는 반드시 async 함수안에서만 사용이 가능하다. 

5. 타이머 관련 API

setTimeout(callback, millisecond)

일정 시간 후에 함수 실행

callback : 실행할 콜백 함수

millisecond : 콜백함수 실행 전 대기 시간

 

setInterval(callback, millisecond)

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

millisecond : 콜백함수 반복 실행 간격 시간

 

clearInterval(timerId)

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

 

clearTimeout(timerId)

콜백 함수 실행 대기 시간 종료

 

 

Chapter 3 - Node.js 모듈 사용법

1. Node.js 모듈 사용법

(1) Node.js 내장 모듈

Node.js는 비동기 이벤트 기반 자바스크립트 런타임

모듈 : 어떤 기능을 조립할 수 있는 형태로 만든 부분. fs모듈은 PC의 파일을 읽거나 저장하는 일을 할 수 있게 도와줌

 

Node.js 내장 모듈 Documentation

 

Index | Node.js v14.18.1 Documentation

 

nodejs.org

 

readFile - 파일을 읽을 때 / writeFile - 파일을 저장할 때

 

모듈을 사용하려면 사용하기 위해 불러오는 과정 필요

<script src="불러오고싶은_스크립트.js"></script>

자바스크립트 코드 가장 상단에 require 구문을 이용하여 다른 파일을 불러올 수 있다.(common JS)

common JS : 웹 브라우저 밖의 자바스크립트를 위한 모듈 생태계의 규칙을 설립하기 위한 프로젝트

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

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

 

(2) 3rd-party 모듈 사용법

써드 파디 모듈은 공식적인 모듈이 아닌 모든 외부 모듈을 이야기 한다.

예를 들어, Node.js에서 underscore는 Node.js 공식문서에 없는 모듈이기 때문에 써드 파티 모듈이다.

써드 파티 모듈을 다운로드 받기 위해서는 npm을 사용해야 한다.

npm install underscore

node_modules에 underscore가 설치되면 내장 모듈 사용하는 것처럼 require 구문으로 불러올 수 있다.

const _ = require('underscore');

 

2. fs.readFile을 통해 알아보는 Node.js 공식 문서 가이드

메소드 fs.readFile은 로컬에 존재하는 파일을 읽어온다.

 

 

Chapter 4 - fetch API

 

1. fetch 를 이용한 네트워크 요청

URL로 비동기 요청이 가능하게 하는 API가 fetch API이다. 사이트가 표시하는 정보는 종류가 다양하고 그 정보들을 시시각각 동적으로 데이터를 받아와야 한다. 이럴 때 웹사이트는 해당 정보만 업데이트 하기 위해 요청 API를 이용한다. fetch API를 이용해 해당 정보를 원격 URL로 불러올 수 있다.

 

특정 URL로 부터 정보를 받아오는 과정이 비동기적으로 이루어지기 때문에 경우에 따라서 시간이 걸릴 수 있다. 이렇게 시간이 소요되는 작업이 요구될 경우 blocking이 발생하면 안되므로 특정 DOM에 정보가 표시될때까지 로딩창을 대신 띄우는 경우도 있다.

 

let url =
  "https://v1.nocodeapi.com/codestates/google_sheets/YbFMAAgOPgIwEXUU?tabId=최신뉴스";
fetch(url)
  .then((response) => response.json())
  .then((json) => console.log(json))
  .catch((error) => console.log(error));

위의 코드에서 유추할 수 있듯이 fetch API는 프로미스를 결과값으로 반환받는 것을 알 수 있다.