# [ES2017] 비동기 함수 Async Await

동기식 코드와 거의 같은 구조의 비동기식 코드를 짤 수 있다.

  • Sync 서브루틴이 즉시 값을 반환
  • Async 서브루틴이 다른 수단으로 값을 반환
Sync Async 단점
서브루틴의 반환 즉시 값 반환 다른 수단으로 값 반환.
- 즉시 반환되지 않으므로 현재의 sync flow 가 종료됨
현재 어휘공간 사용 O X (클로저나, 복잡한 스코프 사용해야함)
- 요청 시의 어휘공간을 별도로 결과시점에 전달할 부가장치 필요.

# async 키워드

function 앞에 위치함

비동기 함수

async function func1() {
  // ...
}
1
2
3

비동기 화살표 함수

const func2 = async () => {
  // ...
}
1
2
3

비동기 메소드

class MyClass {
  async myMethod() {
    // ...
  }
}
1
2
3
4
5

# 항상 Promise 객체를 반환한다.

function 이 Promise 가 아닌 값을 반환해도, 이행 상태의 Promise 로 감싸 반환한다.

async function func1() {
  return 1;
}
/*
func1()
Promise {
    [[PromiseState]]: "fulfilled"
    [[PromiseResult]]: 1
}
*/
1
2
3
4
5
6
7
8
9
10

Promise 의 명시적 반환

비동기 함수 내에서 return 한 값이, Promise 객체의 [[PromiseResult]]

async function func2() {
  return Promise.resolve(2);
}
/*
func2()
Promise {
    [[PromiseState]]: "fulfilled"
    [[PromiseResult]]: 2
}
*/

async function func4() {
  return Promise.reject(2);
}

/* 
func4()
Promise {
    [[PromiseState]]: "rejected"
    [[PromiseResult]]: 2
}
*/ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

비동기 함수에서 return 을 해주지 않았을 때,

async function func3() { }
/*
func3()
Promise {
    [[PromiseState]]: "fulfilled"
    [[PromiseResult]]: undefined
}
*/
1
2
3
4
5
6
7
8

# await 키워드

  • async 함수 안에서만 동작
  • await 키워드 뒤에 오는 Promise 가 처리(settled) 될 때까지 Suspend 된다
    • Promise 가 처리되길 기다리는 동안엔 엔진이 다른일(다른 스크립트 실행, 이벤트 처리 등)을 하기 때문에 CPU 리소스가 낭비되지 않는다.
  • await 연산의 결과 값
    • 뒤에 오는 Promise 객체의 [[PromiseResult]]
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`${ms} 밀리초가 지났습니다.`);
      resolve()
    }, ms);
  });
}

async function main() {
  await delay(1000);
  await delay(2000);
  const result = await Promise.resolve('끝');
  console.log(result);
}

main();
//  1000 밀리초가 지났습니다.
//  2000 밀리초가 지났습니다.
//  끝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

await 은 최상위 레벨 코드에서 동작하지 않으므로 IIFE 을 사용하자

  • Deno 는 top-level await 을 지원한다.
(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  // ...
})();
1
2
3
4
5

await 은 thenable 객체를 받는다.

  • resolve, reject 를 인수로 제공하는 .then 메서드를 호출한다.
class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000밀리초 후에 이행됨(result는 this.num*2)
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
};

async function f() {
  // 1초 후, 변수 result는 2가 됨
  let result = await new Thenable(1);
  alert(result);
}

f();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

async 가 아닌 함수에서 async 함수 호출하기

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return 10;
}

function f() {
  // shows 10 after 1 second
  wait().then(result => alert(result));
}
f();
1
2
3
4
5
6
7
8
9
10

# async/await 이 promise 를 리턴할 때

// bad
AddUser: async (userName) => {
  await fetch(`${ADDRESS}/api/users`, ApiOptions.POST({name: userName}));
},

// good
AddUser: userName =>  fetch(`${ADDRESS}/api/users`, ApiOptions.POST({name: userName})),
1
2
3
4
5
6
7

promise를 바로 return 하면 굳이 async/await을 사용하지 않아도 된답니다!

# 에러 처리

async function f() {
  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}
f();
1
2
3
4
5
6
7
8
async function f() {
  let response = await fetch('http://유효하지-않은-url');
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)
1
2
3
4
5
6

예외 처리를 해주지 않으면. 처리되지 않은 Promise 에러가 발생한다.

  • unhandledrejection 전역 이벤트 핸들러를 이용해 잡을 수 있다.

# Promise -> 비동기 함수

앞서 나오던 복잡한 예제의 Callback Promise 코드와 비교해 보자

const axios = require('axios');
const API_URL = 'https://api.github.com';

async function fetchStarCount() {
  const starCount = {};

  // 1. Github에 공개되어있는 저장소 중, 언어가 JavaScript이고 별표를 가장 많이 받은 저장소를 불러온다.
  const topRepoRes = await axios.get(`${API_URL}/search/repositories?q=language:javascript&sort=stars&per_page=1`);

  // 2. 위 저장소에 가장 많이 기여한 기여자 5명의 정보를 불러온다.
  const topMemberRes = await axios.get(`${API_URL}/repos/${topRepoRes.data.items[0].full_name}/contributors?per_page=5`);

  // 3. 해당 기여자들이 최근에 Github에서 별표를 한 저장소를 각각 10개씩 불러온다.
  const ps = topMemberRes.data.map(user => axios.get(`${API_URL}/users/${user.login}/starred?per_page=10`));
  const starredReposRess = await Promise.all(ps);
  const starredReposData = starredReposRess.map(r => r.data);

  // 4. 불러온 저장소를 모두 모아, 개수를 센 후 저장소의 이름을 개수와 함께 출력한다.
  for (let repoArr of starredReposData) {
    for (let repo of repoArr) {
      if (repo.full_name in starCount) {
        starCount[repo.full_name]++;
      } else {
        starCount[repo.full_name] = 1;
      }
    }
  }
  return starCount;
}

fetchStarCount().then(console.log);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
1
2
3
4
5
6
7
8
9
10

# setTimeout 의 컨버터 timeout 함수

res(f())

  • f의 리턴값을 [[PromiseResult]]으로 가지는 Promise 객체
const timeout = (f, ms) => new Promise(res => setTimeout(_ => res(f()), ms));
const f1 = _ => 'abc';
const f2 = _ => 'def';
const start = performance.now();
1
2
3
4

performance.now() 브라우저 실행시각을 기준으로 시간이 count 되며 나노세컨드의 더 정확한 시간계산이 가능해진다.

const promise1 = new Promise(res => res(f1()));
promise1.then(console.log);
1
2
const promise2 = (async () => f2())();
promise2.then(console.log);
1
2

이 코드의 총 실행시간은 어떻게 될까? 약 1000ms (병렬 실행)

(() => {
    timeout(f1, 500).then(v => console.log(v, performance.now() - start));
})();
(() => {
    timeout(f2, 1000).then(v => console.log(v, performance.now() - start));
})();
1
2
3
4
5
6
(async(f, ms) => {
    console.log(await timeout(f1, ms), performance.now() - start);
})(f1, 500);
(async(f, ms) => {
    console.log(await timeout(f2, ms), performance.now() - start);
})(f2, 1000);
1
2
3
4
5
6

약 1500 ms 에 수행될 것이다.

(() => {
    timeout(f1, 500)
    .then(v => {
        console.log(v, performance.now() - start);
        return timeout(f2, 1000);
    })
    .then(v => console.log(v, performance.now() - start));
})();
1
2
3
4
5
6
7
8
(async () => {
    console.log(await timeout(f1, 500), performance.now() - start);
    console.log(await timeout(f2, 1000), performance.now() - start);
})();
1
2
3
4
(async() => {
    console.log(
        await timeout(f1, 500), performance.now() - start,
        await timeout(f2, 1000), performance.now() - start,
    )
})();
1
2
3
4
5
6

약 1000 ms

(async () => {
    const [v1, v2] = await Promise.all([timeout(f1, 500), timeout(f2, 1000)]);
    console.log(v1, v2, performance.now() - start);
})
1
2
3
4

약 500 ms

(async () => {
    const v = await Promise.race([timeout(f1, 500), timeout(f2, 1000)]);
    console.log(v, performance.now() - start);
})
1
2
3
4

timeout fetch
timeout 이 지원되는 fetch 함수. fetch 에 timeout 이 없다.

const api = async(url, timeout = 5000, info = {}) => {
    // fetch 에 Throw 가 날 가능성이 있기 때문에 붙여줌. Promise 객체가 에러 결과값을 갖고 await 을 만나면 try catch 에서 감지 된다.
    try {
        let id = -1;    // setTimeout 은 어떤 경우에도 id 를 -1 로 리턴하는 경우가 없다.
        const v = await Promise.race([  // v 는 undefined 가 오든지 fetch 의 Response 객체가 올 것이다.
            new Promise(res => id = window.setTimeout(_ => res(), timeout)),
            fetch(new Request(url, info))
        ]);
        
        if (v instanceof Response) {
            clearTimeout(id);
            return v.status === 404 ? new Error('404') : await v.text();
        }
        else return new Error('timeout');   
    }
    catch (e) {
        return e;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(async() => {
    const v = await api("200.html", 1);
    if (v instanceof Error) console.log(`error: ${v}`);
    else console.log(`contents: ${v}`);
})();
1
2
3
4
5

더 친화적인 async await 의 사용방법은 에러객체를 리턴하는 것이 아닌 throw 를 하는 것이다.

api 를 사용하는 쪽에서 try catch 를 사용하면 된다.

const api = async(url, timeout = 5000, info = {}) => {
    let id = -1;    // setTimeout 은 어떤 경우에도 id 를 -1 로 리턴하는 경우가 없다.
    const v = await Promise.race([  // v 는 undefined 가 오든지 fetch 의 Response 객체가 올 것이다.
        new Promise(res => id = window.setTimeout(_ => res(), timeout)),
        fetch(new Request(url, info))
    ]);
    
    if (v instanceof Response) {
        clearTimeout(id);
        if(v.status === 404) new Error('404');
        return await v.text()
    }
    else new Error('timeout');   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

비동기 명령보다 동기 명령이 훨씬 편하다.

  • 지역변수를 사용할 수 있기 때문이다.
  • 클로저를 이용해도 외부 스코프의 변수가 가까이 있다.
const api = async(url, timeout = 5000, info = {}) => {
    let id = -1;    // setTimeout 은 어떤 경우에도 id 를 -1 로 리턴하는 경우가 없다.
    const v = await Promise.race([  // v 는 undefined 가 오든지 fetch 의 Response 객체가 올 것이다.
        new Promise(res => id = window.setTimeout(_ => res(), timeout)),
        fetch(new Request(url, info))
    ]);
    
    if (v instanceof Response) {
        clearTimeout(id);
        if(v.status === 404) new Error('404');
        return await v.json()
    }
    else new Error('timeout');   
};

(async () => {
    try {
        const { id, nick, thumb } = await api('/member');
        const [ {name, email, sex }, friendsId] = await Promise.all([api(`/detail/${id}`), api(`/friends/${id}`)]);
        updateMember(nick, thumb, name, email, sex);
        updateFriends(
            (await Promise.all(friendsId.map(id => api(`/detail/${id}`)))).map((v, idx) => ({id: friendsId[idx], ...v}));
        );
    } catch (e) {
        console.log(e); // json parsing 에러는 throw 를 던지면 여기서 캐치 
    }
})();
const updateMember = (nick, thumb, name, email, sex) => {}
const updateFriends = (details) => details.map(({id, name, email, sex}) => {} ) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# Reference & Comment

  • https://gitlab.com/siots-study/topics/-/wikis/asyncronous
  • https://www.youtube.com/watch?v=JaHlR1IGLN8&list=PL7jH19IHhOLMmmjrwCi7-dMFVdoU0hhgF
  • https://helloworldjavascript.net/pages/285-async.html
  • https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
  • https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
  • https://ko.javascript.info/async
  • https://ko.javascript.info/generators-iterators
  • https://www.bsidesoft.com/8325
  • https://www.bsidesoft.com/6037