Front/JavaScript

비동기(Async) - javascript 기본

oodada 2024. 7. 6. 22:17

동기(Synchronous)와 비동기(Asynchronous)

01. 개요

- 동기 처리

동기 처리란, 작업을 순차적으로 처리하는 것을 말한다.

자바스크립트 코드는 기본적으로 동기적으로 처리된다.
동기적으로 처리되는 코드는 위에서부터 아래로 순차적으로 실행되며, 어떤 작업이 끝나야 다음 작업을 수행할 수 있다.

하지만, 동기적으로 처리되는 코드는 작업이 끝날 때까지 다른 작업을 수행할 수 없다는 단점이 있다.

  • 은행에서 번호 순서대로 업무를 처리하는 것, 순차적으로 처리되는 것
console.log('은행 1번 번호표 업무 시작');
console.log('은행 1번 번호표 업무 끝');

console.log('은행 2번 번호표 업무 시작');
console.log('은행 2번 번호표 업무 끝');

- 비동기 처리

비동기 처리란, 작업을 동시에 처리하는 것을 말한다.

비동기 처리는 이러한 단점을 보완하기 위해 등장했다.
비동기 처리는 작업을 동시에 처리할 수 있으며, 작업이 끝나지 않아도 다른 작업을 수행할 수 있다.

  • 비동기처리의 예: 커피 주문 시 앞번호가 먼저 나오는 것, 동시에 처리되는 것
setTimeout(() => {
    console.log('느린 미니언즈 라떼');
}, 1000);
console.log('빠른 아메리카노');
fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res => res.json())
    .then(data => console.log(data));

console.log('데이터 요청 중...');
  • fetch API를 사용하여 서버에서 데이터를 가져올 수 있습니다.
  • fetch 함수는 비동기 방식으로 서버에 데이터를 요청하고,
  • 요청이 완료되면 then 메서드를 통해 데이터를 처리합니다.
  • 따라서 '데이터 요청 중...'이 먼저 출력되고, 서버 응답이 완료되면 데이터가 출력됩니다.

- 비동기 처리의 장점

  • 효율성: 비동기 처리는 결과를 기다리지 않고 다른 작업을 수행할 수 있기 때문에 시스템 자원을 효율적으로 활용할 수 있습니다.
  • 응답성: 사용자 경험을 향상시키는 데 유용하며, UI가 멈추지 않고 반응을 계속 유지할 수 있습니다.
  • 병렬 처리: 여러 작업을 병렬로 처리함으로써 전체적인 처리 속도를 높일 수 있습니다.

- 비동기 처리의 실무 예시

  • Ajax (Asynchronous JavaScript and XML): 웹 애플리케이션에서 페이지를 전체적으로 새로 고침하지 않고 서버와 통신할 수 있게 합니다.
  • Promise 및 async/await: JavaScript에서 비동기 작업을 다루기 위한 문법으로, 비동기 코드의 가독성을 높입니다.
  • Node.js의 비동기 I/O: Node.js는 비동기 방식으로 파일 시스템, 데이터베이스, 네트워크 요청 등을 처리하여 높은 성능을 제공합니다.
  • 비동기 API 호출: 클라이언트 애플리케이션에서 서버 API를 호출할 때, 비동기 요청을 통해 다른 작업을 차단하지 않고 처리가 가능합니다.
  • 실시간 데이터 처리: 채팅 애플리케이션, 실시간 알림 시스템 등에서 비동기 처리를 통해 즉각적인 데이터 업데이트가 가능합니다.

이 중 가장 많이 사용되는 방식은 Promise 및 async/await입니다. 이에 대해 자세히 알아보겠습니다.

02. 콜백(Callback) 패턴

  • 콜백(Callback): 비동기 처리를 위해 다른 함수에 인자로 넘겨주는 함수로, 비동기 작업이 완료되면 콜백 함수가 실행됩니다.
const a = () => console.log("a");
const b = () => console.log("b");

a(); // a
b(); // b
  • 위 코드는 동기적으로 실행되기 때문에 a 함수가 먼저 실행되고, b 함수가 실행됩니다.
  • 숫자 2가 먼저 출력되게 하려면 어떻게 해야 할까요?
// 비동기 처리를 사용하기 위해 setTimeout 함수를 사용합니다.
// a 함수가 실행되고 1초 후에 b 함수가 실행됩니다.
const a = () => {
    setTimeout(() => {
        console.log("a");
    }, 1000);
};

const b = () => console.log("b");

a(); // a
b(); // b
  • 위 코드는 비동기적으로 실행되기 때문에 a 함수가 실행되고, 1초 후에 1이 출력됩니다.
  • 이처럼 콜백 함수를 사용하면 비동기 작업을 순차적으로 처리할 수 있습니다.
  • 만약 출력하는 숫자 1과 2를 순차적으로 출력하려면 어떻게 해야 할까요?
// 콜백 함수를 사용하여 비동기 작업을 순차적으로 처리합니다.
const a = callback => {
    setTimeout(() => {
        console.log("a"); // a
        callback(); // b
    }, 1000);
};

const b = () => console.log("b");

a(b); // a, b
  • 위 코드는 a 함수가 실행되고, 1이 출력된 후에 콜백 함수가 실행되어 b 함수가 실행됩니다.
  • 이처럼 콜백 함수를 사용하면 비동기 작업을 순차적으로 처리할 수 있습니다.

03. Promise 패턴

new Promise((resolve, reject) => { ... });

  • Promise: 비동기 작업이 완료되었을 때 결과를 반환하거나 에러를 처리할 수 있는 객체로, 비동기 작업을 보다 효율적으로 처리할 수 있습니다. Promise 객체를 사용하면 콜백 함수의 중첩 문제(콜백 지옥)를 해결할 수 있으며, 비동기 코드의 가독성을 높여줍니다.

프로미스는 세 가지 상태를 가진다.

  • 대기(pending) : 비동기 처리가 아직 수행되지 않은 상태
  • 이행(fulfilled) : 비동기 처리가 성공적으로 수행된 상태
  • 거부(rejected) : 비동기 처리가 실패한 상태

프로미스를 사용하여 비동기 처리를 하려면, new Promise()를 사용하여 프로미스 객체를 생성하고, resolve와 reject를 사용하여 프로미스의 상태를 변경한다.

// Promise 객체 생성
// new Promise((resolve, reject) => {...})를 통해 새로운 프로미스 객체 생성
// 이 객체는 비동기 작업을 수행할 콜백 함수를 인자로 받으며,
// 이 콜백 함수는 resolve와 reject라는 두 개의 함수를 매개변수로 가집니다.
// resolve, reject 함수는 프로미스의 상태를 변경하는 역할을 합니다.
const a = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 1초 후에
            console.log(1);
            resolve(); // 성공 시 호출
        }, 1000);
    });
};

const b = () => console.log(2);

// 아래 코드는 역시나 콜백 지옥에 빠집니다.
// then 메서드를 사용하여 비동기 작업을 순차적으로 처리할 수 있습니다.
// a 함수가 실행되고 완료된 후에 b 함수가 실행되고, c 함수가 실행됩니다.
a().then(() => {
    b()
});

위 코드에서는 a, b, c, d 함수가 각각 1초씩 기다린 후 콘솔에 숫자를 출력하는 비동기 작업을 수행합니다. 각 함수는 Promise 객체를 반환하며, 이 객체는 작업이 성공했을 때 resolve를 호출합니다.

- 콜백지옥 해결

// b 함수 호출 시 return 키워드로 반환하게 되면
// promise 객체를 반환하게 되어 then 메서드를 연달아 사용할 수 있습니다.
a()
    .then(() => {
        return b();
    })

위 코드와 같이 각 then 메서드 내에서 다음 비동기 함수를 반환하도록 변경하여, then 체인을 형성합니다. 이렇게 하면 콜백 지옥을 피할 수 있습니다.

- 화살표 축약

a()
    .then(() => b())

화살표 함수의 축약 형태로, 함수 내부가 한 줄로 표현될 때 중괄호를 생략할 수 있습니다.

- 단축 표현

// resolve는 하나의 함수 데이터를 받아서 return으로 처리할 수 있습니다.
// 해서 b, c, d 함수 데이터 자체로 전달할 수 있습니다.
a()
    .then(b)
    .then(() => {
        console.log('done');
    });

- Promise.all

// Promise.all 메서드를 사용하여 여러 개의 비동기 작업을 동시에 처리할 수 있습니다.
Promise.all([a(), b()]).then(() => {
    console.log('모든 작업이 완료되었습니다.');
});

- Promise를 활용한 실무 예시

  • JSONPlaceholder에서 제공하는 API를 사용하여 사용자 정보를 가져와 출력해보겠습니다.

  • fetch(): 네트워크 요청을 보내는 API로, Promise 객체를 반환하여 비동기 작업을 처리합니다.

  • Resolve: Promise 객체가 성공적으로 처리되었을 때 호출되는 콜백 함수로, 비동기 작업이 완료되면 결과를 반환합니다.

// userId 매개변수를 받아 사용자 정보를 가져오는 함수를 구현합니다.
const getUser = userId => {
    // Promise 객체를 반환합니다. resolve 함수를 사용하여 비동기 작업이 완료되면 결과를 반환합니다.
    return new Promise(resolve => {
        // fetch 함수를 사용하여 JSONPlaceholder API를 호출합니다.
        fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
            // fetch 함수가 성공적으로 호출되면 response 객체를 반환합니다.
            // json 메서드를 사용하여 response 객체를 JSON 형태로 변환합니다.
            .then(res => res.json())
            // response 객체가 성공적으로 변환되면 data 객체를 반환합니다.
            .then(data => resolve(data));
    });
};

// 사용자 데이터를 가져오는 함수를 호출합니다.
getUser(1)
    // getUser 함수가 성공적으로 호출되면 data 객체를 반환합니다.
    .then(data => {
        console.log(`사용자 ID 1의 이름은 ${data.name} 입니다.`);
        return getUser(2);
    })
    .then(data => {
        console.log(`사용자 ID 2의 이름은 ${data.name} 입니다.`);
        return getUser(3);
    })
    .then(data => {
        console.log(`사용자 ID 3의 이름은 ${data.name} 입니다.`);
    });
  • 위 코드는 사용자 ID 1, 2, 3의 사용자 정보를 차례대로 가져와 출력합니다.
  • getUser 함수는 Promise 객체를 반환하며, 사용자 정보를 가져온 후에 다음 사용자 정보를 가져옵니다.
  • 이처럼 Promise를 사용하면 비동기 작업을 순차적으로 처리할 수 있습니다.

04. async/await 패턴

  • async/await: Promise를 더욱 간결하게 사용할 수 있는 문법으로, 비동기 작업을 동기적으로 처리할 수 있습니다.
// async 함수를 사용하여 비동기 작업을 처리합니다.
const a = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(1);
            resolve();
        }, 1000);
    });
};

// awite 함수를 사용하여 a함수를 실행하고 완료된 후에 b함수를 실행합니다.
// await 키워드는 뒷 부분의 비동기 작업이 완료될 때까지 기다립니다.
// await a();
// b();

// async 함수를 사용하여 비동기 작업을 순차적으로 처리합니다.
// await 키워드를 사용할 때는 async 함수 내부에서만 사용할 수 있기 때문에
// main 함수를 async 함수로 선언합니다.
const main = async () => {
    await a(); // 1
    b(); // 2
};

main(); // 1, 2
  • 위 코드는 async 함수를 사용하여 비동기 작업을 순차적으로 처리합니다.
  • main 함수는 async 함수로 선언되어 있으며, await 키워드를 사용하여 비동기 작업을 순차적으로 처리합니다.

- async/await를 활용한 실무 예시

  • 위 사용자 정보 API 예시를 async/await를 사용하여 구현해보겠습니다.
const getUser = userId => {
    return new Promise(resolve => {
        fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
            .then(res => res.json())
            .then(data => resolve(data));
    });
};

// await 키워드를 사용하여 비동기 작업을 순차적으로 처리합니다.
// const user1 = await getUser(1);
// console.log(`사용자 ID 1의 이름은 ${user1.name} 입니다.`);

// const user2 = await getUser(2);
// console.log(`사용자 ID 2의 이름은 ${user2.name} 입니다.`);

// const user3 = await getUser(3);
// console.log(`사용자 ID 3의 이름은 ${user3.name} 입니다.`);

// await 키워드를 사용할 때는 async 함수로 묶어주어야 합니다.
const main = async () => {
    const user1 = await getUser(1);
    console.log(`사용자 ID 1의 이름은 ${user1.name} 입니다.`);

    const user2 = await getUser(2);
    console.log(`사용자 ID 2의 이름은 ${user2.name} 입니다.`);

    const user3 = await getUser(3);
    console.log(`사용자 ID 3의 이름은 ${user3.name} 입니다.`);
};

main();
  • 위 코드는 async/await를 사용하여 사용자 ID 1, 2, 3의 사용자 정보를 차례대로 가져와 출력합니다.
  • main 함수는 async 함수로 선언되어 있으며, await 키워드를 사용하여 비동기 작업을 순차적으로 처리합니다.

05. Resolve, Reject, Error Handling

  • Resolve: Promise 객체가 성공적으로 처리되었을 때 호출되는 콜백 함수로, 비동기 작업이 완료되면 결과를 반환합니다.
  • Reject: Promise 객체가 실패했을 때 호출되는 콜백 함수로, 비동기 작업이 실패하면 에러를 반환합니다.
  • Error Handling: Promise 객체가 실패했을 때 에러를 처리하는 방법으로, catch 메서드를 사용하여 에러를 처리합니다.
// 사용자 데이터를 가져오는 함수
const getUser = userId => {
    // resolve, reject 함수를 사용하여 비동기 작업이 성공 또는 실패했을 때 결과를 반환합니다.
    return new Promise((resolve, reject) => {
        fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
            .then(response => {
                // fetch 함수가 성공적으로 호출되면 response 객체를 반환합니다.
                // response 객체가 정상적이지 않을 경우 에러를 반환합니다.
                if (!response.ok) {
                    throw new Error('네트워크 응답이 정상적이지 않습니다: ' + response.statusText);
                }
                return response.json();
            })
            .then(data => resolve(data))
            // fetch 함수가 실패했을 경우 에러를 반환합니다.
            .catch(error => reject(error));
    });
};

// 사용자 데이터를 가져오는 함수를 호출합니다.
getUser(1)
    .then(data => {
        console.log(`사용자 ID 1의 이름은 ${data.name} 입니다.`);
        return getUser(2);
    })
    .then(data => {
        console.log(`사용자 ID 2의 이름은 ${data.name} 입니다.`);
        return getUser(3);
    })
    .then(data => {
        console.log(`사용자 ID 3의 이름은 ${data.name} 입니다.`);
    })
    // catch 메서드를 사용하여 에러를 처리합니다.
    .catch(error => {
        console.error('사용자 정보를 가져오는 중 오류가 발생했습니다.');
    })
    // finally 메서드를 사용하여 작업이 완료되었을 때 메시지를 출력합니다.
    .finally(() => {
        console.log('사용자 정보를 가져오는 작업이 완료되었습니다.');
    });

// async/await를 사용하여 에러를 처리합니다.
const main = async () => {
    try {
        const user1 = await getUser(1);
        console.log(`사용자 ID 1의 이름은 ${user1.name} 입니다.`);

        const user2 = await getUser(2);
        console.log(`사용자 ID 2의 이름은 ${user2.name} 입니다.`);

        const user3 = await getUser(3);
        console.log(`사용자 ID 3의 이름은 ${user3.name} 입니다.`);
    } catch (error) {
        console.error('사용자 정보를 가져오는 중 오류가 발생했습니다.');
    } finally {
        console.log('사용자 정보를 가져오는 작업이 완료되었습니다.');
    }
};
  • 위 코드는 async/await를 사용하여 사용자 ID 1, 2, 3의 사용자 정보를 차례대로 가져와 출력합니다.
  • getWeather 함수는 Promise 객체를 반환하며, fetch 함수가 성공적으로 호출되면 response 객체를 반환합니다.
  • response 객체가 정상적이지 않을 경우 에러를 반환하며, fetch 함수가 실패했을 경우 에러를 반환합니다.
  • catch 메서드를 사용하여 에러를 처리하며, finally 메서드를 사용하여 작업이 완료되었을 때 메시지를 출력합니다.

06. axios

  • axios: HTTP 클라이언트 라이브러리로, 비동기 방식으로 서버와 데이터를 주고받을 수 있습니다.

axios

- 문법

const axios = require('axios');

// 지정된 ID를 가진 유저에 대한 요청
axios.get('/user?ID=12345')
  .then(function (response) {
    // 성공 핸들링
    console.log(response);
  })
  .catch(function (error) {
    // 에러 핸들링
    console.log(error);
  })
  .finally(function () {
    // 항상 실행되는 영역
  });

- axios를 활용한 실무 예시

cdn : <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

const getUser = userId => {
    return new Promise((resolve, reject) => {
        axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`)
            .then(response => resolve(response.data))
            .catch(error => reject(error));
    });
};

// async/await를 사용하여 에러를 처리
const main = async () => {
    try {
        const user1 = await getUser(1);
        console.log(`사용자 ID 1의 이름은 ${user1.name} 입니다.`);

        const user2 = await getUser(2);
        console.log(`사용자 ID 2의 이름은 ${user2.name} 입니다.`);

        const user3 = await getUser(3);
        console.log(`사용자 ID 3의 이름은 ${user3.name} 입니다.`);
    } catch (error) {
        console.error('사용자 정보를 가져오는 중 오류가 발생했습니다.');
    } finally {
        console.log('사용자 정보를 가져오는 작업이 완료되었습니다.');
    }
};

주요 변경사항:

  • fetch → axios.get 으로 변경
  • response.json() 과정이 필요 없음 (axios는 자동으로 JSON 변환)
  • response.ok 체크가 필요 없음 (axios는 에러 상태코드를 자동으로 catch로 보냄)
  • response.data로 바로 데이터에 접근 가능

axios가 더 간단하고 편리하죠! 😊

06. fetch API

  • fetch(주소, 옵션)
  • 네트워크를 통해 리소스를 요청(Request) 및 응답(Response)하는 API로,
  • Promise 객체를 반환하여 비동기 작업을 처리합니다.

console 창에서 fetch 함수를 사용하여 JSONPlaceholder API를 호출해보겠습니다.

// fetch 함수를 사용하여 JSONPlaceholder API를 호출합니다.
console.log(fetch('https://jsonplaceholder.typicode.com/users'));

콘솔창의 promise instance를 클릭하면
then, catch, finally 메서드를 사용할 수 있도록 지정된 것을 확인할 수 있습니다.

fetch 함수를 사용하여 JSONPlaceholder API를 호출하면, Promise 객체를 반환합니다.

fetch('https://jsonplaceholder.typicode.com/users').then(res => console.log(res));

json 메서드를 사용하여 response 객체를 JSON 형태로 변환할 수 있습니다.

fetch('https://jsonplaceholder.typicode.com/users').then(res => console.log(res.json()));

그러면 json 형태로 변환된 데이터를 확인할 수 있습니다.

fetch 함수를 사용하여 JSONPlaceholder API를 호출하면, JSON 형태로 변환된 데이터를 반환합니다.

fetch('https://jsonplaceholder.typicode.com/users', {
    // HTTP 요청 메서드를 지정합니다.
    method: 'POST',
    // 서버로 전송되는 데이터의 형식을 지정합니다.
    headers: {
        'Content-Type': 'application/json',
    },
    // 서버로 전송되는 데이터를 지정합니다.
    // body 옵션에 명시하는 데이터는 문자열 형태로 지정해야 합니다.
    // JSON.stringify 메서드를 사용하여 JSON 형태로 변환합니다.
    body: JSON.stringify({
        title: 'foo',
        body: 'bar',
        userId: 1,
    }),
})
    .then(res => res.json()) // JSON 형태로 변환된 데이터를 반환합니다.
    .then(json => console.log(json)); // JSON 형태로 변환된 데이터를 출력합니다.

// .then(res => {
//     return res.json();
// });

fetch 함수가 promise 객체를 반환하므로, async/await를 사용하여 비동기 작업을 처리할 수 있습니다.

const main = async () => {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    const json = await res.json();
    console.log(json);
};

main();
티스토리 친구하기