Callback-Promise-Async/Await

DOHEE·2022년 11월 4일
0

<0> 동기와 비동기

Javascript를 배우면서 접한 동기와 비동기에 대한 내용이 처음엔 와닿지 않았다. 분명 위에서부터 차례로 내려오면서 실행을 하는데 어떻게 비동기로 움직일 수 있는지 의문이 있었다.

솔직히 어떻게 동기와 비동기가 이루어지는 지 자체는 아직 모르겠다.

몇 번의 프로젝트를 통해 Promise 또는 async/await를 직접 써보면서 정확하게는 이해하지 못 해도 어떻게 사용하는지 정도는 이해한 것 같다. 하지만 누군가 동기와 비동기는 무엇이고 callback과 Promise, async/await은 무슨 차이인지 묻는다면 아직 자신있게 이야기할 자신이 없다.

따라서 이번 포스트를 작성하면서 동기와 비동기, callback과 promise, async/await 까지 스스로가 더 잘 이해할 수 있도록 노력해보고자 한다.

<1> js는 왜 비동기 시스템을 이용하는가

이 포스트를 보는 많은 사람들이 알고 있듯이 javascript는 대표적인 싱글 쓰레드 프로그래밍 언어이다. 싱글 쓰레드라는 것은 코드가 동시에 진행되는 공간이 하나뿐이라는 의미이다.

쓰레드라는 단어를 일상에서 자주 사용하지는 않기 때문에 잘 와닿지 않을 수 있다. 그래서 완벽한 예시는 아니겠지만 은행을 예로 들어보려고 한다.

우리가 실행하는 프로그램을 은행이라고 생각해보면, 은행원이 쓰레드가 되고 은행 금고는 리소스와 같다. 즉, 쓰레드는 일종의 수행을 해주는 사람이라고 볼 수 있다.

은행을 찾아오는 사람이 많아 굉장히 오래 기다리는 경험을 많이 해봤을 것이다. 은행 업무는 동기 처리이다. 한 은행원이 한 고객의 요청을 다 처리할 때까지 기다리고 다 끝나야 다른 고객을 받는다.

은행원이 많을 수록 많은 사람들의 요청을 빨리 해결할 수 있을 것이다.

하지만 js은행(javascript)에는 은행원이 하나뿐이다. 만약 js은행에서 한 사람씩 동기적으로 해결한다면 고객은 하루를 다 기다려도 은행 업무를 보지 못 할지도 모른다. 따라서, js은행은 일을 비동기적으로 해야 한다. 그렇지 않으면 고객이 너무 많이 기다려서 다시는 오지 않을 것이다.

<2> Callback

const data = loadBooks("shakespeare") // hamlet 기대
console.log(data)

위의 코드의 결과가 hamlet일 수도 있고 아닐 수도 있다. shakespeare의 책 이름을 가져오는 속도가 js가 코드를 처리하는 속도보다 느리면 data에는 undefined가 들어간다.

만약 data에 hamlet이 들어간 다음에 data를 console에 찍어보고 싶으면 어떻게 해야 할까?

이 때 우리는 callback을 사용할 수 있다.

books.forEach(book => console.log(booke.name))
const bookNames = books.map(book => book.name)

callback이란 어떤 함수의 매개변수로 들어가는 함수, 내부 실행 함수이다. 위와 같이 forEach나 map에 사용하는 익명 함수가 대표적인 callback이다.

자, 다시 비동기의 문제점으로 돌아가보자.

앞선 코드에서의 문제는 data에 제대로 책이 들어가지 않는 것이었다. 이를 callback으로 어떻게 해결할 수 있을까? setTimeout을 이용해서 설명해보겠다.

const findBook = (author) => {
    let book;
    setTimeout(() => {
        console.log("3 seconds loading complete");
        if (author === "shakespeare") {
            book = {
                author,
                bookName: "Hamlet",
                price: "$5.9",
            };
        } else {
            book = {
                author,
                bookName: "None",
                price: "None",
            };
        }
    }, 3000);
    return book;
};

위의 코드는 복잡해 보이지만 findBook에 작가를 넣어주면 shakespeare일 경우에는 Hamlet을, 그 외에는 None3초 뒤에 반환해주는 함수이다.

const myBook = findBook("shakespeare");
console.log("book :", myBook);

위의 코드를 실행하면 어떤 결과가 나올까? book : undefined가 뜬다. 앞서 말했듯이 javascript는 요청만 넣고 반환을 기다려주지 않기 때문에 findBook을 실행하고 바로 myBook 변수에 넣는다.

책을 찾는데 3초가 걸리기 때문에 변수에 넣을 당시에는 아직 책에 대한 정보가 없어 undefined가 된다.

const findBook = (author, callback) => {
    let book;
    setTimeout(() => {
        console.log("3 seconds loading complete");
        if (author === "shakespeare") {
            book = {
                author,
                bookName: "Hanlet",
                price: "$5.9",
            };
        } else {
            book = {
                author,
                bookName: "None",
                price: "None",
            };
        }
        callback(book);
    }, 3000);
};

findBook("shakespeare", (book) => console.log("book :", book));

따라서 console.log에 대한 코드를 함수로 만들어 매개변수로 넣어 보내주면 책 검색 결과가 나온 뒤에 console을 찍을 수 있는 것이다.

보통 콜백은 일회성인 경우가 많아 위의 예시와 같이 익명함수를 사용한다. 하지만 함수를 정의한 뒤에 넣어주는 것 역시 가능하다.

그렇다면 data가 제대로 넘어오지 않았을 때는 어떻게 해야 할까? 에러 코드를 넣기 위해 위의 코드를 조금만 간소화 해보겠다.

const findBook = (author, callback) => {
    let book;
    let error;
    setTimeout(() => {
        console.log("3 seconds loading complete");
        if (author === "shakespeare") {
            book = {
                author,
                bookName: "Hanlet",
                price: "$5.9",
            };
        } else {
            error = "해당하는 책이 없습니다.";
        }

        error ? console.log(error) : callback(book);
    }, 3000);
};

findBook("shakespeare", (book) => console.log("book :", book)); // success
findBook("DOHEE", (book) => console.log("book :", book)); // error

에러 코드 역시 매개변수로 넣어줄 수 있다. 이 부분은 직접해보길!

비동기인 자바스크립트를 마치 동기인 것처럼 처리할 수 있는 방법인 콜백은 사용하다 보면 너무 길어져 이른 바 콜백 지옥에 빠지게 한다. 아래의 예시는 간단하게 출근하기까지의 일정을 콜백으로 만들어본 것이다.

import { wakeUp, cleanUp, workOut, takeSubway, walkOn, tagCard, takeElevator, setTable } from "./callbacks";

wakeUp("DOHEE", (user) => {
    cleanUp(user, (user) => {
        workOut(user, (user) => {
            takeSubway(user, (user) => {
                walkOn(user, (user) => {
                    tagCard(user, (user) => {
                        takeElevator(user, (user) => {
                            setTable(user, () => {
                                console.log("It's time to work!");
                            });
                        });
                    });
                });
            });
        });
    });
});

지금은 간단하게 console만 찍는 함수이기도 하고 함수들을 외부에서 정의해서 import 해왔기 때문에 덜 복잡해보인다. 하지만 중간에 처리 코드가 들어가고 에러 처리가 포함된다면 걷잡을 수 없이 복잡해질 것이다.

이를 해결하기 위해 나온 것이 Promise이다.

<3> Promise

Promise는 이름 그대로 약속을 의미한다. 지금 당장 데이터가 들어오는 건 아닌데 곧 들어올 것을 뜻한다. 앞선 예시처럼 은행에 빗대보면 Promise는 은행의 대기표 또는 수표 등과 비슷할 것이다.

은행의 대기표가 내 차례가 되면 처리를 해주겠다는 의미인 것처럼 또는 수표가 지금 돈을 주는 건 아니지만 수표를 가지고 은행에 가면 돈으로 바꿔주는 것처럼 Promise는 반환값이 곧 들어올 것임을 약속해준다.

const findBook = (author) => {
    return new Promise((resolve, reject) => {
        let book;
        let error;
        setTimeout(() => {
            console.log("3 seconds loading complete");
            if (author === "shakespeare") {
                book = {
                    author,
                    bookName: "Hanlet",
                    price: "$5.9",
                };
            } else {
                error = "해당하는 책이 없습니다.";
            }
            error ? reject(error) : resolve(book);
        }, 3000);
    });
};

Promise는 함수 안에 새로운 프로미스를 반환하는 형태로 구성되어있다. 위에서 책을 찾아오는 함수를 다시 활용해보면 새로운 Promise를 반환해주고 매개변수로 resolve와 reject를 설정할 수있다.

참고로 resolve와 reject는 정해진 값이 아니라 정해진 자리이므로 헷갈리지 말자! 첫번째 매개변수가 성공했을 때, 두번째 매개변수가 실패했을 때 실행할 함수라는 점!

const book = findBook("shakespeare");
console.log(book);

// Promise { <pending> }

자 그럼 위의 함수를 변수에 넣으면 값이 나올까? Promise { < pending > } 이 반환된다. pending은 약속은 했는데 아직 값이 들어오지 않았다는 의미이다. 쉽게 설명하면 대출 신청은 했는데 아직 대출 허가 또는 돈이 나오지 않았다는 것이다.

Promise의 반환값에는 총 세 종류가 있는데 pending, fulfilled, rejected이다. pending은 앞서 설명했듯이 처리 중이라는 의미, fulfilled는 성공, rejected는 실패를 의미한다.

그렇다면 book을 console에 찍고 싶다면 어떻게 해야할까?

findBook("shakespeare").then((book) => console.log(book));

함수 뒤에 '.'을 찍고 then을 활용해서 데이터에 접근할 수 있다. 매개변수에 함수를 넣으면서 어디가 함수의 시작과 끝인지 확인하기 어려웠던 callback에서 벗어나 .then을 통해 한 눈에 그 다음 함수를 이해할 수 있다는 장점이 있다.

wakeUp("DOHEE")
    .then((user) => cleanUp(user))
    .then((user) => workOut(user))
    .then((user) => takeSubway(user))
    .then((user) => walkOn(user))
    .then((user) => tagCard(user))
    .then((user) => takeElevator(user))
    .then((user) => setTable(user))
    .then(() => console.log("It's time to work!"));

하지만 .then 역시 끝없이 붙어 chaining 되는 문제가 발생했고, 그로 인해 async / await이 생겼다.

마지막으로 Promise의 에러처리에 대해서 얘기해보고자 한다.
Promise의 error를 처리하는 방법은 then의 두번째 인자로 처리하는 방법과 .catch를 이용하는 방법이 있다.

findBook("DOHEE").then(
    (book) => console.log(book),
    (error) => console.log(error)
);

findBook("shakespeare")
    .then((book) => console.log(book))
    .catch((error) => console.log(error));

보다 추천하는 방법은 .catch를 이용하는 것이다. 왜냐하면 .catch를 사용할 경우 .then의 몇번째에서 에러가 발생했고 어떤 문제인지를 알려주는 반면, .then의 매개변수로 넣으면 각각 에러처리를 달리 해야 할 뿐만 아니라 어디서 에러가 발생했는지 알아채기 어렵다.

<4> Async / Await

callback hell에서 벗어나고자 Promise를 사용했는데, Promise도 chaining을 하다보면 가독성이 떨어진다.

우리가 코드를 짤 때 위에서 아래로 차례대로 읽어내리듯이 코드를 이해하기 위해서 만들어진 것이 async과 await이다.

함수 앞에 async라는 예약어를 붙이고, Promise를 반환하는 비동기 처리 코드 앞에 await이라는 예약어를 붙여 마치 chaining을 한 것과 같은 효과를 얻을 수 있다.

const book = findBook("shakespeare");
console.log(book);

// Promise { <pending> }

const consoleOfBook = async (user) => {
    const book = await findBook(user);
    console.log(book);
};

consoleOfBook("shakespeare");

// { author: 'shakespeare', bookName: 'Hanlet', price: '$5.9' }

async 함수 consoleOfBook을 정의하고 findBook 앞에 await을 사용했더니 book이라는 변수에 잘 대입이 된 것을 확인할 수 있다.

마지막으로 async / await의 예외처리다. Promise에서 에러 처리를 위해 .catch를 사용했던 것처럼 async / await에서는 try catch를 사용한다.

try {} 의 중괄호 안에 실행할 코드를 작성하고 catch (error) {} 중괄호 안에 에러 발생 시 실행할 코드를 작성한다.

const consoleOfBook = async (user) => {
    try {
        const book = await findBook(user);
        return book;
    } catch (error) {
        console.log(error);
    }
};

consoleOfBook("shakespeare");
// { author: 'shakespeare', bookName: 'Hamlet', price: '$5.9' }

consoleOfBook("DOHEE");
// 해당하는 책이 없습니다.

<5> 오늘의 총평

async / await를 거의 모든 프로젝트에서 활용하면서 정작 동기와 비동기에 대해서는 큰 관심을 두지 않았던 것 같다. 단지 async / await를 쓰면 데이터가 오는 것을 기다려준다는 정도로만 이해하고 이리저리 갖다 썼는데 async / await이 나온 배경부터 찬찬히 공부해보니 조금은 javascript를 더 잘 이해하게 된 것 같다.

callback이 매개변수로 넣어주는 함수라는 것을 이미 알고 있으면서도 map이나 forEach에 사용하는 함수가 callback이라고는 생각지도 않았다. 단지 그냥 그렇게 사용하는 것이다 정도로 생각했던 것 같다.

앞선 사례들처럼 내가 당연하게 사용하면서도 어떤 방식으로 작용하는 건지 이해하지 못 하는 게 너무나도 많을 것 같다. 앞으로 더 많은 공부가 필요하겠다는 생각이 자꾸 드는 시간이었다.

profile
안녕하세요 : ) 천천히라도 꾸준히 성장하고 싶은 개발자 DOHEE 입니다! ( 프로필 이미지 출처 : https://unsplash.com/photos/_FoHMYYlatI )

0개의 댓글