[Javascript] - 자바스크립트에서의 비동기 처리

 # JavaScript의 비동기 내장함수 종류

 자바스크립트는 단일 스레드(Single-threaded)언어임에도 불구하고, 시간이 오래 걸리는 작업들을 효율적으로 처리하기 위해서 다양한 비동기 처리를 지원하는 내장함수를 가지고 있다.

 

1. Callback 함수 : setTimeout(callback, delay), setInterval(callback, interval)

function delay(callback) {
  setTimeout(function() {
  console.log('2초 후 실행');
  callback();
 }, 2000);
}

console.log('시작');

delay(function() {
  console.log('종료');
});

console.log("1초지남");

 

 Callback 함수는  자바스크립트의 가장 기본적인 비동기 처리함수로서 setTimeout(), setInterval()과 같은 함수가 존재한다. 위 예제코드에서 보이는 것처럼 delay 함수의 매개변수로 

 

function() {
  console.log('종료');
}


위 console.log("종료")를 출력하는 함수가 매개변수(callback)로 사용되었는데  callback() 함수가 실행되기전에 
console.log("2초 후 실행") 과 같이 비동기적으로 처리할 코드를 작성한다. 

 

따라서 위 코드에서 console.log()는 아래와 같은 순서로 찍힐 것이다.

'시작' -> '1초지남' -> '2초 후 실행' -> '종료'

 

 

하지만 이와 같은 callback 함수는 콜백 함수를 중첩하여 사용하는 경우 코드가 복잡해지는

이른바 콜백지옥(callback hell)이라는 문제가 발생하기도 한다.

 

 

< Callback Hell> 

function task1(callback) {
    setTimeout(function() {
        console.log('Task 1 완료.');
        callback();
    }, 300);
}

function task2(callback) {
    setTimeout(function() {
        console.log('Task 2 완료.');
        callback();
    }, 200);
}

function task3(callback) {
    setTimeout(function() {
        console.log('Task 3 완료.');
        callback();
    }, 100);
}

task1(function() {
    task2(function() {
        task3(function() {
            console.log('모든 작업 완료.');
        });
    });
});

 

 

2. Promise

위와 같은 callback 함수의 콜백 지옥(callback hell) 문제를 해결하기 위해 새로운 비동기 처리 방법이다.

Promise는 비동기 작업의 최종완료(Fulfiled), 최종실패(Rejected)를 나타내는 객체이다.

 

 Promise는 Pending, Fulfilled, Rejected 세 가지 상태를 가지며 then(), catch() 메소드를 통해

비동기 작업의 결과를 처리한다.

 

• then() - 성공(resolve) 시에는 then 메서드에 실행할 콜백 함수를 인자로 넘긴다.
• catch() - 실패(reject) 시에는 catch 메서드에 실행할 콜백 함수를 인자로 넘긴다.
• finally() - 성공/실패 여부와 상관없이 모두 실행 시에는 finally 메서드에 실행할 콜백 함수를 인자로 넘긴다.

 

let promise = new Promise(function(resolve, reject) {
    console.log("비동기 작업 시작");

   
    setTimeout(function() {
        let success = Math.random() > 0.5; // 50% 확률로 작업 성공/실패

        if (success) {
            console.log("비동기 작업 성공");
            resolve("성공한 데이터");
        } else {
            console.log("비동기 작업 실패");
            reject(new Error("실패한 이유"));
        }
    }, 1000);
});

console.log("프로미스 생성 완료, 비동기 작업 시작 대기");

promise.then(function(data) {
    console.log("비동기 작업 종료, 결과 데이터: " + data);
}).catch(function(error) {
    console.log("비동기 작업 종료, 발생한 에러: " + error);
});

console.log("비동기 작업이 진행중, 프로그램은 계속 실행됩니다");

 

위 코드가 실행되면 아래와 같은 순서대로 console.log()가 출력된다.

 

1. "프로미스 생성 완료, 비동기 작업 시작 대기"
2. "비동기 작업이 진행중, 프로그램은 계속 실행됩니다"
3. "비동기 작업 시작"
4. "비동기 작업 성공"  or  "비동기 작업 실패"
5. "비동기 작업 종료, 결과 데이터: 성공한 데이터"   or   "비동기 작업 종료, 발생한 에러: Error: 실패한 이유"

 

 

3. fetch API

 Fetch 함수는 ES6 이후에 추가된 웹 브라우저 API 중 하나로서 비동기 네트워크 요청에 사용되며 Promise 기반으로 작성되어있기 때문에 비동기 처리에 편리하다. 마찬가지로 Promise의 후속 처리 메소드인 then(), catch()를 통해 처리한다.

 

    fetch('/loginAction', {
        method: 'POST',
        body: formData,
    })
        .then(response => {
            if (response.ok) { // HTTP 상태 코드가 200-299인 경우 response.ok는 true입니다.
                alert('로그인에 성공하였습니다!');
                location.href = "/";
            } else {
                return response.text(); // 에러 메시지를 받아옵니다.
            }
        })
        .then(message => { // 로그인 실패 시 에러 메시지를 출력합니다.
            if (message) {
                alert('로그인 실패 : ' + message);
            }
        })
        .catch((error) => {
            console.error('Error:', error);
        });

 

 

4. async, await 

async, await은 Promise를 더욱 쉽게 사용할 수 있도록 하는 문법이다.

async 함수는 항상 Promise를 반환하며, await 키워드는 항상 Promise의 결과를 기다린다.

 

<1번 코드>

const axios = require('axios');

async function fetchFromApi() {
console.log('1');
const response = axios.get('https://api.example.com/data');
console.log('2');
return response.data;
}

console.log('A');
fetchFromApi().then(data => console.log('Data:', data));
console.log('B');

 

위 코드의 console.log() 순서는 await 키워드가 사용되지 않았기 때문에 아래와 같을 것이다.

 

A

1

2

B

Data : undefined

 

 axios.get() 함수는 Promise를 반환하는데 await 키워드를 사용하지 않았기 때문에

axios.get() 함수가 즉시 Promise를 반환하고 다음줄 코드를 실행해버린다.

그래서 A -> 1 -> 2 B 순으로 출력되며 마지막 Data : undefined 인 이유는 

 

const response = axios.get('https://api.example.com/data'); 이후에 response는 아직 결과 값을 가지지 않은

Promise 객체로서 즉, Pending 상태이다.

 

따라서 response.data는 undefined인 것이다.

 

 

<2번 코드>

const axios = require('axios');

async function fetchFromApi() {
    console.log('1');
    const response = await axios.get('https://api.example.com/data');
    console.log('2');
    return response.data;
}

console.log('A');
fetchFromApi().then(data => console.log('Data:', data));
console.log('B');

2번 코드는 await 코드를 사용했기 때문에 console.log() 순서가 아래와 같이 출력될 것이다.

 

A

1

B

2

Data : {...서버에서 받은 data}

 

 await 키워드를 사용하면 자바스크립트는 해당 Promise 객체가 resolve 될 때하기 함수의 실행을 일시 중지한다.

따라서 1과 2사이의 선언된 const response에는 네트워크 요청이 완료된 promise 객체의 결과값이 존재한다.

 

그렇기 때문에 Data도 undefined가 아닌 정상적으로 서버에서 받은 Data가 존재하게 된다.