연구 타겟

비동기 처리 방식의 3가지 방식에 대해 다뤄보겠다.

  1. 콜백(callback): 요청이 끝난 후 실행할 함수를 매개변수로 추가하는 방식. 비동기 코드를 순서대로 실행하는 가장 일반적인 방식. 콜백은 함수의 파라미터로 함수를 전달하며, 비동기 처리가 끝났을 때 전달된 함수를 실행한다. 콜백은 가독성이 좋지 못하여 유지보수, 디버깅이 힘들다.
  2. 프로미스(Prmoise): Promise 객체를 반환하는 방식. 콜백 대신 사용하며, Promise 객체는 처음에는 대기였다가 작업이 완료되면 성공 또는 실패 상태가 된다. then(), catch() 메서드를 사용하여 성공과 실패에 대한 처리를 할 수가 있다.
  3. 어싱크 어웨이트(async await): 프로미스를 더욱 간단하게 async await 구문으로 변경하는 문법. 프로미스를 사용하는 비동기 작업을 동기적으로 처리하는 것처럼 코드를 작성할 수 있게 해준다. async가 붙어 있는 함수를 실행할 때 await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다릴 수 있다.

1. 콜백 함수

아래 코드는 예상대로 함수의 실행 순서(register() → saveDB() → sendEmail() → getResult())가 보장됨을 볼 수 있다. 하지만, 콜백은 깊어지면 점점 알아보기 힘든 상황이 발생할 수 있다.

이러한 콜백의 문제를 해결할 목적으로 개발된 것이 ‘프로미스(Promise) 객체’이다.

// callback-test.js

const DB = [];

// 회원 가입 API 함수
function register(user) { // 1. 콜백이 3중으로 중첩된 함수
    return saveDB(user, function (user) { // 콜백
        return sendEmail(user, function (user) { // 콜백
            return getResult(user); // 콜백
        });
    });
}

// 2. DB에 저장 후 콜백 실행
function saveDB(user, callback) {
    DB.push(user);
    console.log(`save ${user.name} to DB`);
    return callback(user);
}

// 3. 이메일 발송 로그만 남기는 코드 실행 후 콜백 실행
function sendEmail(user, callback) {
    console.log(`email to ${user.email}`);
    return callback(user);
}

// 4. 결과를 반환하는 함수
function getResult(user) {
    return `success register ${user.name}`;
}

const result = register({ email: "andy@test.com", password: "1234", name: "andy" });
console.log(result);
// 출력 결과

save andy to DB
email to andy@test.com
success register andy

2. Promise 객체

예제 1 - 단순 예제 Promise

Promise는 자바스크립트에서는 비동기 실행을 동기화하는 구문으로 사용한다.

Promise는 각각 이행, 거절, 대기 세 가지 상태를 가질 수 있으며, Promise는 객체이므로 new 연산자로 인스턴스를 생성할 수 있다.

Promise 객체가 생성되면 대기 상태가 된다. resolve() 함수가 실행되면 이행으로 변경되고 실패하면 reject() 함수가 실행되며 거절로 변경된다.

아래 코드는 위 콜백 함수(saveDB(), sendEmail(), getResult())를 Promise 객체로 변경한 코드이다.

// promise-test.js

const { resolve } = require("path");

const DB = [];

function saveDB(user) {
    const oldDBSize = DB.length;
    DB.push(user);
    console.log(`save ${user.name} to DB`);
    return new Promise((resolve, reject) => { // 콜백 대신 Promise 객체 반환
        if (DB.length > oldDBSize) {
            resolve(user); // 성공 시 유저 정보 반환
        } else {
            reject(new Error("Save DB Error!")); // 1. 실패 시 에러 발생
        }
    });
}

function sendEmail(user) {
    console.log(`email to ${user.email}`);
    return new Promise((resolve) => {
        resolve(user); // Promise 객체를 반환. 실패 처리 없음
    })
}

function getResult(user) {
    return new Promise((resolve, reject) => { // Promise 객체 반환
        resolve(`success register ${user.name}`); // 성공 시 성공 메시지와 유저명 반환
    });
}

function registerByPromise(user) {
    // 2. 비동기 호출이지만, 순서를 지키며 실행
    const result = saveDB(user).then(sendEmail).then(getResult);
    // 3. 아직 완료되지 않았으므로 지연(pending) 상태
    console.log(result);
    return result;
}

const myUser = { email: "andy@test.com", password: "1234", name: "andy" };
const result = registerByPromise(myUser);
// 결괏값이 Promise이므로 then() 메서드에 함수를 넣어서 결괏값을 볼 수 있음
result.then(console.log);
// 출력 결과

save andy to DB
Promise {[[PromiseState]]: 'pending', [[PromiseResult]]: undefined, Symbol(async_id_symbol): 10, Symbol(trigger_async_id_symbol): 9}
email to andy@test.com
Error: 에러 발생
    at C:\Users\dkgke\Desktop\백엔드 기초 스터디\practice\chapter5\promise-test.js:35:80
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {stack: 'Error: 에러 발생
    at C:\Users\dkgke\Desktop\백엔…ions (node:internal/process/task_queues:96:5)', message: '에러 발생'}

아래와 같이 .then() 쓸 수 있는 이유는 user를 인수로 넣었을 때 반환되는 결과가 Promise이기 때문이다.

const result = saveDB(user).then(sendEmail).then(getResult);

Promise 객체의 실행 결과로 실패를 주어야 하는 경우 reject() 함수를 사용한다. 그리고 Promise에서 발생한 에러는 .catch()를 사용한다.

const result = saveDB(user).then(sendEmail).then(getResult).catch(error => new Error(error));

예제 2 - 복잡한 예제 Promise

아래 코드는 현재 상영 영화 순위를 20위까지 보여주는 코드이다. 예외 처리는 하단의 catch()에서 처리하고 있다. 이번 실습코드에서 참고할 내용은 axios 통신 시, then의 early return으로 validation 하는 방식이다.

// callback-promise-async-await.js

// 현재 상용 영화 순위를 20위까지 프로미스를 사용해 확인하기
const axios = require("axios"); // axios 임포트
// 1. 영화 순위 정보 URL
const url = "http://raw.githubusercontent.com/wapj/jsbackend/main/movieinfo.json";

axios
    .get(url) // 2. GET 요청
    .then((result) => {
        if (result.status != 200) { // 상태가 200이 아니면 에러
            throw new Error("요청에 실패했습니다!");
        }

        if (result.data) { // 3. result.data가 있으면 결과를 반환
            return result.data;
        }

        throw new Error("데이터가 없습니다."); // data가 없으면 에러
    })
    .then((data) => { // 4. 3에서 받은 데잍 처리
        if (!data.articleList || data.articleList.size == 0) { // 5. 크기가 0이면 에러
            throw new Error("데이터가 없습니다.");
        }
        return data.articleList; // 6. 영화 리스트 반환
    })
    .then((articles) => {
        return articles.map((article, idx) => { // 7. 영화 리스트를 제목과 순위 정보로 분리
            return { title: article.title, rank: idx + 1 };
        });
    })
    .then((results) => {
        for (let movieinfo of results) { // 받은 영화 리스트 정보 출력
            console.log(`[${movieinfo.rank}위] ${movieinfo.title}`);
        }
    })
    .catch((err) => { // 8. 중간에 발생한 에러들을 여기서 처리
        console.log("<<에러 발생>>")
        console.error(err);
    })
// 출력 결과

[1] 처음부터 잘했으면 얼마나 좋니
[2] <본즈  > 궁지로 내몰린 10대를 보는 시선
[3] 경이로운 생生의 의지로 창조해낸 페르시아어
[4] 뻔하지 않은 사랑 영화
[5] 우린 아무것도 모른다, 틀렸다는것만 증명할 에올
[6] [영화리뷰] < 메뉴> 보고
[7] 진실한 삶의 태도를 제시하다
[8] 중요한  꺾이지 않는 마음
[9] 치즈버거 세트의 행복
[10] 즐기거나 놀리거나,
[11] [영화감상]오늘 , 세계에서  사랑이 사라진다 해도
[12] 아들을 구하고 싶다면 달려라
[13] 인생은 아름다워(2022)
[14] 영화 <사도>, 죽음의 문턱에 와서야 닿는 마음에 대해
[15] 스타워즈: 안도르
[16] 죽음의 문턱에서 거짓말로 살아남은 자의 고백
[17] 닫힌 마음 - 영화 '체리향기'
[18] <오늘 , 세계에서  사랑이 사라진다 해도> 리뷰
[19] < 메뉴> 180 원짜리 먹으러 와서 사레 걸린기분
[20] 닭장을 나온 백호 

예제 3 - 문제점 및 대안 찾기 Promise

프로미스가 콜백보다는 깔끔한 코드를 유지할 수 있지만, 콜백 함수 사용과 비슷하게 then() 함수에 성공 시와 실패 시 처리할 함수를 둘 다 넘기는 경우가 있다. 좋은 방법은 catch()함수로 예외 처리를 하는 것이다.

// promise-anti-pattern.js

const { resolve } = require("path");

function myWork(work) {
    return new Promise((resolve, reject) => {
        if (work === 'done') {
            resolve('게임 가능');
        } else {
            reject(new Error("게임 불가능"));
        }
    })
}

// 1. 나쁨 - 콜백과 다를 바가 없음
myWork('done').then(
    function (value) {
        console.log(value)
    },
    function (err) {
        console.log(err)
    }
);

// 2. 좋음
myWork('doing')
    .then(function (value) { console.log(value) })
    .catch(function (err) { console.error(err) });
// 출력 결과

게임 가능
Error: 게임 불가능

3. async await 구문

async와 await는 자바스크립트에 가장 최근 도입된 비동기 처리 방식이다. 기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완했으며 가독성 높은 코드를 작성할 수 있다.

async는 함수 앞에 붙이는 키워드인데, async가 붙은 함수는 프로미스를 반환한다.

예제 1 - 단순 예제 async

async 키워드가 붙어있는 함수의 반환값을 출력하면, Promise 객체로 리턴하는 것을 확인할 수 있다.

// async-await.js

async function myName() {
    return "Andy";
}

console.log(myName());
// 출력 결과

Promise {[[PromiseState]]: 'fulfilled', [[PromiseResult]]: 'Andy', Symbol(async_id_symbol): 5, Symbol(trigger_async_id_symbol): 1}

예제 2 - 단순 예제 await

await은 Promise 객체의 실행이 완료되기를 기다린다. 그러므로 await의 뒤에는 Promise가 오게 된다.

await은 Promise 객체인 myName() 함수의 실행이 끝나길 기다린다. 출력 결과에서 Promise { }은 console.log(showName())의 결괏값이다. showName()도 async가 붙어있으니 Promise 이다.

showName()을 출력했을 때, PromiseResult: undefined인 이유는 return이 없어서이다.

// async-await.js

async function myName() {
    return "Andy";
}

async function showName() {
    const name = await myName();
    console.log(`showName 함수: ${name}`);
}

console.log(showName());
// 출력 결과

Promise {[[PromiseState]]: 'pending', [[PromiseResult]]: undefined, Symbol(async_id_symbol): 5, Symbol(trigger_async_id_symbol): 1}
showName 함수: Andy

예제 3 - 1~10까지 1초 간격으로 출력

아래 코드는 async await, setTimeout()을 사용해서 1부터 10까지 1초에 하나씩 출력하는 코드이다. 위 동작을 위해서는 Promise와 setTimeout 함수가 필요하다.

// async-await-timing-print.js

function waitOneSecond(msg) { // 1. 1초 대기하고 메시지 출력
    return new Promise((resolve, _) => {
        setTimeout(() => resolve(`${msg}`), 1000);
    })
}

async function countOneToTen() { // 2. 10초 동안 1초마다 메시지 출력 
    for (let x of [...Array(10).keys()]) { // 3. 0부터 9까지 루프를 순회
        // 4. 1초 대기 후 result에 결괏값 저장
        let result = await waitOneSecond(`${x + 1}초 대기 중...`);
        console.log(result);
    }
    console.log("완료");
}

countOneToTen();
// 출력 결과

1 대기 ...
2 대기 ...
3 대기 ...
4 대기 ...
5 대기 ...
6 대기 ...
7 대기 ...
8 대기 ...
9 대기 ...
10 대기 ...
완료

waitOneSecond() 함수는 1초 대기하고 메시지를 출력하는 함수다. 1초를 대기하려면 setTimeout()을 사용해야 하는데 setTimeout()에는 반환값이 없기에 Promise 객체를 직접 생성했다. 직접 Promise를 만들어 반환하므로 async를 붙여주지 않았다. (평소에도 궁금했던 내용이라 보지 않고 쓸 수 있을 정도로 코드를 외워두었다.)

★ 예제 4 - axios 과 async await 활용 예

다음은 앞서 ‘예제 2 - 복잡한 예제 Promise’에서 작성한 코드를 async await을 이용하여 리팩토링한 코드이다. 아래 코드를 참고하여 실무에 사용해보아야겠다.

// top20-movie-async-await

const axios = require("axios");

async function getTop20Movies() { // 1. await을 사용하므로 async를 붙임
    const url = "http://raw.githubusercontent.com/wapj/jsbackend/main/movieinfo.json";
    try {
        // 2. 네트워크에서 데이터를 받아오므로 await으로 기다림
        const result = await axios.get(url); // axios 반환값을 Promise 객체이기 때문에 await으로 받을 수 있음
        const { data } = result; // result.data와 동일
        console.log(result);
        console.log(data);
        // data 또는 articleList 없을 때 예외 처리
        if (!data.articleList || data.articleList.size === 0) {
            throw new Error("데이터가 없습니다.");
        }
        // data에서 필요한 영화 제목과 순위 정보를 뽑아냄
        const movieinfos = data.articleList.map((article, idx) => {
            return { title: article.title, rank: idx + 1 };
        });

        // 데이터 출력
        for (let movieinfo of movieinfos) {
            console.log(`[${movieinfo.rank}위] ${movieinfo.title}`);
        }
    }
    catch (err) {
        // 3. 예외 처리는 기존 코드와 같게 try catch로 감쌈
        throw new Error(err);
    }
}

getTop20Movies();
// 출력 결과

[1] 처음부터 잘했으면 얼마나 좋니
[2] <본즈  > 궁지로 내몰린 10대를 보는 시선
[3] 경이로운 생生의 의지로 창조해낸 페르시아어
[4] 뻔하지 않은 사랑 영화
[5] 우린 아무것도 모른다, 틀렸다는것만 증명할 에올
[6] [영화리뷰] < 메뉴> 보고
[7] 진실한 삶의 태도를 제시하다
[8] 중요한  꺾이지 않는 마음
[9] 치즈버거 세트의 행복
[10] 즐기거나 놀리거나,
[11] [영화감상]오늘 , 세계에서  사랑이 사라진다 해도
[12] 아들을 구하고 싶다면 달려라
[13] 인생은 아름다워(2022)
[14] 영화 <사도>, 죽음의 문턱에 와서야 닿는 마음에 대해
[15] 스타워즈: 안도르
[16] 죽음의 문턱에서 거짓말로 살아남은 자의 고백
[17] 닫힌 마음 - 영화 '체리향기'
[18] <오늘 , 세계에서  사랑이 사라진다 해도> 리뷰
[19] < 메뉴> 180 원짜리 먹으러 와서 사레 걸린기분
[20] 닭장을 나온 백호 

정리

책의 저자가 말하는 거에 따르면, Promise가 필요한 경우(setTimeout()을 사용하거나, 여러 태스크를 동시에 실행해야 하는 경우) 이외에는 모두 async await을 사용한다고 한다. async await이 비동기를 동기화하는데 사용되고, Promise 객체에 대해 이해할 수 있어 무척 도움이 되었다.

구분 callback Promise async/await
에러 처리 콜백 함수 내에서 처리 catch() 메서드로 처리 try-catch 블록으로 처리
가독성 간단한 경우에는 괜찮으나, 점점 복잡해짐 가독성 좋음 가독성 좋음
중첩 처리 콜백 함수 내에서 처리 then() 메서드를 사용 await 키워드를 사용
  • Node.js에서 비동기는 이벤트 루프에 의해서 실행된다.
  • 콜백은 비동기 프로그래밍을 구현하는 기법이다. 요청이 완료되었을 때 실행해야 하는 콜백 함수를 매개변수로 같이 넘기는 방법이다.
  • Promise는 병행 프로그래밍 언어에서 프로그램 실행을 동기화하는데 쓰는 객체다. axios의 반환값은 Promise 객체이다.
  • async와 await은 Promise 객체를 반환하는 함수를 실행할 때 사용하는 키워드이다. async는 함수에서만 사용할 수 있다.

참고

다음에 다루게 될 것

  • 몽고DB 사용하기

댓글남기기