JavaScript 비동기 프로그래밍 | Promise, async/await 완벽 정리
이 글의 핵심
JavaScript 비동기 프로그래밍: Promise, async/await 콜백 (Callback)·Promise.
들어가며
비동기 프로그래밍이란?
비동기(Asynchronous)는 네트워크·타이머처럼 끝나는 시점이 불확실한 작업을 기다리느라 다음 줄을 막지 않고, 완료 시 콜백·프로미스·async/await로 결과를 받는 방식입니다.
동기 vs 비동기:
// 동기 (Synchronous): 순차적 실행
console.log("1");
console.log("2");
console.log("3");
// 출력: 1 2 3
// 비동기 (Asynchronous): 기다리지 않음
console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");
// 출력: 1 3 2 (1초 후)
왜 비동기가 필요한가?
- ✅ 네트워크 요청: API 호출 (수백 ms ~ 수 초)
- ✅ 파일 읽기/쓰기: 디스크 I/O
- ✅ 타이머:
setTimeout,setInterval - ✅ 사용자 입력: 클릭, 키보드 이벤트
Node.js는 런타임마다 다르지만, 전형적으로 단일 스레드 이벤트 루프 위에서 비슷한 패턴의 비동기 I/O를 씁니다. 반면 C++의
std::async는launch정책에 따라 OS 스레드에 가까운 실행을 염두에 두므로, “비동기”의 이미지가 JS와는 다를 수 있습니다. Kotlin 코루틴이나 Rust의 async/await는 그 사이에서 런타임이 협력적 스케줄링을 맡는 경우가 많습니다.
실전 경험에서 배운 교훈
이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.
가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.
이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.
1. 콜백 (Callback)
기본 콜백
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: "홍길동" };
callback(data);
}, 1000);
}
console.log("시작");
fetchData(data => {
console.log("데이터:", data);
});
console.log("끝");
// 출력:
// 시작
// 끝
// 데이터: { id: 1, name: '홍길동' } (1초 후)
콜백 지옥 (Callback Hell)
// 콜백 지옥: 가독성 나쁨
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
});
});
});
// 피라미드 구조 (Pyramid of Doom)
2. Promise
Promise란?
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 3가지 상태:
- Pending: 대기 (초기 상태)
- Fulfilled: 이행 (성공)
- Rejected: 거부 (실패)
Promise 생성
// Promise 생성
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("성공!"); // 이행
} else {
reject("실패!"); // 거부
}
}, 1000);
});
// Promise 사용
promise
.then(result => {
console.log(result); // 성공!
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log("완료");
});
Promise 체이닝
Promise 체이닝으로 여러 비동기 작업을 순차적으로 실행할 수 있습니다:
// 비동기 함수들 정의
function fetchUser(userId) {
// Promise 반환
return new Promise((resolve) => {
// 1초 후 사용자 데이터 반환 (API 호출 시뮬레이션)
setTimeout(() => {
resolve({ id: userId, name: "홍길동" });
}, 1000);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
// 1초 후 주문 데이터 반환
setTimeout(() => {
resolve([{ id: 1, product: "노트북" }]);
}, 1000);
});
}
function fetchOrderDetails(orderId) {
return new Promise((resolve) => {
// 1초 후 주문 상세 데이터 반환
setTimeout(() => {
resolve({ id: orderId, price: 1200000 });
}, 1000);
});
}
// Promise 체이닝으로 순차 실행
fetchUser(1)
// 1단계: 사용자 조회
.then(user => {
// user: { id: 1, name: "홍길동" }
console.log("사용자:", user);
// 다음 Promise 반환 (중요!)
// return을 해야 다음 .then()으로 전달됨
return fetchOrders(user.id);
})
// 2단계: 주문 조회
.then(orders => {
// orders: [{ id: 1, product: "노트북" }]
console.log("주문:", orders);
// 다음 Promise 반환
return fetchOrderDetails(orders[0].id);
})
// 3단계: 주문 상세 조회
.then(details => {
// details: { id: 1, price: 1200000 }
console.log("상세:", details);
// 마지막 단계는 return 불필요
})
// 에러 처리: 어느 단계에서든 에러 발생 시 catch로 이동
.catch(error => {
console.error("에러:", error);
})
// finally: 성공/실패 관계없이 항상 실행
.finally(() => {
console.log("모든 작업 완료");
});
// 출력 (3초 후):
// 사용자: { id: 1, name: '홍길동' } (1초)
// 주문: [{ id: 1, product: '노트북' }] (2초)
// 상세: { id: 1, price: 1200000 } (3초)
// 모든 작업 완료
체이닝의 핵심 규칙:
// ✅ 올바른 체이닝: Promise 반환
promise
.then(result => {
return anotherPromise(); // Promise 반환
})
.then(result2 => {
// anotherPromise의 결과 받음
});
// ❌ 잘못된 체이닝: return 없음
promise
.then(result => {
anotherPromise(); // return 없음!
})
.then(result2 => {
// result2는 undefined
});
콜백 지옥 vs Promise 체이닝:
// ❌ 콜백 지옥 (Callback Hell)
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
});
});
});
// 들여쓰기가 깊어지고 가독성 나쁨
// ✅ Promise 체이닝 (깔끔)
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));
// 평평한 구조, 가독성 좋음
Promise 정적 메서드
Promise는 여러 비동기 작업을 조합할 수 있는 정적 메서드를 제공합니다:
// 1. Promise.resolve(): 즉시 이행된 Promise 생성
Promise.resolve(42).then(x => console.log(x)); // 42
// 이미 값이 있을 때 Promise로 감싸기
// 동기 값을 비동기 체인에 포함시킬 때 유용
// 2. Promise.reject(): 즉시 거부된 Promise 생성
Promise.reject("에러").catch(e => console.error(e)); // 에러
// 테스트나 에러 시뮬레이션에 사용
// 3. Promise.all(): 모든 Promise가 완료될 때까지 대기
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(results => console.log(results)); // [1, 2, 3]
// 배열의 모든 Promise가 성공해야 then 실행
// 결과는 입력 순서와 동일한 배열로 반환
// ❌ 하나라도 실패하면 전체 실패
Promise.all([
Promise.resolve(1),
Promise.reject("에러"), // 이 에러로 인해
Promise.resolve(3)
]).catch(error => console.error(error)); // 에러
// 첫 번째 에러만 catch로 전달됨
// 나머지 Promise는 무시됨
// 4. Promise.allSettled(): 모두 완료 대기 (실패 무시)
Promise.allSettled([
Promise.resolve(1),
Promise.reject("에러"),
Promise.resolve(3)
]).then(results => console.log(results));
// [
// { status: 'fulfilled', value: 1 }, // 성공
// { status: 'rejected', reason: '에러' }, // 실패
// { status: 'fulfilled', value: 3 } // 성공
// ]
// 각 Promise의 성공/실패 여부와 결과를 모두 반환
// 실패해도 전체가 실패하지 않음 (all과의 차이)
// 5. Promise.race(): 가장 먼저 완료되는 것만 반환
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 1000)),
new Promise(resolve => setTimeout(() => resolve(2), 500)), // 가장 빠름
new Promise(resolve => setTimeout(() => resolve(3), 1500))
]).then(result => console.log(result)); // 2 (500ms 후)
// 타임아웃 구현에 유용:
// Promise.race([fetchData(), timeout(5000)])
// 6. Promise.any(): 가장 먼저 성공하는 것 반환 (ES2021+)
Promise.any([
Promise.reject("에러1"), // 실패 무시
new Promise(resolve => setTimeout(() => resolve(2), 500)), // 첫 성공
new Promise(resolve => setTimeout(() => resolve(3), 1000))
]).then(result => console.log(result)); // 2
// race와 달리 실패는 무시하고 첫 성공만 반환
// 모두 실패하면 AggregateError 발생
실전 활용 예시:
// 병렬 API 호출 (Promise.all)
async function loadDashboard() {
try {
// 3개 API를 동시에 호출 (병렬)
const [user, orders, notifications] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchNotifications()
]);
// 모두 완료되면 한 번에 결과 받음
renderDashboard(user, orders, notifications);
} catch (error) {
console.error("대시보드 로딩 실패:", error);
}
}
// 타임아웃 구현 (Promise.race)
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("시간 초과")), ms)
);
}
async function fetchWithTimeout(url, ms = 5000) {
return Promise.race([
fetch(url),
timeout(ms)
]);
}
3. async/await
async/await 기본
// async 함수는 항상 Promise 반환
async function fetchData() {
return "데이터";
}
fetchData().then(data => console.log(data)); // 데이터
// await: Promise가 완료될 때까지 대기
async function getData() {
const data = await fetchData();
console.log(data); // 데이터
}
getData();
async/await 실전
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchUser(userId) {
await delay(1000);
return { id: userId, name: "홍길동" };
}
async function fetchOrders(userId) {
await delay(1000);
return [{ id: 1, product: "노트북" }];
}
async function fetchOrderDetails(orderId) {
await delay(1000);
return { id: orderId, price: 1200000 };
}
// async/await로 순차 실행
async function main() {
try {
console.log("시작");
const user = await fetchUser(1);
console.log("사용자:", user);
const orders = await fetchOrders(user.id);
console.log("주문:", orders);
const details = await fetchOrderDetails(orders[0].id);
console.log("상세:", details);
console.log("완료");
} catch (error) {
console.error("에러:", error);
}
}
main();
// 출력 (3초 후):
// 시작
// 사용자: { id: 1, name: '홍길동' }
// 주문: [{ id: 1, product: '노트북' }]
// 상세: { id: 1, price: 1200000 }
// 완료
병렬 실행
// 순차 실행 (느림)
async function sequential() {
const user1 = await fetchUser(1); // 1초
const user2 = await fetchUser(2); // 1초
const user3 = await fetchUser(3); // 1초
return [user1, user2, user3]; // 총 3초
}
// 병렬 실행 (빠름)
async function parallel() {
const [user1, user2, user3] = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
return [user1, user2, user3]; // 총 1초
}
// 또는
async function parallel() {
const p1 = fetchUser(1);
const p2 = fetchUser(2);
const p3 = fetchUser(3);
const user1 = await p1;
const user2 = await p2;
const user3 = await p3;
return [user1, user2, user3];
}
4. 에러 처리
try-catch
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("에러 발생:", error.message);
return null;
} finally {
console.log("요청 완료");
}
}
Promise 에러 처리
// then/catch
fetch("https://api.example.com/data")
.then(response => {
if (!response.ok) {
throw new Error("HTTP error");
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log("완료"));
// catch는 체인 어디서든 발생한 에러 처리
Promise.resolve(1)
.then(x => {
throw new Error("에러!");
return x + 1;
})
.then(x => x * 2)
.catch(error => console.error(error.message)) // 에러!
.then(() => console.log("계속 실행")); // 계속 실행
5. 실전 예제
예제 1: API 호출
async function fetchGitHubUser(username) {
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`User not found: ${response.status}`);
}
const user = await response.json();
return {
name: user.name,
bio: user.bio,
repos: user.public_repos
};
} catch (error) {
console.error("에러:", error.message);
return null;
}
}
// 사용
fetchGitHubUser("torvalds").then(user => {
if (user) {
console.log(user);
}
});
예제 2: 재시도 로직
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.log(`시도 ${i + 1} 실패`);
if (i === maxRetries - 1) {
throw new Error("최대 재시도 횟수 초과");
}
// 지수 백오프
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
}
// 사용
fetchWithRetry("https://api.example.com/data")
.then(data => console.log(data))
.catch(error => console.error(error));
예제 3: 타임아웃
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout")), ms);
});
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([
fetch(url),
timeout(ms)
]);
return await response.json();
} catch (error) {
if (error.message === "Timeout") {
console.error("요청 시간 초과");
}
throw error;
}
}
// 사용
fetchWithTimeout("https://api.example.com/data", 3000)
.then(data => console.log(data))
.catch(error => console.error(error));
예제 4: 병렬 처리
async function fetchMultipleUsers(userIds) {
const promises = userIds.map(id => fetchUser(id));
try {
const users = await Promise.all(promises);
return users;
} catch (error) {
console.error("사용자 조회 실패:", error);
return [];
}
}
// 사용
fetchMultipleUsers([1, 2, 3]).then(users => {
console.log(users);
});
// allSettled로 부분 실패 허용
async function fetchMultipleUsersSettled(userIds) {
const promises = userIds.map(id => fetchUser(id));
const results = await Promise.allSettled(promises);
return results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
}
예제 5: 순차 처리
// 순차 처리: 이전 결과가 다음에 필요
async function processSequentially(items) {
let result = 0;
for (let item of items) {
result = await processItem(item, result);
}
return result;
}
// reduce로 순차 처리
async function processSequentially(items) {
return items.reduce(async (accPromise, item) => {
const acc = await accPromise;
return await processItem(item, acc);
}, Promise.resolve(0));
}
6. 이벤트 루프 (Event Loop)
이벤트 루프란?
JavaScript는 싱글 스레드지만, 이벤트 루프로 비동기를 처리합니다.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 출력: 1 4 3 2
실행 순서:
- 동기 코드:
1,4 - Microtask Queue (Promise):
3 - Macrotask Queue (setTimeout):
2
Microtask vs Macrotask
// Macrotask: setTimeout, setInterval
setTimeout(() => console.log("Macrotask"), 0);
// Microtask: Promise, queueMicrotask
Promise.resolve().then(() => console.log("Microtask"));
console.log("Sync");
// 출력:
// Sync
// Microtask (먼저!)
// Macrotask
7. 실전 패턴
패턴 1: 로딩 상태 관리
class DataFetcher {
constructor() {
this.loading = false;
this.data = null;
this.error = null;
}
async fetch(url) {
this.loading = true;
this.error = null;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.data = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
return this.data;
}
}
// 사용
const fetcher = new DataFetcher();
fetcher.fetch("https://api.example.com/data");
// React 스타일
async function loadData() {
setLoading(true);
try {
const data = await fetchData();
setData(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
패턴 2: 캐싱
class CachedFetcher {
constructor() {
this.cache = new Map();
}
async fetch(url) {
if (this.cache.has(url)) {
console.log("캐시에서 반환");
return this.cache.get(url);
}
console.log("네트워크 요청");
const response = await fetch(url);
const data = await response.json();
this.cache.set(url, data);
return data;
}
}
// 사용
const fetcher = new CachedFetcher();
await fetcher.fetch("https://api.example.com/data"); // 네트워크 요청
await fetcher.fetch("https://api.example.com/data"); // 캐시에서 반환
패턴 3: 큐 처리
class TaskQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
this.running++;
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
// 사용
const queue = new TaskQueue(2); // 동시 실행 2개
for (let i = 0; i < 10; i++) {
queue.add(async () => {
console.log(`작업 ${i} 시작`);
await delay(1000);
console.log(`작업 ${i} 완료`);
return i;
});
}
8. 자주 하는 실수와 해결법
실수 1: await 없이 Promise 사용
// ❌ 잘못된 방법
async function getData() {
const data = fetchData(); // await 누락!
console.log(data); // Promise { <pending> }
}
// ✅ 올바른 방법
async function getData() {
const data = await fetchData();
console.log(data); // 실제 데이터
}
실수 2: 순차 vs 병렬
// ❌ 불필요한 순차 실행 (느림)
async function slow() {
const user1 = await fetchUser(1); // 1초
const user2 = await fetchUser(2); // 1초
return [user1, user2]; // 총 2초
}
// ✅ 병렬 실행 (빠름)
async function fast() {
const [user1, user2] = await Promise.all([
fetchUser(1),
fetchUser(2)
]);
return [user1, user2]; // 총 1초
}
실수 3: forEach에서 await
// ❌ forEach는 async 무시
async function processItems(items) {
items.forEach(async (item) => {
await processItem(item); // 기다리지 않음!
});
console.log("완료"); // 즉시 출력됨
}
// ✅ for...of 사용
async function processItems(items) {
for (let item of items) {
await processItem(item); // 순차 대기
}
console.log("완료"); // 모두 완료 후 출력
}
// ✅ Promise.all로 병렬
async function processItems(items) {
await Promise.all(items.map(item => processItem(item)));
console.log("완료");
}
실수 4: try-catch 누락
// ❌ 에러 처리 없음
async function getData() {
const data = await fetchData(); // 실패 시 프로그램 중단
return data;
}
// ✅ 에러 처리
async function getData() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error("에러:", error);
return null;
}
}
9. 연습 문제
문제 1: Promise 체이닝을 async/await로 변환
// Promise 체이닝
function getDataPromise() {
return fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchOrderDetails(orders[0].id))
.catch(error => console.error(error));
}
// async/await 변환
async function getDataAsync() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
return details;
} catch (error) {
console.error(error);
}
}
문제 2: 병렬 + 순차 조합
여러 사용자를 병렬로 가져온 후, 각 사용자의 주문을 순차적으로 처리하세요.
async function processUsers(userIds) {
// 1단계: 사용자 병렬 조회
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);
// 2단계: 각 사용자의 주문 순차 처리
const results = [];
for (let user of users) {
const orders = await fetchOrders(user.id);
results.push({ user, orders });
}
return results;
}
// 테스트
processUsers([1, 2, 3]).then(results => {
console.log(results);
});
문제 3: 프로미스 래퍼
콜백 기반 함수를 Promise로 변환하세요.
// 콜백 기반
function readFileCallback(filename, callback) {
setTimeout(() => {
callback(null, `${filename}의 내용`);
}, 1000);
}
// Promise 래퍼
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
readFileCallback(filename, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
// 사용
readFilePromise("test.txt")
.then(data => console.log(data))
.catch(error => console.error(error));
// async/await
async function main() {
try {
const data = await readFilePromise("test.txt");
console.log(data);
} catch (error) {
console.error(error);
}
}
정리
핵심 요약
- 비동기 처리 방법:
- 콜백: 가장 기본, 콜백 지옥 문제
- Promise: 체이닝, 에러 처리 개선
- async/await: 동기 코드처럼 작성 (가장 권장)
- Promise 메서드:
Promise.all(): 모두 완료 대기Promise.allSettled(): 모두 완료 대기 (실패 무시)Promise.race(): 가장 먼저 완료Promise.any(): 가장 먼저 성공
- 에러 처리:
- Promise:
.catch() - async/await:
try-catch
- Promise:
- 성능:
- 순차:
await순서대로 - 병렬:
Promise.all()
- 순차:
- 이벤트 루프:
- Microtask (Promise) > Macrotask (setTimeout)
베스트 프랙티스
- ✅
async/await우선 사용 - ✅ 병렬 처리 가능하면
Promise.all() - ✅ 항상 에러 처리 (
try-catch) - ✅ 타임아웃 설정
- ✅ 재시도 로직 구현
다음 단계
관련 글
- Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await
- C++ future와 promise |
- Swift 비동기 프로그래밍 | async/await, Task
- C++ 코루틴 |
- JavaScript 함수 | 함수 선언, 화살표 함수, 콜백, 클로저 완벽 정리
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「JavaScript 비동기 프로그래밍 | Promise, async/await 완벽 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「JavaScript 비동기 프로그래밍 | Promise, async/await 완벽 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. JavaScript 비동기 프로그래밍: Promise, async/await 완벽 정리. 콜백 (Callback)·Promise로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. JavaScript 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust 비동기 프로그래밍 | async/await, Tokio
- JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러
- JavaScript 비동기 디버깅 실전 사례 | Promise 체인 에러 추적하기
이 글에서 다루는 키워드 (관련 검색어)
JavaScript, 비동기, Promise, async, await, 콜백, 비동기프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.