[Javascript] 비동기 처리

April 27, 2021 · 17 mins read label-icon Javascript

비동기 처리란?

  • 비동기(Asynchronous)
  • 동기(Synchronous)의 반대개념
  • 특정 코드의 실행이 완료되기 전에 다음 코드를 실행하는 것
  • 특정 코드를 기다리지 않고 다음 코드가 실행되는 것

비동기 처리 예제

1
2
3
4
5
6
7
console.log(1)
setTimeout(() => console.log(2),1000)
console.log(3)

// 1
// 3
// (1초 뒤) 2

해당 코드는 비동기 처리로 인해 코드의 순서인 1, 2, 3이 아니라 1, 3이 먼저 실행이 되고 1초 뒤에 2가 실행된다.

콜백

콜백 함수를 사용하면 원하는 순서인 1, 2, 3을 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function num2(callbackFunc){
  setTimeout(() => {
    console.log(2);
    callbackFunc();
  },1000)
}

console.log(1);
num2(function(){
  console.log(3)
})


// 1
// (1초 뒤) 2
// 3

콜백 지옥

위의 예제처럼 콜백함수를 사용해서 비동기를 처리할 수 있지만 처리해야 하는 코드가 많아지면 들여쓰기가 깊어져 가독성도 떨어지고 수정도 어려워지는 콜백 지옥 현상(*)이 일어난다.

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
32
33
function num2(callbackFunc){
  setTimeout(() => {
    console.log(2)
    callbackFunc();
  },1000)
}
function num3(callbackFunc){
  setTimeout(() => {
    console.log(3)
    callbackFunc();
  },1000)
}
function num4(callbackFunc){
  setTimeout(() => {
    console.log(4)
    callbackFunc();
  },1000)
}

console.log(1)
num2(function(){ // (*)
  num3(function(){
    num4(function(){
      console.log('end')
    })
  })
})

// 1
// (1초 뒤) 2
// (1초 뒤) 3
// (1초 뒤) 4
// end

이를 해결하기 위해서는 함수를 분리해주면 해결된다.

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
function num2(){
  setTimeout(() => {
    console.log(2)
    num3();
  },1000)
}
function num3(){
  setTimeout(() => {
    console.log(3)
    num4();
  },1000)
}
function num4(){
  setTimeout(() => {
    console.log(4)
    console.log('end')
  },1000)
}

console.log(1)
num2()

// 1
// (1초 뒤) 2
// (1초 뒤) 3
// (1초 뒤) 4
// end

이런 식으로 함수를 분리하면 콜백지옥 현상이 일어나지 않지만, 코드를 해석할 때 함수명을 계속 추적해야 한다는 단점이 있다. 그래서 ES6에서는 Promise와 Generator 등이 도입되었고, ES2017에서는 async/await가 도입되었다.

Promise

  • 자바스크립트 비동기 처리에 사용되는 객체
  • Promise는 대기(pending), 이행(fulfilled), 거부(rejected) 총 3가지의 상태를 가진다.
  • Promise 객체는 state와 result, 2개의 프로퍼티를 가지고 있다.
  • Promise 객체의 초기값은 state는 "pending", result는 undefined를 가진다.
  • 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 한다. (resolve, reject)
  • resolve와 reject중 먼저 호출된 함수가 호출된다.
1
2
3
4
5
6
7
8
9
10
11
12
let promise = new Promise(function(resolve, reject){
  // resolve나 reject를 반드시 호출
})

console.log(promise);
// Promise 객체의 초기값
/*
{
  state : "pending"
  result: undefined
}
*/

resolve

  • 일이 성공적으로 완료된 경우 결과와 함께 호출
  • Promise 객체의 state는 "fulfilled", result는 해당 결과값으로 변한다.
1
2
3
4
5
6
7
8
9
10
11
12
let promise = new Promise(function(resolve, reject){
  resolve('성공!');
})

console.log(promise);
// resolve 호출 시의 Promise 객체
/*
{
  state : "fulfilled"
  result: "성공!"
}
*/

reject

  • 에러 발생 시 에러 객체인 error와 함께 호출
  • Promise 객체의 state는 "rejected", result는 해당 에러값으로 변한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise(function(resolve, reject){
  reject(new Error('에러 발생!'));
})

console.log(promise);
// reject 호출 시의 Promise 객체
/*
{
  state : "rejected"
  result: "에러 발생!"
}
Error: 에러 발생!
*/

then

  • 첫 번째 인수는 이행(성공)됐을 경우 실행, 두 번째 인수는 거부(에러)됐을 경우 실행
    1
    2
    3
    4
    
    promise.then(
      function(result){/* result를 다룸 */},
      function(error){/* error를 다룸 */}
    )
    
  • 이행(성공)된 경우
    1
    2
    3
    4
    5
    6
    7
    8
    
    let promise = new Promise(function(resolve, reject) {
      resolve("성공!")
    });
    
    promise.then(
      result => console.log(result), // "성공"
      error => console.log(error) // 실행되지 않음
    );
    
  • 거부(에러)된 경우
    1
    2
    3
    4
    5
    6
    7
    8
    
    let promise = new Promise(function(resolve, reject) {
      reject(new Error('에러 발생!'));
    });
    
    promise.then(
      result => console.log(result), // 실행되지 않음
      error => console.log(error) // Error: 에러 발생!
    );
    

catch

  • 에러가 발생한 경우만 다룰 경우 사용
  • then의 두 번째 인수와 동일
    1
    2
    3
    4
    5
    6
    7
    
    let promise = new Promise(function(resolve, reject) {
      reject(new Error('에러 발생!'));
    });
    
    promise.catch(
      error => console.log(error) // Error: 에러 발생!
    );
    

finally

  • 이행이나 거부 관계없이 항상 실행됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 이행(성공)된 경우

let promise = new Promise(function(resolve, reject) {
  resolve('성공!');
});

promise
.finally(() => console.log('실행!'))
.then(
  result => console.log(result),
  error => console.log(error) // 실행되지 않음
);

// 실행!
// 성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 거부(에러)된 경우

let promise = new Promise(function(resolve, reject) {
  reject(new Error('에러 발생!'));
});

promise
.finally(() => console.log('실행!'))
.then(
  result => console.log(result), // 실행되지 않음
  error => console.log(error)
);

// 실행!
// Error: 에러 발생!

Promise 체이닝

  • Promise의 결과값이 then 핸들러의 체인(사슬)을 통해 전달되는 것
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    new Promise(function(resolve, reject) {
      resolve(1);
    }).then(function(result){
      console.log(result); // 1
      return result * 2;
    }).then(function(result){
      console.log(result); // 2
      return result * 2;
    }).then(function(result){
      console.log(result); // 4
      return result * 2;
    });
    
  • fetch를 사용하는 경우 자주 사용된다.
    1
    2
    3
    4
    5
    6
    7
    
    fetch('https://ko.javascript.info/article/promise-chaining/user.json')
    .then(function(res){
      return res.json() // JSON으로 파싱
    })
    .then(function(user){
      console.log(user.name) // iliakan
    })
    

Promise 메서드

Promise.all

  • 요소 전체가 Promise인 배열을 받아 새로운 Promise를 반환하는 메서드
  • 모든 Promise가 이행될 때까지 기다렸다가 결과값을 반환한다.
  • Promise.all에 전달되는 값이 하나라도 거부되면 반환하는 값은 에러와 함께 거부되고 나머지 결과는 무시된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

let requests = urls.map(url => fetch(url));

console.log('requests',requests); // [Promise, Promise, Promise]

Promise.all(requests)
.then(responses => responses.forEach(
  response => console.log(`${response.url}: ${response.status}`)
));

// https://api.github.com/users/iliakan: 200
// https://api.github.com/users/remy: 200
// https://api.github.com/users/jeresig: 200

Promise.race

  • all과 비슷하지만 먼저 처리되는 Promise의 값 또는 에러를 반환한다.
  • race = 경주
1
2
3
4
5
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(console.log); // 1

async/await

  • async/await를 사용하면 Promise를 더욱 편리하게 사용할 수 있다.

async

  • 함수 앞에 위치한다.
  • async를 추가한 함수는 await를 사용할 수 있다.
  • async를 추가한 함수는 항상 Promise를 반환한다.
1
2
3
4
5
6
async function func(){
  return '반환값!';
}

console.log(func()); // Promise {...}
func().then(res => console.log(res)) // '반환값!'

await

  • async 함수 안에서만 동작한다.
  • Promise가 처리될 때까지 기다린 뒤 결과를 반환한다. (await = 기다리다)
1
2
3
4
5
6
7
8
9
10
11
12
// 이행(성공)된 경우

async function func() {
  try {
    let response = await fetch('https://ko.javascript.info/article/promise-chaining/user.json');
    let user = await response.json();
    console.log(user) // {name: "iliakan", isAdmin: true}
  } catch(err) {
    console.log(err); // 실행되지 않음
  }
}
func();
1
2
3
4
5
6
7
8
9
10
11
12
// 거부(에러)된 경우

async function func() {
  try {
    let response = await fetch('http://유효하지-않은-url');
    let user = await response.json();
    console.log(user) // 실행되지 않음
  } catch(err) {
    console.log(err); // TypeError: Failed to fetch
  }
}
func();

참고

https://ko.javascript.info/async