[완벽가이드] 비동기 자바스크립트

쏘쏘임·2022년 7월 10일
0

Basic Javascript

목록 보기
2/2
post-thumbnail

일반적으로 웹 브라우저의 자바스크립트 프로그램은 이벤트 주도적이다. 따라서 일반적으로 서버는 네트워크를 통해 클라이언트 요청이 들어온 후에야 작업을 시작한다.

이번 장에서 배울 것:
1. 비동기 코드를 쉽게 만드는 중요한 기능: promise, async, await
2. 단순한 루프에서 비동기 이벤트 스트림 다루기: ES2018 비동기 이터레이터, for/await 루프 도입

13.1 콜백과 비동기 프로그래밍

  • JS 의 가장 기본적인 비동기 프로그래밍 -> 콜백

13.1.1 타이머

setTimeout(fn, ms), setInterval(fn, ms)
일정 시간이 지나면 코드를 실행하는 단순한 비동기 프로그램 유형 중 하나.

13.1.2 이벤트

지정된 컨텍스트에 지정된 이벤트가 일어날 때마다 함수(이벤트 핸들러, 이벤트 리스너)를 호출.

13.1.3 네트워크 이벤트

XMLHttpRequest 클래스와 콜백 함수를 사용해 HTTP 요청을 보내고 서버의 응답을 비동기적으로 처리

13.1.4 노드의 콜백과 이벤트

노드(서버사이드 JS 환경)는 비동기적으로 만들어져 있으며 많은 API가 콜백과 이벤트를 사용한다. (ex. 파일 콘텐츠 읽고 쓰기 등)

노드에서는 on() 메서드로 이벤트 핸들러를 등록한다.

https 내장 모듈을 이용해 http 통신하는 예제

const https = require('https');

// URL의 텍스트 콘텐츠를 읽고 비동기적으로 콜액에 전등립니다.
function getText(url, callback) {
  // URL에 HTIP GET 요청을 시작합니다.
  const request = https.get(url);
  
  // 응답 이벤트를 처리할 함수를 등록합니다.
  request.on('response', response => {
    // 응답 이벤트가 있다는 것은 응답 헤더를 받았다는 의미입니다.
    let httpStatus = response.statusCode;
    // HTIP 응답의 바디는 아직 받지 못했으므로
    // 바디를 받았을 때 호출할 이벤트 핸들러를 등록합니다.
    response.setEncoding('utf-8'); // 유니코드 텍스트를 예상합니다.
    let body = ''; // 텍스트는 이 변수에 누적됩니다.
    // 바디의 텍스트 덩어리를 사용할 수 있게 되면 이 이벤트 핸들러를 호출합니다.

    response.on('data', chunk => {
      body += chunk;
    });
    
    // 응답이 완료되면 이 이벤트 핸들러를 호출합니다.
    response.on('end', () => {
      if (httpStatus === 200) {
        // HTIP 응답이 OKi라면
        callback(null, body); // 응답 바디를 콜백에 전달합니다.
      } else {
        // 그렇지 않다면 에러를 전달합니다.
        callback(httpStatus, null);
      }
    });
  });

  // 저수준 네트워크 에러를 처리할 이벤트 핸들러도 등록합니다.

  request.on('error', err => {
    callback(err, null);
  });
}

13.2 프라미스

Promise : 비동기 작업의 결과를 나타내는 객체

콜백의 문제점 개선
1. 가독성: 콜백의 중첩(콜백 헬)을 좀 더 선형에 가까운 프라미스 체인으로 바꿈
2. 에러 처리: 최초 실행자에게 에러를 전달할 수 없던 콜백의 문제 해결

13.2.1 프라미스 사용

then()

  • 프라미스 반환 함수 호출에 직접 이어붙이는 형태로 사용
  • 프라미스 반환 함수, 결과를 사용하는 함수 모두 이름을 동사 형태 사용

에러 처리

  • catch()

용어 정리

  • fulfill(이행) 혹은 reject(거부), : 작업의 성공 여부
  • pending(대기) 혹은 settled(완료) : 작업의 완료 여부

13.2.2 프라미스 체인

  • 메서드 중첩: 표현식 하나에서 메서드를 하나 이상 호출하는 것
fetch (" /api/user/profi le")
	.then(response => { return response.json(); })
	.then(profile => { displayUserProfile(profile); }) ;
  • 프라미스의 핵심 장점 -> 콜백 헬 방지
  • 위의 메서드 중첩을 다음과 같이 깔끔하게 쓰자
fetch(theURL) // 작업 1. 프라미스 을 반환 
	.then(callbackl) // 작업 2. 프라미스 를 반환 
	.then(callback2); // 작업 3. 프라미스 을 반환

13.2.3 프라미스 해석

  • 프라미스는 중간 콜백 함수의 반환 값을 담은 프로미스 객체를 전달한다. 다음 프라미스로 잘 전달되면 해석, 아니면 거부된다.

  • 모든 프라미스 체인은 연결되어 있다.

13.2.4 프라미스와 에러

  • catch()
  • finally()
startAsyncOperation( )
	.then(doStageTwo) 
	.catch (recoverFromStageToError) 
	.then(doStageThree) 
	.then(doStageFour) 
	.catch(logStageThreeAndFourErrors);
  • .catch()에 전달하는 콜백은 이전 단계에서 에러가 일어났을 때만 호출된다. 이 곳에 전달된 에러는 프라미스 체인을 타고 내려가지 않는다. (410p)
  • catch의 콜백함수가 완료된 후에는 그 반환 값이 프라미스 체인을 따라 다음 프라미스로 넘어간다.

13.3 async 와 await

ES2017
" async, await는 효율적인 프라미스 코드에서 프라미스를 숨겨 (비효율적이지만) 읽기 쉽고 이해하기 쉬운 동기적 코드와 비슷하게 만듭니다. " 420p

이행된 프라미스 값 = 동기적 함수의 반환 값.
거부된 프라미스의 값 = 동기적 함수에서 일으킨 에러

13.3.1 await 표현식

  • await 키워드는 프로그램 흐름을 차단하지 않으며 지정된 프라미스가 완료되기 전에 '아무 일도 하지 않는다.'
  • 포인트: await를 사용하는 코드는 항상 비동기적이다.

13.3.2 async 함수

top level await 추가 조사

왜 사용하나

비동기적으로 데이터를 로드하는 모듈을 초기화할 수 있게 한다.

  • 동적으로 모듈 로딩하기
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)

console.log(messages.welcome);
  • 모듈 로딩이 실패하면 fallback하기
let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
  • 가장 먼저 로드되는 리소스 사용하기
const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

under hood

탑레벨 await 모듈을 임포트 해오는 예시

first.mjs:

const response = await fetch('http://example.com/first.txt');
export const first = await response.text();

main.mjs:

import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');

대충 이런 느낌으로 동작한다.
1. 각각의 비동기 모듈은 fulfilled된 프로미스 객체를 반환한다.
2. 비동기 모듈을 불러온 main.mjs에서는 비동기 모듈들을 Promise.all로 모두 fulfilled상태로 둔 다음 관련 작업들을 실행한다.
3. 동기 모듈은 원래대로 동작

first.mjs:

export let first;
export const promise = (async () => { // (A)
  const response = await fetch('http://example.com/first.txt');
  first = await response.text();
})();

main.mjs:

import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
  await Promise.all([firstPromise, secondPromise]); // (C)
  assert.equal(first, 'First content!');
  assert.equal(second, 'Second content!');
})();

탑레벨 await의 장단점

The two most important benefits of top-level await are:

  • It ensures that modules don’t access asynchronous imports before they are fully initialized.
  • It handles asynchronicity transparently: Importers do not need to know if an imported module is asynchronous or not.

On the downside, top-level await delays the initialization of importing modules. Therefore, it‘s best used sparingly. Asynchronous tasks that take longer are better performed later, on demand.

However, even modules without top-level await can block importers (e.g. via an infinite loop at the top level), so blocking per se is not an argument against it.

13.3.3 여러 개의 프라미스 대기

await는 프라미스에 기반하므로 직접 사용할 때와 마찬가지로 Promise.all()을 사용하면 된다. (getJSON 은 async로 감싸인 비동기 함수)
let [valuel, value2] = await Promise.all([getJSON(urll) , getJSQN(url2)]);

13.3.4 세부 사항

await 키워드를 함수 바디를 동기적 덩어리로 구분하는 일종의 표식이라고 생각하면 편하다.

13.4 비동기 순회 (비동기 이터레이터)

프라미스 기반으로 만들어졌고 for/of 루프의 새 형태인 for/await 와 함께 사용하도록 설

13.4.1 for/await 루프

  • 노드 12는 readable 스트림을 비동기적으로 이터러블로 만든다.

13.4.2 비동기 이터레이터

일반적인 이터레이터와의 차이

  • 심벌 이름 (Symbol.asyncIterator)를 가진 메서드 존재
    - for/await는 Symbol.iterator 메서드보다 Symbol.asyncIterator를 먼저 시도
  • next()메서드가 직접적으로 순회 결과 객체를 반환하는 것이 아니라 순회 결과 객체로 해석되는 프라미스를 반환한다.

13.4.3 비동기 네저네리터

  • 비동기 제너레이터는 비동기 함수의 특징과 제너레이터의 특징을 모두 가진다.
  • await, yield를 사용할 수 있고 yield로 전달하는 값은 자동으로 프라미스가 된다.
  • async function* 을 사용한다.
    (예시)
// aait를 사용할 수 있도록 setTimeout ( )을 감싸는 프라미스 기반 래퍼 함수입니다. 
// 밀리초 단위로 지정된 시간이 지나면 이행되는 프라미스를 반환합니다. 
function elapsedTime(ms) {
	return new Promise(resolve => setTimeout(resolve, ms));
} 

// 지정된 횟수만큼 (무한히 반복할 수도 있습니다) 지정된 시간마다 
// 차운터를 증가시켜 전달하는 비동기 제너레이터 합수 
async function* clock(interval, max=Infinity) { 
	for(let count=1; count <= max; count++) { 
	// 일반적인 for 루프 
		await elapsedTime(interval); // 지정된 시간만큼 대기하고 
		yield count;
	} 
}	
// 비동기 제너레이터와 for/await를 사용하는 테스트 합수 
// for/await를 쓸 수 있어야 하므로 비동기로 만들었습니다. 
async function test() { // 카운터를 전달합니다. 
	for await (let tick of clock(300, 100)) { //300밀리초마다 100번 반복
		console.log(tick); 
	}
}

13.4.4 비동기 이터레이터 구현

비동기 제너레이터를 사용하지 않고 비동기 이터레이터 직접 만들기. 왜...
(일단 패스)

13.5 요약

  • JS 프로그래밍은 대부분 비동기다.
  • 콜백의 문제: 콜백 헬, 에러처리
  • 프라미스: then() 의 선형 체인으로 중첩 제거, catch()로 에러 처리 코드 모으기
  • async, await 로 비동기 코드를 동기적으로 작성
  • 비동기 이터러블 객체
    - for/await 루프 사용 가능
    - Symbol.asyncIterator메서드 혹은 async function* 제너레이터 함구를 호출해 만들 수 있다.
    - 노드 스트림의 '데이터' 이벤트의 대안
    - 클라이언트 사이드 js에서는 사용자 입력 이벤트를 스트림으로 표현하는데 사용할 수 있음

노트

  • 노드에서 사용하는 이벤트 핸들러는 언제 쓰나? -> 워크 스레드, 웹워크(돔 접근은 못함) 등등
profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글