본문으로 건너뛰기
Advertisement

async/await

async/await는 Promise를 더 읽기 쉬운 동기식 코드처럼 작성할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다. ES2017에서 도입되었습니다.


기본 문법

// async 함수 선언
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user; // 자동으로 Promise.resolve(user)로 감싸짐
}

// async 함수는 항상 Promise를 반환
const result = fetchUser(1);
console.log(result); // Promise { <pending> }

// 화살표 함수
const getUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};

// 객체 메서드
const api = {
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
};

// 클래스 메서드
class UserService {
async findById(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}

Promise의 문법적 설탕

async/await와 Promise는 완전히 동등합니다:

// Promise 방식
function fetchUserPromise(id) {
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) throw new Error('HTTP 에러: ' + response.status);
return response.json();
})
.then(user => {
console.log(user.name);
return user;
})
.catch(err => {
console.error('에러:', err);
throw err;
});
}

// async/await 방식 (동일한 동작)
async function fetchUserAsync(id) {
const response = await fetch(`/api/users/${id}`);

if (!response.ok) throw new Error('HTTP 에러: ' + response.status);

const user = await response.json();
console.log(user.name);
return user;
}

// await는 Promise가 아닌 값도 받아들임 (그대로 반환)
async function example() {
const value = await 42; // 그냥 42
const str = await 'hello'; // 그냥 'hello'
console.log(value, str); // 42 hello
}

에러 처리: try/catch

// try/catch로 에러 처리
async function fetchWithErrorHandling(id) {
try {
const response = await fetch(`/api/users/${id}`);

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

const user = await response.json();
return user;

} catch (err) {
if (err.name === 'TypeError') {
console.error('네트워크 연결 실패:', err.message);
} else if (err.message.startsWith('HTTP 404')) {
console.error('사용자를 찾을 수 없습니다');
} else {
console.error('예상치 못한 에러:', err);
}
return null; // 기본값 반환
} finally {
console.log('요청 완료 (성공/실패 무관)');
}
}

// 에러를 위로 전파
async function processUser(id) {
const user = await fetchUser(id); // 에러 발생 시 자동 전파
return transformUser(user);
}

async function main() {
try {
const user = await processUser(1);
console.log(user);
} catch (err) {
console.error('최상위 에러 처리:', err);
}
}

await는 async 함수 내에서만 사용 가능

// 일반 함수에서 await 사용 불가
function normalFunction() {
// const data = await fetch('/api/data'); // SyntaxError!
}

// async 함수에서만 사용 가능
async function asyncFunction() {
const data = await fetch('/api/data'); // OK
}

// 콜백에서 async 사용
const buttons = document.querySelectorAll('button');
buttons.forEach(async (button) => { // 각 콜백이 async 함수
button.addEventListener('click', async (event) => {
const data = await fetchData();
console.log(data);
});
});

// Array 메서드의 async 콜백 주의
const ids = [1, 2, 3];

// 잘못된 예: forEach는 async를 기다리지 않음
ids.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // 순서 보장 안 됨
});

// 올바른 예: for...of 사용
for (const id of ids) {
const user = await fetchUser(id);
console.log(user); // 순서대로 실행
}

Top-level await (ES2022)

ES2022부터 ES 모듈에서 최상위 레벨에서 await를 사용할 수 있습니다.

// config.mjs - 모듈 최상위에서 await 사용
const config = await fetch('/api/config').then(r => r.json());

export const apiUrl = config.apiUrl;
export const maxRetries = config.maxRetries;

// 이 모듈을 import하면 config 로드가 완료된 후 사용 가능
// main.mjs
import { apiUrl, maxRetries } from './config.mjs';
// config.mjs의 top-level await가 완료된 후 import됨

console.log(apiUrl); // 이미 로드된 값
// 데이터베이스 연결 초기화
// db.mjs
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);
await client.connect(); // Top-level await

export const db = client.db('myapp');
export const usersCollection = db.collection('users');

순차 실행 vs 병렬 실행

이것이 async/await에서 가장 중요한 성능 개념입니다.

// 순차 실행 (각 await가 이전 것이 완료된 후 실행)
async function sequential() {
console.time('순차');

const user = await fetchUser(1); // 100ms 대기
const posts = await fetchPosts(1); // 100ms 대기
const comments = await fetchComments(1); // 100ms 대기

console.timeEnd('순차'); // ~300ms
return { user, posts, comments };
}

// 병렬 실행 (Promise.all 사용)
async function parallel() {
console.time('병렬');

const [user, posts, comments] = await Promise.all([
fetchUser(1), // 동시에 시작
fetchPosts(1), // 동시에 시작
fetchComments(1) // 동시에 시작
]);

console.timeEnd('병렬'); // ~100ms (가장 느린 것에 맞춤)
return { user, posts, comments };
}

// 의존성이 있는 경우: 일부는 순차, 일부는 병렬
async function mixed() {
const user = await fetchUser(1); // 먼저 필요

// user.id가 있어야 하지만, 두 요청은 병렬로
const [posts, followers] = await Promise.all([
fetchPosts(user.id),
fetchFollowers(user.id)
]);

return { user, posts, followers };
}

Promise.all + await 조합 패턴

// 기본 패턴
async function loadDashboard(userId) {
const [profile, stats, activity] = await Promise.all([
getProfile(userId),
getStats(userId),
getActivity(userId)
]);

return { profile, stats, activity };
}

// 에러 처리 포함
async function loadDashboardSafe(userId) {
const results = await Promise.allSettled([
getProfile(userId),
getStats(userId),
getActivity(userId)
]);

return {
profile: results[0].value ?? null,
stats: results[1].value ?? {},
activity: results[2].value ?? []
};
}

// 동적 병렬 처리
async function processAll(items) {
// 모든 항목을 동시에 처리
const promises = items.map(item => processItem(item));
return Promise.all(promises);
}

// 배치 처리 (동시성 제한)
async function processBatch(items, batchSize = 5) {
const results = [];

for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processItem));
results.push(...batchResults);
console.log(`배치 ${Math.floor(i/batchSize) + 1} 완료`);
}

return results;
}

에러 처리 패턴: try/catch vs .catch()

// 방법 1: try/catch (async/await 스타일)
async function withTryCatch() {
try {
const data = await riskyOperation();
return processData(data);
} catch (err) {
handleError(err);
return defaultValue;
}
}

// 방법 2: .catch() (Promise 스타일)
async function withCatch() {
const data = await riskyOperation().catch(err => {
handleError(err);
return null;
});

if (!data) return defaultValue;
return processData(data);
}

// 방법 3: 에러를 값으로 처리 (Go 언어 스타일)
async function withErrorAsValue() {
const [err, data] = await riskyOperation()
.then(data => [null, data])
.catch(err => [err, null]);

if (err) {
handleError(err);
return defaultValue;
}

return processData(data);
}

// 헬퍼 함수
function to(promise) {
return promise.then(data => [null, data]).catch(err => [err, null]);
}

async function example() {
const [err, user] = await to(fetchUser(1));
if (err) return console.error(err);

const [err2, posts] = await to(fetchPosts(user.id));
if (err2) return console.error(err2);

console.log(user, posts);
}

실전 예제: API 클라이언트

class ApiClient {
#baseUrl;
#defaultHeaders;
#timeout;

constructor(baseUrl, options = {}) {
this.#baseUrl = baseUrl;
this.#defaultHeaders = {
'Content-Type': 'application/json',
...options.headers
};
this.#timeout = options.timeout ?? 10000;
}

async #request(method, path, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);

try {
const response = await fetch(`${this.#baseUrl}${path}`, {
method,
headers: { ...this.#defaultHeaders, ...options.headers },
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal
});

if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new ApiError(
errorBody.message ?? `HTTP ${response.status}`,
response.status,
errorBody
);
}

if (response.status === 204) return null;
return response.json();

} catch (err) {
if (err.name === 'AbortError') {
throw new ApiError('요청 타임아웃', 408);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}

async get(path, options) {
return this.#request('GET', path, options);
}

async post(path, body, options) {
return this.#request('POST', path, { ...options, body });
}

async put(path, body, options) {
return this.#request('PUT', path, { ...options, body });
}

async delete(path, options) {
return this.#request('DELETE', path, options);
}
}

class ApiError extends Error {
constructor(message, status, data = {}) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}

// 사용 예시
const api = new ApiClient('https://api.example.com', {
headers: { Authorization: `Bearer ${token}` },
timeout: 5000
});

async function createPost(title, content) {
try {
const post = await api.post('/posts', { title, content });
console.log('생성됨:', post.id);
return post;
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
await refreshToken();
return createPost(title, content); // 재시도
}
throw err;
}
}

재시도 로직

// 지수 백오프 재시도
async function withRetry(fn, options = {}) {
const {
attempts = 3,
baseDelay = 1000,
maxDelay = 30000,
shouldRetry = (err) => true
} = options;

let lastError;

for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;

if (attempt === attempts - 1 || !shouldRetry(err)) {
throw err;
}

const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * 0.3 * delay; // ±30% 지터
console.log(`재시도 ${attempt + 1}/${attempts}, ${Math.round(delay + jitter)}ms 후...`);

await new Promise(resolve => setTimeout(resolve, delay + jitter));
}
}

throw lastError;
}

// 사용
const data = await withRetry(
() => fetch('/api/unstable').then(r => r.json()),
{
attempts: 5,
baseDelay: 500,
shouldRetry: (err) => err.status !== 404 // 404는 재시도 안 함
}
);

고수 팁

1. async 함수의 반환값

async function examples() {
// 모두 Promise를 반환
return 42; // Promise.resolve(42)
return Promise.resolve(42); // 이미 Promise (자동 평탄화)
throw new Error('실패'); // Promise.reject(new Error('실패'))
}

2. await를 잊었을 때 디버깅

// 흔한 실수
async function buggy() {
const data = fetchData(); // await 없음!
console.log(data.name); // TypeError: data.name is undefined
// ↑ data는 Promise 객체
}

// async 함수 반환 즉시 처리
async function correct() {
const data = await fetchData();
console.log(data.name); // 올바름
}

3. 병렬화 실수

// 버그: Promise를 먼저 만들지 않으면 순차 실행됨
async function buggyParallel() {
// 이것도 순차 실행! await가 바로 붙어있기 때문
const p1 = await fetchA(); // 완료 대기
const p2 = await fetchB(); // p1 완료 후 시작
}

// 올바른 병렬 실행
async function trueParallel() {
const p1 = fetchA(); // 즉시 시작 (await 없음)
const p2 = fetchB(); // 즉시 시작

const [a, b] = await Promise.all([p1, p2]); // 둘 다 완료 대기
}
Advertisement