본문 바로가기

JavaScript

20210721 JavaSciprt DeepDive 17 : 프로미스, fetch, 제너레이터, async/await, 에러 처리

JavaScript Deep Dive 17



📄 용어 및 중요사항 정리


프로미스


  • 비동기 함수 : 함수 내부에 비동기로 동작하는 코드를 포함한 함수
    • 비동기 함수 내부의 비동기 동작 코드는 비동기 함수가 종료된 이후에 완료됨
    • 비동기 동작 코드에서 처리 결과를 외부로 반환하거나, 상위 스코프의 변수에 할당 하면 기대한 대로 동작하지 않음
    • 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 외부에서 콜백 함수를 받아 수행해야 함 -> 콜백 헬 발생
    • 콜백 헬
      • 에러 처리 한계 : 에러는 호출자 방향으로 전파되지만, 비동기 함수의 콜백은 비동기 함수가 호출하지 않음 -> 콜백에 대한 에러는 catch 되지 않음

  • 프로미스 :
    • 콜백 헬을 극복하기 위해 도입됨
    • 표준 빌트인 객체
    • 비동기 처리 상태와 철리 결과를 관리하는 객체
    • new Promise 생성자 함수로 생성
    • 비동기 처리를 수행할 콜백 함수를 인수로 전달 받음
      • 비동기 처리 후속 콜백 resolve(성공), reject(실패) 콜백 함수를 인수로 받음
    • 비동기 상태 정보 (status)
      • pending : 비동기 처리가 수행되지 않은 상태
      • settled : fulfilled(성공), rejected(실패)
    • 비동기 결과 (result)
      • undefined -> value or error
const promise = new Promise((resolve, reject) => {
  if (/* 비동기 처리 성공 조건 */) {
    // 비동기 성공시 호출할 함수
    resolve('result')
  } else {
    // 비동기 실패시 호출할 함수
    reject('failure reason')
  }
})

프로미스의 후속 처리 메서드


  • Promise.prototype.then(resolveCallback, rejectedCallback)
    • resolveCallback : fulfilled 상태시 프로미스의 비동기 처리 결과를 인수로 받음
    • rejectedCallback : rejected 상태시 프로미스의 에러를 인수로 받음
    • return : 언제나 프로미스 반환
  • Promise.prototype.catch(callback)
    • rejected 상태인 경우 호출 됨
    • callback : error 처리
    • return : 언제나 프로미스 반환
  • Promise.prototype.finally(callback)
    • 프로미스 성공 실패 상관 없이 무조건 한번 호출
    • return : 언제나 프로미스 반환

프로미스의 에러 처리


  • 방법01 : then 메서드의 두번째 메서드로 에러 처리
    • 프로미스 체이닝시 다른 then 메서드에서 발생하는 에러는 캐치하지 못하여, 코드 복잡해지고 가독성이 떨어짐
  • 방법02 : catch 메서드 (추천)
    • 모든 then 메서드 호출한 이후 호출시 비동기 처리 에러, then 메서드 내부의 에러까지 모두 캐치 가능

프로미스 체이닝


  • then, catch, finally 후속 처리 메서드는 언제나 프로미스를 반환하므로, 연속 호출 가능 함
  • 후속 처리 메서드의 콜백 함수가 프로미스가 아닌 값을 반환해도, 암묵적으로 resolve, reject하여 프로미스를 생성해 반환함

프로미스의 정적 메서드


  • Promise.resolve/reject(value) : 이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용
// Promise.resolve/reject를 활용한 방식
const resolvePromise = Promise.resolve([1, 2, 3]);
resolvePromise.then(console.log); // [1, 2, 3]
const rejectPromise = Promise.reject(new Error("Error!"));
rejectedPromise.catch(console.log); // Error: Error!

// 기존 방식
const resolvePromise = new Promise((resolve) => resolve([1, 2, 3]));
resolvePromise.then(console.log); // [1, 2, 3]
const rejectPromise = new Promise((_, reject) => reject(new Error("Error!")));
rejectedPromise.catch(console.log); // Error: Error!

  • Promise.all([asyncFunc1, asyncFunc2, asyncFunc3, ...])
    • 여러개의 비동기 처리를 모두 병렬 처리할 때 사용함
    • 배열 등의 이터러블을 인수로 전달 받음
    • 모든 프로미스가 모두 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환함
    • 처리한 결과 값은 넣은 비동기 처리 함수 순서 차례 대로 배열에 저장함(완료 순서X)
    • 하나라도 rejected 상태가 되면 나머지 프로미스 fulfiled 상태를 기다리지 않고 즉시 종료 됨
    • 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메서드로 프로미스로 래핑 함
    • map 메서드를 통해서 여러 반복 작업을 비동기 적으로 처리 할 수 있음

  • Promise.race([asyncFunc1, asyncFunc2, asyncFunc3, ...])
    • 배열 등의 이터러블을 인수로 전달 받음
    • 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환
    • 하나라도 rejected 상태가 되면 나머지 프로미스 fulfiled 상태를 기다리지 않고 즉시 종료 되고, 해당 rejected 프로미스를 반환

  • Promise.allSettled([asyncFunc1, asyncFunc2, asyncFunc3, ...])
    • 배열 등의 이터러블을 인수로 전달 받음
    • 전달 받은 프로미스가 모두 settled 상태가 되면 처리 결과를 배열로 반환함
    • fulfilled, rejected 상태 상관 없이 모든 프로미스들의 처리 결과를 나타내는 객체가 담겨 있음
      • 처리 결과 객체의 프로퍼티
        • fulfilled -> status, value
        • rejected -> status, reason



마이크로 태스크 큐


  • 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스큐에 저장 됨
  • 마이크로 태스크 큐는 태스크 큐와 별도의 큐 임
  • 태스크 큐보다 우선순위가 높음
  • 이벤트 루프는 마이크로 태스크 큐의 함수를 먼저 가져와서 실행함
    • 마이크로 태스크 큐가 비면 그때 태스크 큐에서 함수를 가져와 실행 함



fetch


  • XMLHttpRequest 객체와 마찬가지로 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API
  • fetch 함수는 사용법도 간단하고, 프로미스를 지원함
  • fetch(url [, options])
    • HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환함
    • URL만 전달하면 GET 요청을 전송함
    • then을 통해서 fatch 함수가 resolve한 Response 객체를 전달 받을 수 있음
    • Response 객체의 prototype 메서드가 존재함
      • Response.prototype.json() : MIME 타입 - application/json인 응답 몸체 취득
        • 취득시 역직렬화 시켜줌
    • url : HTTP 요청을 전송할 URL
    • options : options 객체
      • method : 'POST', 'PATCH', 'DELETE'
      • headers : {'accept or content-Type':'MIME type'}
      • body : 요청 전송할 body
// fetch를 이용한 요청 함수 만들기
const request = {
  get(url) {
    return fetch(url);
  },
  post(url, payload) {
    return fetch(url, {
      method: "POST",
      headers: { "content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  },
  patch(url, payload) {
    return fetch(url, {
      method: "PATCH",
      headers: { "content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  },
  delete(url) {
    return fetch(url, { method: "DELETE" });
  },
};



제너레이터


  • 제너레이터(generator) : 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수
    • 특징
      • 함수 호출자에게 함수 실행의 제어권을 양도(yield)할 수 있음(함수 호출자가 함수 실행 제어 가능)
      • 함수 호출자에게 상태를 전달할 수 있고 함수 호출자로부터 상태를 전달 받을 수도 있음
      • 제너레이터 함수 호출시 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환함 (함수 코드 실행 X)

  • function*, yield 사용
    • 화살표 함수로 정의 불가
    • new 연산자와 함꼐 생성자 함수로 호출 불가

제너레이터 객체


  • 제너레이터 객체는 이터러블이면서 동시에 이터레이터 임
  • 제너레이터 객체는 next 메서드를 가지는 이터레이터 이므로 Symbol.iterator 메서드를 호출해서 별도로 이터레이터를 생성할 필요가 없음
  • 제너레이터 객체 메서드
    • next() : 제너레이터 함수의 yield 표현식 까지 코드 블록을 실행하고, yield된 값을 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 갖는 이터레이터 result 객체를 반환
      • 인수를 넣으면 yield 표현식을 할당받는 변수에 할당 됨
      • 처음 호출되는 next는 인수를 넣어도 무시됨
    • return(value) : 인수로 전달받은 값을 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 result 객체 반환
    • throw(error) : 전달 받은 에러를 발생시키고 undefined를 value 프로퍼티 값, true를 done 프로퍼티 값으로 갖는 이터레이터 result 객체를 반환

제너레이터의 일시 중지와 재개


  • yield 키워드와 next 메서드를 통해 실행 중지와 재개를 할 수 있음
  • next 메서드 호출시 yield 표현식 까지 실행되고 일시 중지 됨 -> 함수 제어권 호출자로 양도
  • next 호출시 반환된 result 객체에는 yield 표현식 뒤에 값이 value 프로퍼티에 할당되고, done 프로퍼티에 제너레이터 함수가 끝까지 실행 되었는지 불리언 값 할당
function* getFunc() {
  const x = yield 1;
  const y = yield x + 10;
  return x + y;
}
const generator = getFunc();
let res = generator.next();
console.log(res); //  {value: 1, done: false}
res = getnerator.next(10);
console.log(res); //  {value: 20, done: false}
res = getnerator.next(20);
console.log(res); //  {value: 30, done: true}


제너레이터의 활용


  • 간단한 이터러블의 구현
// 기존 이터러블 구현
const infiniteFibonacci = (function () {
  let [pre, cur] = [0, 1]
  return {
    [Symbol.iterator]() { return this}
    next() {
      [pre, cur] = [cur, pre + cur];
      return {value: cur}
    }
  }
})()

// 제너레이터를 이용한 이터러블 구현
const infiniteFibonacci = (function* () {
  let [pre, cur] = [0, 1]

  while (true) {
    [pre, cur] = [cur, pre + cur]
    yield cur
  }
}())
  • 비동기 처리
    • 비동기 처리를 동기 처리 처럼 구현 가능
    • 제너레이터 실행기가 필요하면 co 라이브러리 사용 추천
// 제너레이터 실행기
const async = (generatorFunc) => {
  const generator = generatorFunc();

  const onResolved = (arg) => {
    const result = generator.next(arg);

    return result.done
      ? result.value
      : result.value.then((res) => onResolved(res));
  };

  return onResolved;
};

async(function* fetchTodo() {
  const url = "https://jsonplaceholder.typicode.com/todos/1";
  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo);
})();



async/await


  • 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속으로 처리할 필요 없이 동기 처리 처럼 프로미스를 사용할 수 있게 해줌

  • async 함수
    • async 키워드를 사용해 정의한 함수로, 언제나 프로미스를 반환 함
    • 명시적으로 프로미스를 반환하지 않더라도 암묵적으로 resolve하는 프로미스를 반환함
// async 함수 선언문
async function foo(n) {
  return n;
}

// async 함수 표현식
const bar = async function (n) {
  return n;
};

// async 화살표 함수
const baz = async (n) => n;

// async 메서드
const obj = {
  async foo(n) {
    return n;
  },
};

// async 클래스 메서드
class MyClass {
  async bar(n) {
    return n;
  }
}

  • await 키워드
    • 프로미스가 settled 상태가 될 때까지 대기하다가 settled 상태가 되면 resolved한 처리 결과 반환
    • await는 프로미스 앞에서 사용해야 함
    • 서로 연관 되지 않은 프로미스의 경우 시간을 더 잡아 먹으므로 , await Promise.all을 사용해 처리함

  • 에러 처리
    • 원래 비동기 함수의 콜백 함수를 호출한 것은 비동기 함수가 아니기 때문에 try...catch 문을 사용해 에러를 캐치할 수 없음
    • async/await 에서 try...catch 문으로 에러를 모두 캐치 할 수 있음
      • 프로미스를 반환하는 비동기 함수는 명시적으로 호출 가능하여 호출자가 명확함
    • async 함수 내에서 catch 문을 사용하지 않으면, async 함수는 발생한 에러를 reject하는 프로미스를 반환함



에러 처리


  • 에러가 발생하지 않는 코드를 작성하는 것은 불가능 하고, 에러 발생시 프로그램은 강제 종료 됨
  • try...catch 문을 통해서 프로그램 강제 종료를 막을 수 있음

  • 예외 처리 : 직접적으로 에러를 발생하지는 않는 상황
    • 예외적인 상황에 의해서 다른 코드가 에러를 발생할 수 있음
    • 예) querySelector의 경우 없으면 null을 반환함 -> null을 할당 받은 변수에 프로퍼티를 참조하여 사용하는 경우 error 발생
      • querySelector의 반환 값을 단축 평가 또는 옵셔널 체이닝 연산자 사용해 예외 처리 하거나, 에러 처리 코드를 미리 등록해 두고 에러 발생시 에러 처리 코드로 점프 되도록 하는 방법

try...catch...finally 문


  • 에러 처리(error handling) : try...catch...finally 문을 사용하여 에러를 처리하는 것
    • try : 에러가 발생할 가능성이 있는 코드 블럭
    • catch : try에서 에러 발생시 실행되는 코드 블럭 (try에서 발생한 Error 객체가 전달됨)
    • finally : 에러 발생과 상관 없이 반드시 한번 실행되는 코드 블럭

Error 객체


  • new Error(errorMessage)
    • errorMessage: 에러를 상세히 설명하는 에러 메세지(string)를 인수로 받음
  • message 프로퍼티 : Error 생성자 함수에 전달한 인수를 값으로 갖는 프로퍼티
  • stack 프로퍼티 : 에러를 발생시킨 콜스택의 호출 정보를 나타내는 문자열 (디버깅 목적)
  • 에러 객체 종류 7가지
    • Error : 일반적인 에러 객체
    • SyntaxError : JS 문법에 맞지 않는 문 해석시 발생하는 에러 객체
    • ReferenceError : 참조 함수 없는 식별자를 참조시 발생하는 에러 객체
    • TypeError : 피연산자 또는 인수의 데이터 타입이 유효하지 않을 때
    • RangeError : 숫자값의 허용 범위를 벗어났을 때 발생
    • URIError : encodeURI 또는 decodeURI 함수에 부적절한 인수 전달시 발생
    • EvalError : eval 함수에서 발생하는 에러 객체

thorw 문


  • 해당 에러 객체로 에러를 발생 시킴
  • try 문에 error 발생시, catch 문에 error 변수가 생성되고 해당 error 객체가 할당됨

에러 전파


  • throw된 에러를 캐치 하지 않으면, 에러는 호출자 방향으로 전파 됨 (콜 스택의 아래 방향으로 전파됨)
  • 비동기 함수, 프로미스 후속 처리 메서드의 콜백 함수는 호출자가 없기 때문에 에러 전파가 되지 않음