도림.로그

Tags

Series

About

비동기 프로그래밍 - JavaScript - async / await

2024. 06. 09.

비동기 프로그래밍

▶ Expand post list

    이어서

    이전 포스팅까지 해서 Promise를 활용한 비동기 프로그래밍에 대해서 알아봤습니다. 마지막으로 async / await 에 대해 알아보도록 하겠습니다.

    async / await?

    async / await는 비교적 최신 JavaScript 문법입니다. 많은 사람들이 Java의 버전 중 큰 분기점이 되는 버전으로 Java 8을 떠올리는데요, 이에 대응하는 JavaScript의 큰 전환점이 ES6였습니다. async / await 도 이 ES6에서 소개된 문법입니다. async는 함수의 선언부에 붙일 수 있는 구문입니다. async가 붙은 함수는 어떤 값을 반환하더라도 Promise를 반환하게 됩니다. 또한 awaitPromise의 호출부에 사용할 수 있는 구문입니다. await가 붙은 Promise는 fulfilled 혹은 rejected 상태가 될 때까지 기존 실행 흐름을 멈추고 대기하게 됩니다.

    DB 접근 예제

    async / await를 사용하게 되면 Promise를 사용할 때와 어떤 차이점이 있을지 지금까지 사용해오던 예제를 그대로 활용하여 알아보도록 하겠습니다.

    DB에 쿼리를 날려 특정 유저가 작성한 게시글과 그 게시글의 댓글을 불러와보자.

    앞 포스트들에서 했던 것과 구현 자체가 크게 다르지 않기 때문에 전체 코드를 보며 차이점얼 알아보도록 하겠습니다.

    const database = {
      users: [
        { id: 1, name: "John", age: 30 },
        { id: 2, name: "Jane", age: 25 },
        { id: 3, name: "Bob", age: 40 },
      ],
      posts: [
        { id: 1, title: "Post 1", body: "Lorem ipsum", userId: 1 },
        { id: 2, title: "Post 2", body: "Dolor sit amet", userId: 2 },
        { id: 3, title: "Post 3", body: "Consectetur adipiscing elit", userId: 1 },
      ],
      comments: [
        { id: 1, body: "Great post!", postId: 1 },
        { id: 2, body: "Thanks for sharing", postId: 2 },
        { id: 3, body: "I learned a lot from this", postId: 1 },
      ],
    };
    
    async function findUser(userId) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const user = database.users.find((user) => user.id === userId);
          if (!user) {
            reject(`Invalid user id: ${userId}`);
            return;
          }
          resolve(user);
        }, 1000);
      });
    }
    
    async function findPostsByUser(userId) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const posts = database.posts.filter((post) => post.userId === userId);
          if (posts.length === 0) {
            reject(`No posts found for user id: ${userId}`);
            return;
          }
          resolve(posts);
        }, 1000);
      });
    }
    
    async function findCommentsByPost(postId) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const comments = database.comments.filter((comment) => comment.postId === postId);
          resolve(comments);
        }, 1000);
      });
    }
    
    async function getUsersPostsCommentsAsync(userId) {
      try {
        const user = await findUser(userId);
        const posts = await findPostsByUser(userId);
        const postsWithComments = await Promise.all(posts.map(async (post) => {
          const comments = await findCommentsByPost(post.id);
          return { ...post, comments };
        }));
        return { user, posts: postsWithComments };
      } catch (error) {
        console.error(error);
      }
    }
    
    getUsersPostsCommentsAsync(1)
      .then((result) => {
        console.log(result);
      });

    일단 각 함수에 async 키워드가 붙어있는 것을 볼 수 있습니다. 사실 예제 코드에서는 마지막 구현부인 getUsersPostsCommentsAsync()를 제외하고는 이 키워드가 있는지의 여부는 큰 영향을 미치지 않습니다. 함수 내부에서 명시적으로 Promise를 반환하고 있기 때문입니다. async함수는 일반 값을 반환하더라도 그 값이 Promise로 감싸져서 반환되기 때문입니다. 이는 동기적으로 로직을 작성했을 경우 Promise를 resolve 하여 반환하는 것과 같은 동작을 수행한다고 생각해볼 수도 있습니다. 또한 async 함수에서 예외가 발생하면(throw Error()) 그것은 Promise가 reject 되는 것과 같은 영향을 미치게 됩니다. 아래 예제 코드를 보시면 조금 더 이해가 빠르실 것 같습니다.

    async function foo() {
      return 100;
    }
    
    foo().then((num) => console.log(100));
    
    // Output
    // 100
    
    async function bar() {
      throw Error("some error occurred from bar");
    }
    
    bar()
      .then(() => console.log("success"))
      .catch((e) => console.error(e));
    
    // Output
    // Error: some error occurred from bar
    //     at bar (/example/index.js:8:9)
    //     at Object.<anonymous> (/example/index.js:11:1)
    //     at Module._compile (node:internal/modules/cjs/loader:1159:14)
    //     at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    //     at Module.load (node:internal/modules/cjs/loader:1037:32)
    //     at Module._load (node:internal/modules/cjs/loader:878:12)
    //     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    //     at node:internal/main/run_main_module:23:47

    이 코드에서 bar()로부터 reject가 발생할 경우, console.error(e)하게 되어 있는데요. 보시다시피 async 함수에서 예외가 발생하니 해당 핸들러에 잡히는 것을 알 수 있습니다.

    async / await 그리고 Promise

    위의 내용을 토대로 볼 때, asyncawait를 활용하면 Promise에 의해 비동기적으로 동작하던 코드를 조금 더 동기적인 관점으로 바라볼 수 있다는 것을 알 수 있습니다. 특히 예제의 구현부를 보면, Promise를 다루기 위한 보일러플레이트가 없어지면서 코드 자체가 크게 컴팩트해졌다는 사실을 역시 볼 수 있습니다. 이 코드의 컴팩트함에 크게 기여하는 것은 await라고 볼 수 있는데요, Promise가 정상적으로 완료되어 fulfil될 경우의 값을 await를 통해 받아온다고 볼 수 있습니다.

    마치며

    간단한 JavaScript 비동기 프로그래밍 글을 3개 쓰는 데에 1년이나 걸렸네요. 도움이 되었으면 좋겠는데, 아쉽게도 개인적인 개념 정리에 그친 것 같긴 합니다. 저는 업무에선 대부분 async / await를 활용하여 로직을 작성하는 것을 선호합니다. 이 편이 로직의 가독성을 높이는 것에 큰 도움을 준다고 생각하기 때문입니다. 다만 async / await는 Promise를 활용하는 더 나은 방법을 제공한다 정도로 생각하고, Promise에 대한 전반적인 이해를 항상 가지고 있는 것이 중요할 것 같습니다.

    감사합니다.

    끝.

    #javascript#async#await

    © 2024, Built with Gatsby