async 함수

async는 function 앞에 위치합니다.

function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환합니다. 

async function f(){
   return 1;
}

 

// 위와 같은 함수
async function f() {
  return Promise.resolve(1);
}

await 함수

await async 함수 안에서만 동작합니다. await는 말 그대로 프라미스가 처리될 때까지 함수 실행을 기다리게 만듭니다. 처리되면 그 결과와 함께 실행이 재개됩니다.

프라미스가 처리되길 기다리는 동안엔 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않습니다.

 

예시

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

 

 

await promise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법입니다.

 

promise .then 을   async/await 로 변환 예시

 

promise chaning 에서의 예시

fetch('/article/promise-chaining/user.json')
  .then(response => response.json()) // json 읽기
  .then(user => fetch(`https://api.github.com/users/${user.name}`)) // 사용자 정보 읽기
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // 아바타 보여주기
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // 
    }, 3000);
  }))
  // 3초 후 동작함
  .then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`));

 

async / await  로 변환

async function showAvatar() {

  // JSON 읽기
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // github 사용자 정보 읽기
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // 아바타 보여주기
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // 3초 대기
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

 

 

 

다른 사용예시들

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();

 

 

에러 핸들링

try{ ... } catch { ... } 를 이용

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
    let user = await response.json();
  } catch(err) {
    // fetch와 response.json에서 발행한 에러 모두를 여기서 잡습니다.
    alert(err);
  }
}

 

 

다시던지기를 이용한 코드 예시

 

.then 이용

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new HttpError(response);
      }
    })
}

// 유효한 사용자를 찾을 때까지 반복해서 username을 물어봄
function demoGithubUser() {
  let name = prompt("GitHub username을 입력하세요.", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`이름: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("일치하는 사용자가 없습니다. 다시 입력해 주세요.");
        return demoGithubUser();
      } else {
        throw err;
      }
    });
}

demoGithubUser();

async/ awiat 이용

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

async function loadJson(url) {
  let response = await fetch(url);
  if (response.status == 200) {
    return response.json();
  } else {
    throw new HttpError(response);
  }
}

// 유효한 사용자를 찾을 때까지 반복해서 username을 물어봄
async function demoGithubUser() {

  let user;
  while(true) {
    let name = prompt("GitHub username을 입력하세요.", "iliakan");

    try {
      user = await loadJson(`https://api.github.com/users/${name}`);
      break; // 에러가 없으므로 반복문을 빠져나옵니다.
    } catch(err) {
      if (err instanceof HttpError && err.response.status == 404) {
        // 얼럿 창이 뜬 이후에 반복문은 계속 돕니다.
        alert("일치하는 사용자가 없습니다. 다시 입력해 주세요.");
      } else {
        // 알 수 없는 에러는 다시 던져집니다.
        throw err;
      }
    }
  }


  alert(`이름: ${user.name}.`);
  return user;
}

demoGithubUser();

 

예제

 

aysnc / await 이용하여 코드변경

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    })
}

loadJson('no-such-user.json')
  .catch(alert); // Error: 404

 

      변경

async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }

  throw new Error(response.status);
}

loadJson('no-such-user.json')
  .catch(alert); // Error: 404 (4)

 

 

참고자료

https://ko.javascript.info/async-await

프라미스화

콜백을 받는 함수를 프라미스를 반환하는 함수로 바꾸는 것을 '프라미스화(promisification)'라고 합니다.

이는 자바스크립트에서 비동기 작업을 더 깔끔하게 처리할 수 있게 해줍니다.

따라서 콜백보다는 프라미스가 더 편리하기 때문에 콜백 기반 함수와 라이브러리를 프라미스를 반환하는 함수로 바꾸는 게 좋은 경우가 생깁니다.

 

예시 코드

 콜백

function loadData(callback) {
  setTimeout(() => {
    callback('데이터 로드 완료');
  }, 1000);
}

loadData(data => {
  console.log(data); // 데이터 로드 완료
});

 

 프라미스화

function loadData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('데이터 로드 완료');
    }, 1000);
  });
}

loadData().then(data => {
  console.log(data); // 데이터 로드 완료
});

 

f가 두 개를 초과하는 인수를 가진 콜백, callback(err, res1, res2, ...)을 받는경우 처리하는 방법

새롭게 만든 함수를 promisify(f, true)형태로 호출

// 콜백의 성공 결과를 담은 배열을 얻게 해주는 promisify(f, true)
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // f에 사용할 커스텀 콜백
        if (err) {
          reject(err);
        } else {
          // manyArgs가 구체적으로 명시되었다면, 콜백의 성공 케이스와 함께 이행 상태가 됩니다.
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
};

// 사용법:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)

 

 

참고자료

https://ko.javascript.info/promisify

프라미스 API

Promise.all( promises )

주어진 배열에 있는 모든 프로미스가 이행될 때까지 기다렸다가 그 결과값을 주어진 배열의 순서에 따라 반환합니다.

 예시

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // [1,2,3] 반환

 

배열 속 프로미스중 하나라도 실패를 하면 promise.all  전체가 거부되고 에러가 반환됩니다.

   예시

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: 에러 발생!

Promise.allSettled( promises )

최근에 추가된 문법

Promise.all 과 다르게 여러개의 요청 중에 하나가 실패해도 다른 성공된 결과와 함께 반환

여러 요청 중 하나가 실패해도 다른 요청 결과는 여전히 필요한 경우에 유용한 메서드

     

요청 예시

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/Violet-Bora-Lee',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

 

결과 예시

[
  {status: 'fulfilled', value: ...응답...},
  {status: 'fulfilled', value: ...응답...},
  {status: 'rejected', reason: ...에러 객체...}
]

 

만약에 브라우저가 promise.allSettled 를 지원하지 않는다면 폴리필을 구현

   폴리필? 브라우저에서 지원하지 않는 코드를 사용한 가능한 코드나 프롤그인으로 변환 한 코드를 의미합니다.

if(!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({
      status: 'fulfilled',
      value
    }), reason => ({
      status: 'rejected',
      reason
    }))));
  };
}

 

Promise.race( promises )

Promise.all과 비슷하지만 가장 먼저 처리되는 프라미스의 결과(혹은 에러)를 반환합니다.

 예시

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

 

Promise.resolve( value )

주어진 값을 이용하여 이행 상태의 프라미스를 만듭니다.

Promise.reject( error )

주어진 에러를 사용해 거부 상태의 프라미스를 만듭니다

 

* 메서드 Promise.resolve Promise.reject async/await 문법이 생긴 후로 쓸모없어졌기 때문에 근래에는 거의 사용하지 않습니다.

 

참고자료

https://ko.javascript.info/async

프라미스와 에러 핸들링

암시적 try…catch

체인 마지막의 .catch는 try..catch와 유사한 역할을 합니다.

 .then 핸들러를 원하는 만큼 사용하다 마지막에 .catch 하나만 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있습니다.

 

.catch 는 프라미스에서 발생한 모든 에러를 다룹니다. reject()가 호출되거나 에러가 던져지면 .catch에서 이를 처리합니다.

 

예시

new Promise((resolve, reject) => {
  resolve("OK");
}).then((result) => {
  throw new Error("에러 발생!"); // 프라미스가 거부됨
}).catch(alert); // Error: 에러 발생!
new Promise((resolve, reject) => {
  resolve("OK");
}).then((result) => {
  blabla(); // 존재하지 않는 함수
}).catch(alert); // ReferenceError: blabla is not defined

예시에서 보듯 .catch는 첫번째 핸들러일 필요가 없고 하나 혹은 여러 개의 .then 뒤에 올 수 있습니다.

 

사이트에는 아무런 문제가 없지만 응답으로 받은 JSON의 형식이 잘못된 경우를 살펴봅시다.

가장 쉬운 에러 처리 방법은 체인 끝에 .catch 를 붙이는 것입니다.

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

 정상적인 경우라면 .catch 는 절대 트리거 되지 않습니다. 하지만, 네트워크 문제, 잘못된 형식의 JSON 등으로 인해 위쪽 프라미스 중 하나라도 거부 되면 .catch 에서 에러를 잡게 됩니다.

 

다시 던지기

 try..catch에선 에러를 분석하고, 처리할 수 없는 에러라 판단되면 에러를 다시 던질 때가 있습니다.

프라미스에도 유사한 일을 할 수 있습니다.

.catch 안에서 throw 를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어갑니다.

여기서 에러가 성공적으로 처리되면 바로 다음 .then 핸들러로 제어 흐름이 넘어가 실행이 이어집니다.

 

예시

new Promise((resolve, reject) => {

  throw new Error("에러 발생!");

}).catch(function(error) {

  alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");

}).then(() => alert("다음 핸들러가 실행됩니다."));

    - catch 에서 정상적으로 종료되었기 때문에 다음 성공핸들러 .then 이 호출 되것을 확인 가능합니다.

 

catch 에서 에러를 처리하지 못한 예시

 // 실행 순서: catch -> catch
new Promise((resolve, reject) => {

  throw new Error("에러 발생!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // 에러 처리
  } else {
    alert("처리할 수 없는 에러");

    throw error; // 에러 다시 던지기
  }

}).then(function() {
  /* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)

  alert(`알 수 없는 에러가 발생함: ${error}`);
  // 반환값이 없음 => 실행이 계속됨

});

 

처리되지 못한 거부

에러를 처리해줄 핸들러가 없으면 에러가 갇혀버리게 됩니다.

  예시

new Promise(function() {
  noSuchFunction(); // 존재하지 않는 함수를 호출하기 때문에 에러가 발생함
})
  .then(() => {
    // 성공상태의 프라미스를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음
  }); // 끝에 .catch가 없음!

 이런 경우 스크립트가 죽고 자바스크립트 엔진은 전역 에러를 생성합니다.

이떄 콘솔 창을 열어 확인하면 전역 에러를 확인 가능합니다.

브라우저 환경에서는 이러한 에러를 unhandledrejection 이벤트로 처리할 수 있습니다.

window.addEventListener('unhandledrejection', function(event) {
  // unhandledrejection 이벤트엔 두 개의 특수 프로퍼티가 있습니다.
  alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
  alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});

new Promise(function() {
  throw new Error("에러 발생!");
}); // 에러를 처리할 수 있는 .catch 핸들러가 없음

     이런 에러는 회복할 수 없기 때문에 최선의 방법은 사용자에게 문제 상황을 알리고 가능하다면 서버에 에러 정보를 보내는 것입니다.

 

.catch 가 실행되지 않는 예제

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("에러 발생!");
  }, 1000);
}).catch(alert);

  위 예시에서는 executer 가 실행되는 동안이 아니라 나중에 에러가 발생합니다. 그래서 프라미스는 에러를 처리 할 수 없습니다.

 

참고자료

https://ko.javascript.info/async

프라미스 체이닝(promise chaining)

result.then 핸들러의 체인을 통해 전달된다는 점에서 착안한 아이디어 

 

예시

- fetch 이용

fetch('/article/promise-chaining/user.json')
  // 원격 서버가 응답하면 .then 아래 코드가 실행됩니다.
  .then(function(response) {
    // response.text()는 응답 텍스트 전체가 다운로드되면
    // 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환합니다.
    return response.text();
  })
  .then(function(text) {
    // 원격에서 받아온 파일의 내용
    alert(text); // {"name": "Violet-Bora-Lee", "isAdmin": true}
  });

 

-- 간단하게 변경

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name));

 

- fetch  + 함수

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // 3초 후 동작함
  .then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`))

-- 간략화 ( 함수단위로 분해)

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

 

참고자료

https://ko.javascript.info/async

+ Recent posts