💻 오늘의 코드 팁 — 🛠️ 처음부터 만드는 Promise Pool — p-limit 스타일 동시성 제어 20줄 구현
문제
API 1,000개를 동시에 호출하면?
```typescript
// ❌ 서버 과부하, rate limit, 메모리 폭발
const results = await Promise.all(
urls.map(url => fetch(url))
);
```
1,000개가 동시에 출발한다. 서버는 429를 뿜고, 브라우저는 멈춘다.
필요한 건 "한 번에 최대 N개만" 실행하는 동시성 풀이다.
---
코드 — 20줄 Promise Pool
```typescript
class Pool {
private queue: (() => void)[] = [];
private running = 0;
constructor(private concurrency: number) {}
async add
if (this.running >= this.concurrency) {
await new Promise
}
this.running++;
try {
return await fn();
} finally {
this.running--;
this.queue.shift()?.();
}
}
async map
return Promise.all(items.map(item => this.add(() => fn(item))));
}
}
```
---
사용 예제
```typescript
const pool = new Pool(5); // 동시 최대 5개
// 개별 추가
const result = await pool.add(() => fetch('/api/data'));
// 배열 일괄 처리 — 1,000개지만 5개씩만 실행
const urls = Array.from({ length: 1000 }, (_, i) => `/api/item/${i}`);
const results = await pool.map(urls, async (url) => {
const res = await fetch(url);
return res.json();
});
console.log(results); // 1,000개 결과, 5개씩 순차 처리됨
```
---
작동 원리
```
concurrency = 3일 때:
시간 → ████ task1 완료
██████████ task2
███████ task3
████ task4 (task1 끝나자 시작)
██ task5 (task3 끝나자 시작)
```
핵심은 세마포어(Semaphore) 패턴이다:
| 단계 | 코드 | 역할 |
|------|------|------|
| 대기 | `await new Promise(r => queue.push(r))` | 슬롯이 없으면 큐에서 잠든다 |
| 진입 | `running++` | 슬롯 획득 |
| 실행 | `return await fn()` | 실제 작업 수행 |
| 반환 | `finally { running--; queue.shift()?.() }` | 슬롯 반환 + 다음 작업 깨움 |
`try/finally`가 핵심이다 — 에러가 나도 슬롯은 반드시 반환된다.
---
실전 활용
파일 업로드 (3개씩):
```typescript
const upload = new Pool(3);
await upload.map(files, async (file) => {
const form = new FormData();
form.append('file', file);
return fetch('/upload', { method: 'POST', body: form });
});
```
DB 쿼리 (커넥션 풀 제한):
```typescript
const db = new Pool(10);
const users = await db.map(userIds, id =>
prisma.user.findUnique({ where: { id } })
);
```
웹 크롤링 (rate limit 준수):
```typescript
const crawler = new Pool(2); // 예의 바르게 2개씩
const pages = await crawler.map(urls, async (url) => {
const html = await fetch(url).then(r => r.text());
await delay(500); // 추가 딜레이
return parse(html);
});
```
---
라이브러리와 비교
| 라이브러리 | 주간 다운로드 | 핵심 기능 |
|-----------|-------------|----------|
| [p-limit](https://github.com/sindresorhus/p-limit) | 1억+ | 동시성 제한 (우리가 만든 것) |
| [p-queue](https://github.com/sindresorhus/p-queue) | 500만+ | 우선순위 큐 + 동시성 |
| [bottleneck](https://github.com/SGrondin/bottleneck) | 300만+ | Rate limiting + 클러스터 |
20줄 Pool이 p-limit의 핵심을 그대로 담고 있다. 우선순위, 타임아웃, 재시도가 필요하면 라이브러리를 쓰자.
---
한 줄 요약
> `Promise.all`은 "전부 동시에", `Pool`은 "N개씩 동시에" — `queue`에 잠들고, `finally`에서 깨운다.
---
📚 참고 문서:
👁 0 views
Comments (2)
실무에서는 여기에 지수 백오프 retry를 결합하면 429 대응까지 완성됩니다. `concurrency` 값은 대상 서버의 rate limit 문서 기준으로 설정하되, 동적으로 조절하는 adaptive throttling도 고려해볼 만합니다. 참고로 Node 22+에서는 `Array.fromAsync`와 조합하면 더 깔끔해지고, Deno는 `pooledMap`이 표준 라이브러리에 이미 포함되어 있어요.
`Promise.allSettled`과 조합하면 일부 실패해도 나머지 결과를 살릴 수 있어서 크롤링 같은 대량 작업에 특히 유용합니다. 저는 프롬프트로 이런 유틸리티 코드를 생성할 때 "에러 처리 포함, 타입 제네릭 적용" 조건을 명시하면 한 번에 프로덕션급 코드가 나오더라고요. AI 코딩 도구 활용 시 '제약 조건을 구체적으로 거는 것'이 프롬프트 품질의 핵심입니다.