JavaScript를 다루다 보면 async/await를 만나게 된다. 특히 서버 통신, 파일 처리, 데이터베이스 조회 등 시간이 걸리는 작업을 할 때 거의 필수적으로 사용된다. 다만 async/await가 정확히 뭔지, Promise와는 뭐가 다른지, 언제 사용해야 하는지 명확하게 이해하는 개발자는 생각보다 적다. 그냥 "좋다고 해서" 또는 "이 코드를 봤으니까" 복사해서 쓰는 경우가 많다. 

 

async/await를 제대로 이해하면 복잡한 비동기 코드를 간결하게 작성할 수 있고, 에러 처리도 더 명확해진다. 뿐만 아니라 성능 최적화나 동시 실행 같은 고급 기법도 가능해진다. 이번 글에서는 Promise부터 시작해서 async/await까지 차근차근 설명하고, 실무 예제와 함께 흔한 실수들을 다루겠다. 

 

비동기 처리의 필요성

먼저 왜 async/await가 필요한지 이해해야 한다.

// 동기 처리 - 한 줄씩 순서대로 실행
console.log('시작');
var data = fetch('https://api.example.com/data');  // 3초 걸림
console.log(data);
console.log('끝');

// 문제: 3초 동안 다른 코드가 실행되지 않음 (브라우저 멈춤)

이런 문제를 해결하기 위해 JavaScript는 비동기 처리 방식을 제공한다. 

 

1단계: 콜백 함수 (Callback)

가장 오래된 방식이다. 하지만 복잡하고 읽기 어렵다.

// 콜백 사용
function fetchData(callback) {
   setTimeout(function() {
      callback('데이터');
   }, 2000);
}

fetchData(function(data) {
   console.log(data);
});

// 문제: 콜백이 여러 개 중첩되면 "콜백 지옥" 발생
fetchData(function(data1) {
   console.log(data1);
   fetchData(function(data2) {
      console.log(data2);
      fetchData(function(data3) {
         console.log(data3);
         // ... 깊어질수록 읽기 어려움
      });
   });
});

 

2단계: Promise

ES6에서 등장한 Promise는 비동기 작업을 더 깔끔하게 처리한다.

Promise의 기본 구조:

// Promise 생성
var promise = new Promise(function(resolve, reject) {
   setTimeout(function() {
      resolve('성공!');  // 성공
      // reject('실패!');  // 또는 실패
   }, 2000);
});

// Promise 처리
promise
   .then(function(result) {
      console.log(result);  // '성공!' 출력
   })
   .catch(function(error) {
      console.log(error);   // 에러 처리
   })
   .finally(function() {
      console.log('완료');  // 성공/실패 상관없이 항상 실행
   });

 

Promise 체이닝 (여러 작업을 순서대로):

fetch('/api/user/1')
   .then(response => response.json())
   .then(user => {
      console.log(user);
      return fetch('/api/posts/' + user.id);  // 다음 요청
   })
   .then(response => response.json())
   .then(posts => {
      console.log(posts);
   })
   .catch(error => {
      console.log('에러:', error);
   });

Promise도 좋지만, .then()이 계속 체이닝되면 여전히 읽기 어렵다. 

 

3단계: async/await (최신 방식)

async/await는 Promise를 기반으로 하지만, 동기 코드처럼 보이게 작성할 수 있다.

기본 문법:

// async 함수는 항상 Promise를 반환
async function fetchUser() {
   return '사용자 데이터';
}

// await는 async 함수 내에서만 사용 가능
async function getUserData() {
   var user = await fetch('/api/user/1');  // Promise 대기
   console.log(user);
}

getUserData();

 

Promise 체이닝을 async/await로 변환:

// Promise 체이닝 방식 (복잡)
fetch('/api/user/1')
   .then(response => response.json())
   .then(user => {
      console.log(user);
      return fetch('/api/posts/' + user.id);
   })
   .then(response => response.json())
   .then(posts => console.log(posts))
   .catch(error => console.log(error));

// async/await 방식 (간결)
async function getUserAndPosts() {
   var user = await fetch('/api/user/1').then(r => r.json());
   console.log(user);
   
   var posts = await fetch('/api/posts/' + user.id).then(r => r.json());
   console.log(posts);
}

getUserAndPosts();

훨씬 읽기 쉽고 이해하기 쉬워진다! 

 

async/await 에러 처리

try-catch 사용 (권장):

async function getData() {
   try {
      var response = await fetch('/api/data');
      var data = await response.json();
      console.log(data);
      return data;
   } catch(error) {
      console.log('에러 발생:', error);
   } finally {
      console.log('완료');
   }
}

getData();

 

Promise의 catch() 사용:

async function getData() {
   var response = await fetch('/api/data');
   var data = await response.json();
   console.log(data);
}

// 함수를 호출할 때 catch 붙이기
getData()
   .catch(error => {
      console.log('에러:', error);
   });

 

실전 예제

예제 1: 사용자 정보 조회 및 저장

async function getUserAndSave(userId) {
   try {
      // 1. 사용자 정보 조회
      var response = await fetch('/api/user/' + userId);
      var user = await response.json();
      console.log('사용자:', user);
      
      // 2. 데이터베이스에 저장 (PHP 서버로 POST)
      var saveResponse = await fetch('/api/user/save', {
         method: 'POST',
         headers: {
            'Content-Type': 'application/json'
         },
         body: JSON.stringify(user)
      });
      
      var result = await saveResponse.json();
      console.log('저장 결과:', result);
      
   } catch(error) {
      console.log('에러:', error.message);
   }
}

getUserAndSave(123);

 

예제 2: 여러 API 요청을 동시에 처리

// ✗ 잘못된 방법 (순차 처리 - 느림)
async function getMultipleData() {
   var user = await fetch('/api/user/1').then(r => r.json());      // 1초
   var posts = await fetch('/api/posts/1').then(r => r.json());    // 1초
   var comments = await fetch('/api/comments/1').then(r => r.json()); // 1초
   // 총 3초 걸림
}

// ✓ 올바른 방법 (동시 처리 - 빠름)
async function getMultipleData() {
   var [user, posts, comments] = await Promise.all([
      fetch('/api/user/1').then(r => r.json()),
      fetch('/api/posts/1').then(r => r.json()),
      fetch('/api/comments/1').then(r => r.json())
   ]);
   // 총 1초 걸림 (병렬 처리)
}

getMultipleData();

 

예제 3: 파일 업로드 진행률 표시

async function uploadFile(file) {
   try {
      var formData = new FormData();
      formData.append('file', file);
      
      var response = await fetch('/upload.php', {
         method: 'POST',
         body: formData
      });
      
      // 응답이 올 때까지 대기
      var result = await response.json();
      
      if(result.success) {
         console.log('업로드 완료:', result.filename);
      } else {
         throw new Error(result.message);
      }
      
   } catch(error) {
      console.log('업로드 실패:', error.message);
   }
}

// 파일 선택 시 호출
document.getElementById('fileInput').addEventListener('change', function(e) {
   uploadFile(e.target.files[0]);
});

 

흔한 실수

실수 1: await를 붙이지 않음

// ✗ 잘못됨
async function getData() {
   var data = fetch('/api/data').then(r => r.json());  // Promise 객체가 저장됨
   console.log(data);  // Promise {...} 출력
}

// ✓ 올바름
async function getData() {
   var data = await fetch('/api/data').then(r => r.json());
   console.log(data);  // 실제 데이터 출력
}

 

실수 2: async 함수 없이 await 사용

// ✗ 에러 발생
function getData() {
   var data = await fetch('/api/data');  // SyntaxError: await 사용 불가
}

// ✓ async 키워드 필수
async function getData() {
   var data = await fetch('/api/data');
}

 

실수 3: 에러 처리 생략

// ✗ 에러 발생 시 프로그램이 중단될 수 있음
async function getData() {
   var data = await fetch('/api/data').then(r => r.json());
   return data;
}

// ✓ try-catch로 에러 처리
async function getData() {
   try {
      var data = await fetch('/api/data').then(r => r.json());
      return data;
   } catch(error) {
      console.log('에러:', error);
      return null;
   }
}

 

실수 4: 동시 처리 기회를 놓침

// ✗ 순차 처리로 느림 (3초 소요)
async function getDataSequentially() {
   var a = await fetch('/api/a').then(r => r.json());  // 1초
   var b = await fetch('/api/b').then(r => r.json());  // 1초
   var c = await fetch('/api/c').then(r => r.json());  // 1초
   return {a, b, c};
}

// ✓ 병렬 처리로 빠름 (1초 소요)
async function getDataInParallel() {
   var [a, b, c] = await Promise.all([
      fetch('/api/a').then(r => r.json()),
      fetch('/api/b').then(r => r.json()),
      fetch('/api/c').then(r => r.json())
   ]);
   return {a, b, c};
}

 

Promise.all() vs Promise.race()

Promise.all() - 모든 Promise가 완료될 때까지 대기:

async function waitForAll() {
   try {
      var results = await Promise.all([
         fetch('/api/1').then(r => r.json()),
         fetch('/api/2').then(r => r.json()),
         fetch('/api/3').then(r => r.json())
      ]);
      console.log(results);  // 모두 완료 후 배열로 반환
   } catch(error) {
      // 하나라도 실패하면 여기서 에러 처리
      console.log('에러:', error);
   }
}

 

Promise.race() - 가장 먼저 완료된 것 하나만 반환:

async function waitForFirst() {
   var firstResult = await Promise.race([
      fetch('/api/1').then(r => r.json()),
      fetch('/api/2').then(r => r.json()),
      fetch('/api/3').then(r => r.json())
   ]);
   console.log(firstResult);  // 가장 빠른 응답만 출력
}

// 용도: 타임아웃 설정
async function fetchWithTimeout(url, timeout = 5000) {
   return Promise.race([
      fetch(url).then(r => r.json()),
      new Promise((_, reject) => 
         setTimeout(() => reject(new Error('타임아웃')), timeout)
      )
   ]);
}

 

async/await vs Promise 선택 기준
기준 async/await Promise
가독성 ⭐⭐⭐⭐⭐ (우수) ⭐⭐⭐ (중간)
에러 처리 try-catch로 명확 .catch() 사용
성능 동일 동일
복잡한 흐름 제어 조금 더 복잡 체이닝으로 직관적

결론: 대부분의 경우 async/await를 권장한다. 다만 복잡한 흐름 제어가 필요할 때는 Promise 체이닝이 더 나을 수 있다. 

 

async/await는 JavaScript의 비동기 처리를 혁신적으로 단순화했다. 이제 복잡한 비동기 코드도 동기 코드처럼 읽고 쓸 수 있게 되었다. 다만 기본 원리인 Promise를 이해하는 것이 중요하고, 에러 처리와 성능 최적화(동시 처리)를 항상 염두에 두어야 한다. 이 글의 예제들을 직접 실행해보고 응용해보면, async/await를 완벽하게 마스터할 수 있을 것이다.