자바스크립트 완벽가이드 | 13장 비동기 자바스크립트

dev_hee·2022년 6월 26일
0


Event Drivend 란

비동기적으로 동작한다
= 데이터가 들어오거나, 어떤 이벤트가 일어날 때 까지 계산을 멈추고 대기하는 일이 잦다.

웹 브라우저의 자바스크립트 프로그램은 이벤트 주도적이다.
프로그램이 실제로 무엇인가 실행하기 전에 사용자가 무언가 클릭하고 탭하여 이벤트를 발생시켜야한다.


콜백과 비동기 프로그래밍

콜백

자바스크립트에서 가장 기본적인 비동기 프로그래밍은 콜백을 통해 이루어진다.

  • 콜백 : 다른 함수에게 전달하는 함수.

콜백을 전달받은 함수는 어떤 조건을 만족하거나 어떤 (비동기) 이벤트가 발생하면 콜백함수를 호출한다.

타이머

일정 시간이 지나면 코드를 실행하는 단순한 비동기 프로그래밍

setTimeout() 함수로 타이머를 설정할 수 있다.

setTimeout(checkForUpdates, 60000);
  • checkForUpdates : 콜백 함수
  • setTimeout : 콜백 함수를 등록하고 비동기 조건(1분)을 지정함
  • 60000 : 1분, 즉 1분이 지난 후 콜백함수가 호출될 것

이벤트

클라이언트 사이드 자바스크립트는 이벤트 주도적이다.
미리 지정된 계산을 수행하기 보다, 사용자가 무엇을 하길 기다렸다가 그 행동에 반응한다.

이벤트 주도 자바스크립트 프로그램

  • 지정된 컨텍스트에 지정된 타입의 이벤트를 처리할 콜백 함수(이벤트 핸들러, 이벤트 리스너)를 등록함.
  • 웹 브라우저는 지정된 이벤트가 발생할 때마다 함수를 호출함.
  • addEventListender 을 통해 콜백을 등록함.

네트워크 이벤트

브라우저에서 실행되는 JS 는 다음 처럼 웹 서버에서 데이터를 가져올 수 있다.

  • callback: 콜백을 인자로 받는다. 첫 번째 인자는 에러를, 두 번째 인자는 성공 결과를 받는다.
  • request: 백엔드의 API 에게 HTTP 요청을 보낸다.
  • onload: 응답을 받았을 때 호출할 콜백을 등록한다.
  • onerror: 네트워크 에러가 발생했을 때 호출할 다른 콜백을 등록한다.
function getNumber(callback){
 let request = new XMLHttpRequest();
 request.open("GET", URL);
 request.send();
  
 request.onload = function() {
   if(request.status === 200){
    let currentNumber = parseFloat(request.responseText);
     callback(null, currentNumber);
   } else {
     callback(request.statusText, null);
   }
 };
  
  request.onerror = request.ontimeout = function(e) {
   callback(e.type, null); 
  }
}

getNumber 함수에서 값을 사용하는 법

  • 비동기로 요청을 보내기 때문에, getNumber가 현재 번호를 동기적으로 반환할 수 없다.
  • 대신 getNumber 를 호출하는 호출자는 API 결과나 에러를 사용하는 콜백 함수를 전달한다.

프로미스 Promise

프로미스란...

  • 프로미스란 비동기 프로그래밍을 단순화하도록 설계된 코어 기능이다.
  • 비동기 작업의 결과를 나타내는 객체이다.
  • 프로미스의 값을 동기적으로 가져올 수 있는 방법은 존재하지 않는다. 값이 준비 됏을 때 콜백함수를 호출하도록 프로미스에게 요청할 뿐이다.
  • 호출자가 프라미스 객체에 콜백을 등록하면 콜백은 비동기 작업이 끝났을 때 호출된다.
  • 콜백을 사용하는 새로운 방법이다.

[콜백 기반 프로그래밍의 문제점]

  • 콜백 안에 콜백이, 그 안에 또 콜백이 이어지는 콜백 헬이 발생하여 가독성이 떨어진다.
  • 에러 처리가 어렵다. 예외가 발생하면, 이 예외를 비동기 동작의 최초 실행자에게 전달할 방법이 없다.

[프로미스가 콜백 기반 프로그래밍의 문제점을 해결한 방법]

  • 콜백 헬을 방지하기 위해, 프로미스 후속 처리 메서드로 선형에 가까운 프로미스 체인으로 바꿔준다.
  • 에러를 처리하는 방법을 표준화 하고 프로미스 체인을 통해 에러를 정확히 전달하는 방법을 제공한다.

[프로미스 작업의 특징]

  • 비동기 작업 하나가 앞으로 어떤 결과를 보일지 나타낸다.
  • 프로미스는 반복되는 비동기 작업을 나타낼 수 없다. 즉 setTimeout 을 대신할 수 있지만, setInterval 처럼 콜백을 반복적으로 호출하는 프로미스는 대신할 수 없다.
  • 마찬가지로, load 이벤트는 프로미스로 대체할 수 있지만, HTML 버튼 click 이벤트 핸들러는 프로미스를 사용하지 않는다. 사용자가 버튼을 한번 클릭하면 이후에 대응할 수 없기 때문...

프로미스 사용

프로미스를 반환하는 유틸리티 함수를 어떻게 사용할까?
JSON 형식인 HTTP 응답 바디를 분석하고 프로미스를 반환하는 getJSON 함수를 사용해보자.

getJSON(url).then(jsonData => {
  // JSON 값을 분석하면 비동기적으로 호출될 콜백 함수
});
  • getJSON: URL에 비동기 HTTP 요청을 보내고 응답을 대기하면서 프로미스 객체를 반환
  • then() : 프로미스 객체의 then 인스턴스 메서드. 콜백 함수를 then 메서드에 전달하면, HTTP 응답을 받아 JSON 으로 분석하고 그 결과값을 then 에게 전달된 함수에게 전달한다.

then()

클라이언트 사이드 자바스크립트에서 이벤트 핸들러를 등록할 때 사용하는 addEventListener() 과 유사한 콜백 등록 메서드이다.

  • 프로미스 객체에서 then 메서드를 여러 번 호출하면 각 콜백은 비동기 작업이 완료될 때 호출된다.
  • then 에 등록된 각 함수는 단 한번만 호출된다.
  • then을 호출할 때 비동기 작업이 이미 완료된 상태더라도, then에 전달된 콜백은 비동기적으로 호출된다. (태스크 큐에 저장되어 이벤트 루프에 의해 호출된다는 뜻인 것 같음)
  • getJSON 이 반환하는 프라미스 객체를 변수에 할당하지 않고, 바로 then 메서드를 이어붙이는 형태를 주로 사용한다.
function displayUserProfile(profile) { /* ... */ }

// 이 코드는 영어 문장과 거의 비슷하게 읽힌다.
getJSON("/api/user/profile").then(displayUserProfile);

프라미스의 에러 처리

1. then 메서드에 두 번째 에러 핸들링 함수를 전달해 에러 처리

getJSON("/api/user/profile").then(displayUserProfile, handleProfileError);
  • 프라미스 객체가 반환된 후에 동작이 이뤄지므로, 이 동작이 값을 반환할 지 예외를 일으킬지 미리 알 수 없다.

[동기 작업]

  • 동기 작업이 정상 완료된 경우 호출자에게 결과를 반환한다.
  • 동기 작업에서 뭔가 잘못되면, 이 예외를 처리할 catch 절을 만날 때 까지 콜 스택을 거슬러 올라가 전달될 예외를 일으킨다.

[프로미스 기반 비동기 작업]

  • 비동기 작업이 정상 완료된 경우 then의 첫 번째 인자인 함수에 그 결과가 전달된다.
  • 비동기 작업에 예외가 생긴경우, 호출자가 콜스택에 존재하지 않으므로 호출자에게 예외를 전달할 방법이 존재하지 않는다. 따라서 then 의 두 번째 인자인 함수에게 예외를 전달한다.
  • getJSON가 정상적으로 실행되면 그 결과가 displayUserProfile 에게 전달된다.
  • getJSON에서 문제가 생겨 에러가 발생하면 에러 객체가 handleProfileError에게 전달된다.

2. catch 메서드

일반적으로 then 에게 에러 핸들러 까지 전달하진 않는다.

  • then 의 두 번째 인수로 전달된 에러 핸들러는 displayUserProfile 에서 발생할 에러를 캐치하지 못한다.
  • catchgetJSON에서 발생한 에러 뿐 아니라 displayUserProfile 에서 발생할 에러까지 캐치한다.

catchthen 메서드를 호출하면서 첫 번째 인자로 null 을 전달하는 형태를 짧게 줄인 것이다.

// 다음 두 문는 완전히 동일하다.
getJSON("/api/user/profile").then(displayUserProfile).then(null, handleProfileError);

getJSON("/api/user/profile").then(displayUserProfile).catch(handleProfileError);

프로미스 용어

  • 프로미스의 두 가지 상태
    프로미스가 한번 완료 상태가 되면, 이행 또는 거부 상태가 변할 수 없다.
대기 Pending완료 settled
이행 fullfilled
거부 rejected

프로미스 체인

비동기 작업 시퀀스를 then 으로 체인으로 이어서 콜백헬을 방지한다.

fetch(URL)                         // 1. HTTP 요청을 보낸다
  .then(res => res.json())         // 2. 응답 JSON 바디를 가져온다.
  .then(doc => render(doc))        // 3. JSON 분석이 끝나면 문서를 사용자에게 표시한다.
  .then(rendered => {              // 4. 문서 렌더링이 끝나면
  	 cachedInDatabase(rendered);   // 로컬 데이터베이스에 캐시한다.
    })
  .catch(err => handle(err));      // 5. 에러 핸들링

fetch API

fetch("/api/user/profile").then(res => {
  // 프라미스가 해석되면 상태와 헤더가 존재한다. 
  if(res.ok &&
     res.headers.get("Content-Type") == "application/json"){
    	// 아직 응답 바디를 받지 못했다.
    }
  }
});

XMLHttpRequest 객체로 HTTP 요청을 예전 방법 대신, 새로운 프로미스 기반 Feat API 를 사용하자.

  • Fetch API 는 fetch() 함수 하나뿐이다.
  • fetch() 는 URL 을 받고 프라미스를 반환한다. 프로미스를 반환하기 때문에 then 후속 처리 메서드를 사용할 수 있다.
  • 반환된 프라미스는 HTTP 응답이 도착하기 시작하여 HTTP 상태와 헤더를 읽으면 이행(fullfilled) 된다.
  • fetch() 가 반환하는 프라미스가 이행(fullfilled)되면, 프라미스는 then() 메서드에 전달한 콜백에 응답 객체를 전달한다.
  • 응답 객체는 요청 상태와 헤더에 접근을 허용하며, 응답 바디에 각각 텍스트와 JSON 형태로 접근할 수 있는 text()json() 메서드도 가지고 있다.
  • 하지만 아직 응답 바디는 도착하지 않았을 수도 있기 때문에, 응답 바디에 접근하는 text()json() 메서드도 프라미스를 반환한다.

물론 다음과 같이 fetchjson() 메서드를 사용해 HTTP 응답 바디를 가져올 순 있지만, 프라미스를 콜백처럼 중첩해서 사용하는 것은 프라미스 설계 목적에 부합하지 않는다.

// Bad
fetch("/api/user/profile").then(res => {
  res.json().then(profile => { 
    // json 으로 분석된 바디를 요청한다.
    // json() 메서드 또한 프로미스를 반환하기 때문에 then 메서드를 사용할 수 있다.
    // 하지만 이렇게 프라미스를 중첩해서 사용하면 설계 목적과 부합하지 않는다.
      displayUserProfile(profile);
    });
  });
});

프로미스는 다음처럼 연속적인 체인으로 사용해야 한다.

fetch("/api/user/profile")
  .then(res => res.json())
  .then(profile => {
     displayUserProfile(profile);
  });

메서드 체인

// 인자를 제외하고 메서드 호출만 작성하면 다음과 같다.
fetch().then().then()

위 처럼 표현식 하나에 메서드를 하나 이상 호출하는 것을 메서드 체인이라고 부른다.

  • fetch() 함수는 프로미스 객체를 반환
  • 첫 번째 .then() 은 반환된 프로미스 객체의 메서드(then)를 호출함
  • 호출된 .then() 메서드는 프로미스 객체를 반환함
  • 두 번째 .then() 도 반환된 프로미스 객체의 메서드(then)를 호출함

[프로미스 객체와 콜백]

  • 프로미스는 프로미스 객체 하나에 여러개의 콜백을 등록하지 않는다.
  • then() 메서드를 호출할 때 마다 새 프로미스 객체를 반환하며 하나의 프로미스에 하나의 콜백이 등록된다.
  • 새 프로미스 객체는 then() 에 전달된 함수가 완료되기 전에는 이행(fullfilled) 되지 않는다.

메서드 체인의 예시

fetch(URL)           // 작업 1. 프로미스 1을 반환
  .then(callback1)   // 작업 2. 프로미스 2를 반환
  .then(callback2);  // 작업 3. 프로미스 3을 반환
  1. fetch() 호출하면 fetch 는 URL 로 HTTP GET 요청을 보내는 작업 1을 수행하고 프로미스 1을 반환한다.
  2. 첫 번째 .then 에서는 반환된 프로미스1의 then() 메서드를 호출하며, 프로미스1이 이행(fullfilled) 됐을 때 호출 할 callback1 함수를 전달했다. then() 메서드는 callback1 을 어딘가에 저장하고 프로미스 2를 반환한다. 작업 2는 callback1이 호출될 때 시작한다.
  3. 두 번째 .then 에서는 반환된 프로미스2의 then() 메서드를 호출하며, 프로미스2이 이행(fullfilled) 됐을 때 호출 할 callback2 함수를 전달했다. then() 메서드는 callback2 을 어딘가에 저장하고 프로미스 3를 반환한다. 작업 3는 callback2이 호출될 때 시작한다.

위 세 단계는 표현식을 처음 실행할 때 모두 동기적으로 일어난다.
1 단계에서 보낸 HTTP 요청이 인터넷을 통해 전송되는 동안 비동기적으로 일시 중지한다.

  1. HTTP 응답이 도착하기 시작한다. fetch() 호출의 비동기적 부분은 HTTP 상태와 헤더를 감싼 응답 객체를 값으로 프라미스1 을 이행(fullfilled) 한다.
  2. 프라미스1이 이행되면 그 값인 응답 객체가 callback1() 함수에 전달되어 작업2가 시작된다.
  3. 작업 2가 정상적으로 완료되고 프라미스2를 이행한다.
  4. 프라미스2를 이행하는 값은 callback2 함수에 전달되어 작업 3의 입력이 된다. 작업 3이 정상적으로 완료되면 프라미스3이 이행된다.
profile
🎨그림을 좋아하는 FE 개발자👩🏻‍💻

0개의 댓글