어제 오늘 내일

JavaScript 비동기 처리 완전 정복: async/await 사용법 본문

IT/Javascript

JavaScript 비동기 처리 완전 정복: async/await 사용법

hi.anna 2026. 8. 3. 12:30

JavaScript로 개발을 하다 보면 서버에서 데이터를 가져오거나, 파일을 읽거나, 타이머를 다루는 등 시간이 걸리는 작업을 자주 만나게 됩니다. 이런 작업들을 효율적으로 처리하기 위한 핵심 도구가 바로 async/await입니다. 이 글에서는 비동기 처리가 왜 필요한지부터, async/await를 실전에서 어떻게 활용하는지까지 차근차근 살펴보겠습니다.

1. 비동기 처리가 필요한 이유

JavaScript는 기본적으로 싱글 스레드로 동작합니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 그런데 만약 네트워크 요청처럼 오래 걸리는 작업이 끝날 때까지 다음 코드 실행을 멈추고 기다린다면 어떻게 될까요? 화면이 멈추고, 사용자는 아무것도 할 수 없는 답답한 경험을 하게 될 것입니다.
이 문제를 해결하기 위해 JavaScript는 비동기(asynchronous) 방식을 사용합니다. 오래 걸리는 작업은 백그라운드에 맡겨두고, 그동안 다른 코드를 계속 실행하다가 작업이 끝나면 결과를 받아 처리하는 방식입니다.

2. 콜백에서 Promise, 그리고 async/await로

비동기 처리 방식은 시간이 지나면서 계속 발전해왔습니다.

2-1. 콜백 함수 (Callback)

가장 초기 방식은 콜백 함수였습니다.

function getUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: "홍길동" });
  }, 1000);
}

getUser(1, (user) => {
  console.log(user);
  getUser(2, (user2) => {
    console.log(user2);
    getUser(3, (user3) => {
      console.log(user3);
      // 콜백이 계속 중첩되면서 코드가 옆으로 길어짐
    });
  });
});

콜백이 중첩될수록 코드가 오른쪽으로 계속 깊어지는 이른바 "콜백 지옥(Callback Hell)" 현상이 발생해 가독성이 크게 떨어집니다.

2-2. Promise의 등장

이를 개선하기 위해 ES6에서 Promise가 도입되었습니다.

function getUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: "홍길동" });
    }, 1000);
  });
}

getUser(1)
  .then((user) => {
    console.log(user);
    return getUser(2);
  })
  .then((user2) => {
    console.log(user2);
    return getUser(3);
  })
  .then((user3) => {
    console.log(user3);
  })
  .catch((error) => {
    console.error(error);
  });

.then() 체이닝 덕분에 콜백 지옥은 어느 정도 해결됐지만, 로직이 복잡해지면 여전히 코드가 길어지고 흐름을 한눈에 파악하기 어려운 경우가 많습니다.

2-3. async/await의 등장

ES2017(ES8)부터 도입된 async/await는 Promise를 기반으로 하면서도, 마치 동기 코드를 작성하듯 비동기 코드를 작성할 수 있게 해줍니다.

function getUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: "홍길동" });
    }, 1000);
  });
}

async function main() {
  const user1 = await getUser(1);
  console.log(user1);

  const user2 = await getUser(2);
  console.log(user2);

  const user3 = await getUser(3);
  console.log(user3);
}

main();

훨씬 직관적이고 읽기 쉬운 코드가 되었습니다.

3. async/await 기본 문법

3-1. async 키워드

함수 앞에 async를 붙이면 해당 함수는 항상 Promise를 반환하는 비동기 함수가 됩니다.

async function sayHello() {
  return "안녕하세요";
}

sayHello().then((msg) => console.log(msg)); // "안녕하세요"

함수 안에서 일반 값을 return해도, 자동으로 그 값을 담은 Promise로 감싸져서 반환됩니다.

3-2. await 키워드

awaitasync 함수 내부에서만 사용할 수 있으며, Promise가 처리(resolve 또는 reject)될 때까지 함수 실행을 잠시 멈추고 기다립니다.

async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  console.log(data);
}

await를 만나면 해당 Promise가 완료될 때까지 기다린 후, 그 결과값을 반환합니다. 이때 전체 프로그램이 멈추는 것이 아니라, 해당 async 함수의 실행만 잠시 대기 상태가 되고 다른 코드는 계속 실행될 수 있습니다.

4. 에러 처리: try/catch

Promise에서는 .catch()로 에러를 처리했다면, async/await에서는 익숙한 try/catch 문법을 그대로 사용할 수 있습니다.

async function getUserData(id) {
  try {
    const response = await fetch(`https://api.example.com/users/${id}`);

    if (!response.ok) {
      throw new Error(`요청 실패: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("사용자 정보를 가져오는 중 오류 발생:", error.message);
    return null;
  } finally {
    console.log("요청 처리가 완료되었습니다.");
  }
}

try 블록 안에서 await한 Promise가 reject되면 즉시 catch 블록으로 넘어가기 때문에, 동기 코드와 동일한 감각으로 에러를 처리할 수 있다는 점이 큰 장점입니다.

5. 여러 비동기 작업 병렬로 처리하기

5-1. 순차 처리의 함정

아래 코드처럼 서로 관련 없는 작업을 await로 하나씩 순서대로 처리하면 불필요하게 시간이 오래 걸립니다.

async function badExample() {
  const user = await getUser();      // 1초 소요
  const posts = await getPosts();    // 1초 소요
  const comments = await getComments(); // 1초 소요
  // 총 소요 시간: 약 3초
}

세 작업이 서로 의존 관계가 없다면, 굳이 순서대로 기다릴 필요가 없습니다.

5-2. Promise.all로 병렬 처리하기

이럴 때는 Promise.all()을 사용해 여러 작업을 동시에 시작하고, 모두 끝날 때까지 한 번에 기다리는 것이 효율적입니다.

async function goodExample() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ]);
  // 총 소요 시간: 약 1초 (가장 오래 걸리는 작업 기준)

  console.log(user, posts, comments);
}

세 요청이 동시에 시작되기 때문에, 전체 소요 시간이 가장 오래 걸리는 작업 하나의 시간으로 줄어듭니다.

참고: Promise.all()은 하나라도 실패(reject)하면 즉시 전체가 실패로 처리됩니다. 일부가 실패해도 나머지 결과를 모두 받고 싶다면 Promise.allSettled()를 사용하면 됩니다.

6. 실전 예제: API 데이터 가져오기

실제 프로젝트에서 자주 쓰이는 패턴을 정리한 예제입니다.

async function fetchUserProfile(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }

    const profile = await response.json();
    return profile;
  } catch (error) {
    if (error instanceof TypeError) {
      console.error("네트워크 연결을 확인해주세요.");
    } else {
      console.error("프로필 조회 실패:", error.message);
    }
    throw error; // 필요하다면 호출한 쪽으로 에러를 다시 전달
  }
}

// 사용하는 쪽
async function displayProfile() {
  try {
    const profile = await fetchUserProfile(123);
    console.log("프로필:", profile);
  } catch (error) {
    console.log("프로필을 표시할 수 없습니다.");
  }
}

7. 자주 하는 실수와 주의사항

1) forEach 안에서 await를 사용하는 경우

forEach는 async/await와 함께 사용해도 기다려주지 않습니다.

// 잘못된 예: 순서를 보장하지 않고 기다리지도 않음
items.forEach(async (item) => {
  await processItem(item);
});

// 올바른 예 1: for...of 사용 (순차 처리)
for (const item of items) {
  await processItem(item);
}

// 올바른 예 2: Promise.all + map 사용 (병렬 처리)
await Promise.all(items.map((item) => processItem(item)));

2) 최상위 레벨에서 await 사용
모던 모듈 환경(ES 모듈)에서는 top-level await가 가능하지만, 일반 스크립트나 함수 밖에서는 async 함수로 감싸야 합니다.

// async 함수로 감싸서 실행
(async () => {
  const data = await fetchData();
  console.log(data);
})();

3) try/catch를 생략하지 않기

await한 Promise가 실패했을 때 try/catch가 없으면 처리되지 않은 에러(unhandled rejection)로 이어질 수 있으므로, 실패 가능성이 있는 비동기 작업은 항상 에러 처리를 함께 고려하는 것이 좋습니다.

8. 정리

개념 설명
async 함수를 비동기 함수로 선언, 항상 Promise 반환
await Promise가 처리될 때까지 대기, async 함수 내부에서만 사용 가능
try/catch 비동기 작업의 에러를 동기 코드처럼 처리
Promise.all 여러 비동기 작업을 병렬로 처리해 시간 단축

async/await는 복잡한 비동기 로직을 간결하고 읽기 쉬운 코드로 바꿔주는 강력한 도구입니다. 콜백 지옥이나 복잡한 .then() 체이닝 대신, 마치 동기 코드를 작성하듯 자연스러운 흐름으로 비동기 처리를 구현할 수 있다는 것이 가장 큰 장점입니다. 다만 병렬로 처리 가능한 작업까지 무조건 순차적으로 await하지 않도록 주의하고, 상황에 맞게 Promise.all 같은 도구도 함께 활용하는 것이 좋은 비동기 코드를 작성하는 핵심입니다.

반응형
Comments