
JavaScript에서 비동기 처리를 구현할 때 async/await를 사용하는 경우가 많다.
async/await는 Promise보다 코드 흐름을 읽기 쉽고 동기 코드와 비슷한 형태로 작성할 수 있다는 장점이 있다.
async function getUser() {
const response = await fetch('/api/user')
const user = await response.json()
return user
}
코드만 보면 fetch()가 끝날 때까지 프로그램 전체가 멈춰 있는 것처럼 보일 수 있다.
하지만 실제 동작은 그렇지 않다.
async/await 역시 Promise를 기반으로 동작하기 때문에 JavaScript의 비동기 처리를 제대로 이해하려면 Promise의 동작 방식도 함께 이해해야 한다.
async 함수는 Promise를 반환한다
async가 선언된 함수는 항상 Promise를 반환한다.
async function getName() {
return 'Kim'
}
const result = getName()
console.log(result)
result에는 문자열 Kim이 바로 들어가는 것이 아니라 Promise 객체가 들어간다.
개념적으로 다음과 비슷하게 동작한다.
function getName() {
return Promise.resolve('Kim')
}
따라서 값을 사용하려면 await 또는 then()을 사용해야 한다.
const name = await getName()
또는 다음과 같이 작성할 수 있다.
getName().then(name => {
console.log(name)
})
async/await와 Promise는 서로 다른 비동기 처리 방식이라기보다 같은 Promise 기반 비동기 처리를 다른 문법으로 작성하는 것에 가깝다.
await는 프로그램 전체를 멈추지 않는다
다음 코드를 살펴본다.
async function loadData() {
console.log('1')
const response = await fetch('/api/data')
console.log('2')
return response
}
await를 만나면 loadData() 함수의 실행은 해당 Promise가 처리될 때까지 중단된다.
하지만 JavaScript 실행 환경 전체가 멈추는 것은 아니다.
그동안 다른 이벤트나 작업은 계속 처리할 수 있다.
따라서 await는 동기적인 blocking 코드와 다르다.
const response = await fetch('/api/data')
이 코드는 네트워크 응답이 올 때까지 CPU를 점유하면서 기다리는 것이 아니다.
Promise가 처리되면 중단했던 함수의 실행을 이어간다.
await를 순서대로 사용하면 불필요하게 느려질 수 있다
async/await를 사용할 때 실무에서 자주 발생하는 문제가 있다.
서로 관계없는 API 요청을 순차적으로 처리하는 경우이다.
const user = await fetchUser()
const products = await fetchProducts()
const notifications = await fetchNotifications()
세 요청 사이에 의존성이 없다면 첫 번째 요청이 끝난 뒤 두 번째 요청을 시작할 이유가 없다.
각 요청에 1초씩 걸린다고 가정하면 전체 작업이 약 3초가 걸릴 수 있다.
이런 경우에는 동시에 요청을 시작하는 것이 적절하다.
const [user, products, notifications] = await Promise.all([
fetchUser(),
fetchProducts(),
fetchNotifications()
])
세 작업을 병렬로 시작하기 때문에 가장 오래 걸리는 요청이 완료되는 시점을 기준으로 전체 작업을 마칠 수 있다.
API 요청이 많은 화면에서는 이러한 차이가 실제 로딩 성능에 영향을 줄 수 있다.
Promise.all도 무조건 사용하면 안 된다
여러 비동기 작업이 있다고 해서 항상 Promise.all()을 사용하는 것은 적절하지 않다.
두 번째 요청이 첫 번째 요청 결과를 필요로 하는 경우가 있다.
const user = await fetchUser()
const orders = await fetchOrders(user.id)
fetchOrders()를 호출하려면 user.id가 필요하므로 두 작업은 순차적으로 실행해야 한다.
반면 다음과 같이 서로 독립적인 요청이라면 병렬 처리가 가능하다.
const [profile, notices] = await Promise.all([
fetchProfile(),
fetchNotices()
])
비동기 코드를 작성할 때는 단순히 await를 사용하는 것보다 작업 사이의 의존 관계를 먼저 확인하는 것이 중요하다.
Promise.all의 실패 방식도 알아야 한다
Promise.all()에는 주의할 점이 있다.
전달한 Promise 중 하나라도 reject되면 전체 Promise.all()도 reject된다.
const result = await Promise.all([
fetchUser(),
fetchProducts(),
fetchNotifications()
])
예를 들어 fetchNotifications()에서 오류가 발생하면 Promise.all()은 실패한다.
모든 작업의 성공 여부를 각각 확인해야 한다면 Promise.allSettled()를 사용할 수 있다.
const results = await Promise.allSettled([
fetchUser(),
fetchProducts(),
fetchNotifications()
])
결과는 각각 fulfilled 또는 rejected 상태를 가진다.
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value)
} else {
console.error(result.reason)
}
})
여러 API 중 일부가 실패하더라도 나머지 결과를 사용해야 하는 화면에서 활용할 수 있다.
try/catch의 범위도 중요하다
async/await에서는 일반적으로 try/catch로 오류를 처리한다.
try {
const user = await fetchUser()
return user
} catch (error) {
console.error(error)
}
하지만 하나의 try 안에 너무 많은 작업을 넣으면 어떤 작업에서 오류가 발생했는지 처리하기 어려워질 수 있다.
try {
const user = await fetchUser()
const products = await fetchProducts()
const notifications = await fetchNotifications()
} catch (error) {
console.error(error)
}
각 요청의 실패에 따라 다른 처리가 필요하다면 오류 처리 범위를 분리하는 것이 좋다.
반대로 모든 요청 실패를 동일하게 처리한다면 하나의 try/catch로 묶는 것이 더 단순하다.
오류 처리 구조 역시 API의 특성에 따라 결정해야 한다.
return await는 항상 필요한 것이 아니다
다음과 같은 코드도 자주 볼 수 있다.
async function getUser() {
return await fetchUser()
}
단순히 Promise 결과를 반환하는 것이 목적이라면 다음처럼 작성할 수 있다.
async function getUser() {
return fetchUser()
}
두 코드가 항상 완전히 동일한 것은 아니다.
특히 함수 내부의 try/catch에서 해당 Promise의 rejection을 직접 처리해야 한다면 await가 필요할 수 있다.
async function getUser() {
try {
return await fetchUser()
} catch (error) {
console.error('사용자 조회 실패', error)
throw error
}
}
따라서 return await를 무조건 제거하거나 무조건 사용하는 것보다 오류를 어느 범위에서 처리할 것인지 기준으로 판단하는 것이 적절하다.
비동기 코드에서는 실행 순서를 먼저 생각해야 한다
async/await의 장점은 코드가 읽기 쉽다는 것이다.
하지만 문법이 단순해졌다고 해서 비동기 처리 자체가 단순해진 것은 아니다.
실무에서는 다음 항목을 확인하는 것이 좋다.
- 현재 함수가 어떤 Promise를 반환하는지 확인한다.
- 각 비동기 작업 사이에 의존성이 있는지 확인한다.
- 독립적인 작업이라면 Promise.all()을 고려한다.
- 일부 실패를 허용해야 한다면 Promise.allSettled()을 고려한다.
- 오류를 어느 계층에서 처리할지 결정한다.
async/await를 단순히 Promise를 대신하는 문법으로만 이해하면 API 호출이 불필요하게 순차 실행되거나 오류 처리 범위가 지나치게 넓어지는 코드를 작성하기 쉽다.
결국 중요한 것은 await를 많이 사용하는 것이 아니라 Promise가 언제 생성되고 언제 처리되는지를 이해하는 것이다.
정리
async/await는 JavaScript의 비동기 코드를 읽기 쉽게 만들어주는 문법이다.
하지만 내부적으로 Promise를 기반으로 동작하기 때문에 Promise에 대한 이해를 대신해주지는 않는다.
순차 실행이 필요한 작업에는 await를 사용하고, 독립적인 작업에는 Promise.all()과 같은 방법을 검토한다.
또한 일부 작업의 실패를 허용해야 한다면 Promise.allSettled()을 사용할 수 있다.
실무에서 비동기 코드의 성능과 안정성을 높이려면 문법 자체보다 비동기 작업 사이의 의존성과 실패 처리 방식을 먼저 설계하는 것이 중요하다.
'FrontEnd > JavaScript' 카테고리의 다른 글
| JavaScript에서 Promise.all과 Promise.allSettled의 차이와 사용 기준 (0) | 2026.09.02 |
|---|---|
| [JS] 자바스크립트 기본 제공 함수로 개발/테스트/운영 모드 구분하는 방법 (feat. window.location) (1) | 2023.12.20 |
| [ChatGPT] OpenAi You exceeded your current quota, please check your plan and billing details (0) | 2023.03.22 |
| [JavaScript] 정규표현식 숫자 제외 삭제 하는 방법 (0) | 2022.09.24 |
| [JavaScript] 문자열을 배열로 변경하는 방법, How to change string to Array/Object (0) | 2022.08.24 |
