
async 완전 정복Javascript에서의 비동기 통신을 시작으로 Python에서의 비동기 통신,
이를 활용한 Python 라이브러리 사용법까지 확인
async functionJavaScript에서 비동기 함수(asynchronous function)를 사용하는 이유는
이벤트 루프(event loop)와 렌더링 최적화 측면에서 매우 중요합니다.
JavaScript는 싱글 스레드(single-threaded) 언어입니다.
즉, 한 번에 하나의 작업만 수행할 수 있어요.
그런데 브라우저에서 동작하는 JavaScript는 다양한 작업을 처리해야 하죠:
이런 작업들을 효율적으로 처리하기 위해 이벤트 루프(event loop)가 사용됩니다.
브라우저는 약 60fps(초당 60프레임) 속도로 화면을 렌더링합니다.
즉, 약 16.67ms마다 화면(프레임)을 업데이트하려고 시도합니다.
하지만 JavaScript 코드 실행이 오래 걸리면,
이 프레임 렌더링이 지연되거나 막히게 됩니다.
이를 렌더링 블로킹이라고 하며, 사용자에게는 버벅임이나 응답 없음처럼 보이게 됩니다.
비동기 함수는 오래 걸리는 작업 (예: API 호출, 파일 읽기, 타이머 등)을
백그라운드(콜백 큐, 마이크로태스크 큐)로 넘기고,
메인 스레드를 블로킹하지 않도록 합니다.
// ⚠️ 가상의 동기 API 예시 — 실제로 fetchSync는 존재하지 않음
function getData() {
const data = fetchSync("https://example.com/data"); // 오래 걸림
console.log("데이터 도착:", data);
}
getData();
이 코드는
fetchSync()가 데이터를 모두 받아올 때까지
JavaScript의 메인 스레드를 블로킹합니다.
따라서 UI 렌더링이나 사용자 입력 처리도 일시적으로 멈추게 됩니다.
const url = "https://jsonplaceholder.typicode.com/posts/1";
// 비동기 함수에서 사용
async function getDataAwait() {
console.log("1. await fetch 호출 전");
const data = await fetch(url); // 비동기
console.log("await 데이터 도착:", data);
console.log("2. await fetch 호출 후");
}
// 요청은 백그라운드에서 실행되지만 응답은 무시됨
function getData() {
console.log("3. fetch 호출 전");
fetch(url) // 비동기 요청
.then((response) => response.json()) // 응답을 JSON으로 파싱
.then((data) => {
console.log("데이터 도착:", data);
})
.catch((error) => {
console.error("에러 발생:", error);
});
console.log("4. fetch 호출 후");
}
getDataAwait();
getData();
1. await fetch 호출 전
2. fetch 호출 전
3. fetch 호출 후
데이터 도착: {
...
}
await 데이터 도착: Response {
...
}
4. await fetch 호출 후
이 코드는
fetch()가 완료될 때까지 기다리는 동안
이벤트 루프는 다른 작업(UI 렌더링, 입력 처리 등)을 계속 수행함.
await는async함수 내의 실행 순서를 보장
Promise 객체Promise란?
Promise는 미래에 완료될 수도 있고 실패할 수도 있는 비동기 작업을 표현하는 객체입니다.
즉, “지금은 없지만 나중에 어떤 값이 생길 거야” 를 약속(Promise)하는 객체입니다.
Promise의 상태 (State)Promise는 3가지 상태를 가집니다.
| 상태 | 설명 |
|---|---|
pending | 대기 중 (비동기 작업이 아직 완료되지 않음) |
fulfilled | 성공 (작업이 완료되고 값이 반환됨) |
rejected | 실패 (에러가 발생함) |
Promise 생성 방법Promise는 비동기 작업의 성공 또는 실패를 처리하기 위한 객체입니다.
가장 기본적인 생성 방법은 new Promise를 사용하는 것입니다.
new Promiseconst promise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = true;
if (success) {
resolve("작업 성공!");
} else {
reject("작업 실패...");
}
});
resolve(value) → 작업 성공 시 호출reject(error) → 작업 실패 시 호출async functionasync 함수는 내부적으로 자동으로 Promise를 반환하므로, Promise를 간단히 정의할 수 있는 방법입니다.
async function asyncTask() {
return "작업 성공!";
}
return → 내부적으로 resolve()와 동일throw → 내부적으로 reject()와 동일예를 들어:
async function asyncTask() {
const success = true;
if (success) {
return "작업 성공!"; // resolve("작업 성공!")
} else {
throw new Error("작업 실패..."); // reject("작업 실패...")
}
}
async function을 사용하면Promise를 더 간결하고 가독성 좋게 정의할 수 있습니다.
await란?await는 Promise를 반환하는 함수 앞에 사용해서,
해당 Promise가 해결(resolve)되거나 거부(reject)될 때까지 기다립니다.
반드시 async 함수 안에서만 사용할 수 있습니다.
await를 사용하는가?await를 사용하면 여러 비동기 작업을 순서대로 실행할 수 있음.async function doTasks() {
const user = await getUser();
const posts = await getPostsByUser(user.id);
console.log(posts);
}
.then() 체인을 사용해야 했지만, await를 사용하면 마치 동기 코드처럼 작성할 수 있어서 훨씬 읽기 쉬움.// 전통적인 방식 (then)
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
// await 사용
async function getData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
}
// 예외처리
async function loadData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('에러 발생:', error);
}
}
JavaScript의 메인 스레드는, 비동기 함수가 처리되기를 기다리는 동안에도 다른 작업을 계속 실행할 수 있습니다.
이는 JavaScript가 싱글 스레드이면서도 비동기적으로 동작할 수 있는 이유이기도 합니다.
비동기 프로그래밍은 JavaScript에서 사용자 경험(UX)과 성능을 높이는 핵심 개념입니다.
그 중심에 있는 것이 바로 Promise, async/await이고,
이를 통해 복잡한 비동기 흐름을 더 예측 가능하고 가독성 있게 만들 수 있습니다.
Promise는 비동기 작업의 상태와 결과를 표현하는 객체로,.then() / .catch()를 통해 결과를 처리할 수 있습니다.async 함수는 자동으로 Promise를 반환하며,await 키워드를 사용하면 마치 동기 코드처럼 자연스럽게 비동기 흐름을 작성할 수 있습니다.await는 여러 비동기 작업을✅ 결론적으로,
Promise와async/await를 이해하면
복잡한 비동기 로직도 간결하고 직관적으로 작성할 수 있으며,
JavaScript의 이벤트 루프와 싱글 스레드 구조에 맞춰 효율적인 프로그램을 만들 수 있습니다.
async defPython에서는 async def로 정의된 함수는 Promise 대신 Coroutine 객체를 반환합니다.
async def foo():
return 42
coro = foo() # 실행되지 않음
result1 = await foo()
result2 = await coro
이때 foo()는 Coroutine 객체이며,
실행하려면 await하거나 asyncio.run() 같은 방식으로 명시적으로 실행시켜야 합니다.
| 개념 | JavaScript | Python |
|---|---|---|
| 비동기 함수 키워드 | async function | async def |
| 반환 객체 | Promise | Coroutine |
| 실행 방식 | 자동 실행 (.then(), await) | 명시적 실행 (await, asyncio.run) |
| 대기 키워드 | await | await |
| 실행 시점 차이 | 즉시 실행 (Promise 생성 시) | await될 때 실행 (지연 실행) |
💡 Python의 Coroutine은 JavaScript의 Promise와 유사하지만
lazy evaluation (지연 실행)이라는 중요한 차이가 있습니다.
await을 만나면 중단되며, 나중에 재개됨async for, async with와 같이 비동기 컨텍스트와 함께 사용 가능asyncio 이벤트 루프에서 스케줄되어 실행await 키워드를 통해 다른 비동기 함수 호출 가능import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://api.github.com',
'https://api.github.com/events',
'https://api.github.com/repos/python/cpython'
]
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
responses = await asyncio.gather(*tasks)
for url, response in zip(urls, responses):
print(f"URL: {url}\n응답 길이: {len(response)}\n")
asyncio.run(main())
# URL 3개를 순서대로 하나씩 처리
await fetch(session, urls[0])
await fetch(session, urls[1])
await fetch(session, urls[2])
# URL 3개를 동시에 요청
tasks = [fetch(session, url) for url in urls]
responses = await asyncio.gather(*tasks)
gather()는 여러 Coroutine을 동시에 실행하고, 결과를 한 번에 반환합니다.
Python의 메인 쓰레드 또한 Coruotine 이 처리되기 위해 기다리는 동안 다른 작업을 진행한다.
Python에서 async def는 Coroutine을 반환하며, 이는 명시적으로 실행되어야 합니다.
JavaScript의 Promise처럼 비동기 처리를 표현하지만,
즉시 실행되지 않고 지연된다는 점이 큰 차이입니다.
asyncio.gather()와 async with, Semaphore 같은 기능을 활용하면
복잡한 동시성 작업도 효과적으로 처리할 수 있습니다.
실무에서는 예외 처리 및 리소스 제한도 함께 고려해야 안정적인 비동기 코드가 됩니다.
FastAPI에서의 async defFastAPI는 ASGI(Application Server Gateway Interface) 표준을 따릅니다.
내부적으로 FastAPI는 요청을 받으면
ASGI 이벤트 루프 상에서 코루틴을 실행하여 비동기 처리합니다.
덕분에 WebSocket, Server-Sent Events, 백그라운드 작업, 비동기 DB 호출 등을 자연스럽게 처리할 수 있습니다.
async def 함수 내부에서 await 사용 가능@app.get("/user/{user_id}")
async def get_user(user_id: int):
user = await fetch_user_from_db(user_id)
return user
fetch_user_from_db()는 비동기 함수 (async def)이며, await으로 호출합니다.await을 감지하고즉, ASGI 서버는 비동기 처리를 통해 요청 처리 중 발생하는 대기 시간을 활용하여 다른 요청도 효율적으로 동시에 처리합니다.
🟢 결과적으로:
# fastapi_async.py
import asyncio
import time
from datetime import datetime
import uvicorn
from fastapi import FastAPI
app = FastAPI()
# ✅ 비동기 버전
@app.get("/ping_async")
async def ping_async():
print(f"[{datetime.now().strftime('%H:%M:%S')}] 비동기 요청 수신")
await asyncio.sleep(2) # 비동기 대기
print(f"[{datetime.now().strftime('%H:%M:%S')}] 비동기 응답 반환")
return {"message": "pong_async", "timestamp": datetime.now().isoformat()}
# ❌ 동기 버전
@app.get("/ping_sync")
def ping_sync():
print(f"[{datetime.now().strftime('%H:%M:%S')}] 동기 요청 수신")
time.sleep(2) # 동기 대기 (이 동안 서버가 멈춤)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 동기 응답 반환")
return {"message": "pong_sync", "timestamp": datetime.now().isoformat()}
gunicorn fastapi_async:app \
-k uvicorn.workers.UvicornWorker \
-w 1 \
-b 0.0.0.0:18080 \
--thread 1
# client.py
import asyncio
from datetime import datetime
import aiohttp
async def fetch(session, url, i):
print(f"[{datetime.now().strftime('%H:%M:%S')}] 요청 {i} 시작 → {url}")
async with session.get(url) as response:
json_data = await response.json()
print(
f"[{datetime.now().strftime('%H:%M:%S')}] 요청 {i} 응답 수신: {json_data['message']}"
)
return json_data
async def run_test(endpoint: str):
print(f"\n▶️ [{endpoint}] 테스트 시작")
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, endpoint, i) for i in range(5)]
results = await asyncio.gather(*tasks)
print(f"✅ [{endpoint}] 테스트 완료")
if __name__ == "__main__":
asyncio.run(run_test("http://127.0.0.1:18080/sleep/")) # 비동기 테스트
asyncio.run(run_test("http://127.0.0.1:18080/sleep-sync")) # 동기 테스트
▶️ [http://127.0.0.1:18080/sleep/] 테스트 시작
[18:16:24] 요청 0 시작 → http://127.0.0.1:18080/sleep/
[18:16:24] 요청 1 시작 → http://127.0.0.1:18080/sleep/
[18:16:24] 요청 2 시작 → http://127.0.0.1:18080/sleep/
[18:16:24] 요청 3 시작 → http://127.0.0.1:18080/sleep/
[18:16:24] 요청 4 시작 → http://127.0.0.1:18080/sleep/
[18:16:29] 요청 0 응답 수신: FastAPI sleep finished
[18:16:29] 요청 1 응답 수신: FastAPI sleep finished
[18:16:29] 요청 2 응답 수신: FastAPI sleep finished
[18:16:29] 요청 3 응답 수신: FastAPI sleep finished
[18:16:29] 요청 4 응답 수신: FastAPI sleep finished
✅ [http://127.0.0.1:18080/sleep/] 테스트 완료
▶️ [http://127.0.0.1:18080/sleep-sync] 테스트 시작
[18:16:29] 요청 0 시작 → http://127.0.0.1:18080/sleep-sync
[18:16:29] 요청 1 시작 → http://127.0.0.1:18080/sleep-sync
[18:16:29] 요청 2 시작 → http://127.0.0.1:18080/sleep-sync
[18:16:29] 요청 3 시작 → http://127.0.0.1:18080/sleep-sync
[18:16:29] 요청 4 시작 → http://127.0.0.1:18080/sleep-sync
[18:16:34] 요청 0 응답 수신: FastAPI sleep finished
[18:16:34] 요청 1 응답 수신: FastAPI sleep finished
[18:16:34] 요청 3 응답 수신: FastAPI sleep finished
[18:16:34] 요청 2 응답 수신: FastAPI sleep finished
[18:16:34] 요청 4 응답 수신: FastAPI sleep finished
✅ [http://127.0.0.1:18080/sleep-sync] 테스트 완료
테스트 결과 두 Endpoint의 차이가 없다.
사실 FastAPI 공식문서에서도 아래처럼 말한다.
모르겠다면, 그냥 `def`를 사용하십시오.
즉, FastAPI에서 알아서 최적화를 해주는 것으로 아래를 참고하면 된다.
**참고**: `path operation functions`에서 필요한만큼 `def`와 `async def`를 혼용할 수 있고, 가장 알맞은 것을 선택해서 정의할 수 있습니다.
FastAPI가 자체적으로 알맞은 작업을 수행할 것입니다.
어찌되었든, 상기 어떠한 경우라도, FastAPI는 여전히 비동기적으로 작동하고 매우 빠릅니다.
그러나 `asyns def`을 수행함으로써 어느 정도의 성능 최적화가 가능합니다.
FastAPI의 async def 사용 가이드| 사용 상황 | 설명 |
|---|---|
| FastAPI 엔드포인트 | HTTP 요청을 처리하는 라우터 함수는 비동기로 처리하는 게 일반적입니다. |
| I/O 작업이 포함된 함수 | DB 쿼리, 파일 읽기, 외부 API 요청 등 시간이 오래 걸리는 작업에는 await을 사용해야 하므로 async def 필요합니다. |
| 사용 상황 | 설명 |
|---|---|
| 순수 계산 로직 | CPU만 사용하는 작업(예: 수학 계산, 문자열 처리 등)은 def가 더 효율적입니다. |
| 단순한 데이터 조작 | 예: dict 필터링, 리스트 정렬 등 동기적 로직은 굳이 async로 만들 필요 없습니다. |
| 항목 | FastAPI | Django REST Framework (DRF) |
|---|---|---|
| 기반 인터페이스 | ✅ ASGI (비동기) 전용 | 🔶 WSGI (동기) 기반 ✅ ASGI 부분 지원 (Django 3.0+) |
| 비동기 처리 | async def 완전 지원100% 비동기 코루틴 설계 | 제한적 지원async def 지원은 있지만 일부 라이브러리는 비동기 미지원 |
| 성능 지향성 | 고성능, 낮은 레이턴시, 병렬성 강점 | 기능 풍부하지만 상대적으로 느림 (특히 I/O 병목 상황에서) |
| WebSocket 지원 | ✅ 완전 지원 (ASGI 구조) | ❌ DRF는 WebSocket 비지원 Django Channels 별도 필요 |
| 설계 철학 | 비동기 웹 API 서버에 최적화(Fast, Modern, Lightweight) | 전통적인 웹 앱 + API에 적합 (Admin, 인증, ORM 통합 등) |
| 생태계 | 경량, 자유로운 설계직접 구성 필요 | 대규모 웹앱에 적합한 내장 기능 (Admin, Auth 등) |
| 대표 서버 | uvicorn, hypercorn (ASGI 서버 필수) | gunicorn, uwsgi (기본 WSGI 서버) |


역할
CGI의 성능 문제(요청마다 프로세스 생성)를 해결하기 위해 등장한 고성능 인터페이스.
특징
활용 예시
Python에서는:

WSGI를 위스키라고 읽는다.
WSGI(Web Server Gateway Interface)는 동적인 웹 페이지 요청을 처리하는 핵심 요소로 사용됩니다.

📌 즉, 실제 동적 처리를 담당하는 주체는 WSGI 애플리케이션이며,
WSGI 서버는 그 중간 연결 통로(미들웨어) 역할을 합니다.
WSGI는 PEP 3333에 정의된 Python 웹 애플리케이션 인터페이스의 표준으로,
함수 호출 기반의 동기적 구조를 전제로 설계되었습니다.
즉, 요청(request)이 들어오면, 응답(response)을 반환할 때까지 한 번에 처리하는 구조입니다.
이는 다음과 같은 특징을 가집니다:
👉 이런 한계 때문에,
비동기 처리가 필요한 경우 ASGI(Asynchronous Server Gateway Interface)가 사용됩니다
(예: FastAPI, Django Channels).
Python 생태계에서는 다음과 같은 WSGI 서버가 많이 사용됩니다:
| 서버 이름 | 특징 |
|---|---|
| Gunicorn | 가장 널리 사용되는 WSGI 서버. 멀티프로세싱 기반 |
| uWSGI | 성능과 확장성 뛰어나며, 많은 옵션 제공 |
| Waitress | 순수 Python 기반, Windows 지원 우수 |
| mod_wsgi | Apache 서버에 내장되어 직접 실행 가능 |

WSGI 서버는 단독으로 사용 가능하지만, 일반적으로 Nginx 같은 프록시 서버와 함께 구성됩니다. 그 이유는 다음과 같은 단점 때문입니다:
👉 그래서 Nginx는 보안, 성능, 정적 파일 처리 등을 맡고, WSGI 서버는 실제 파이썬 애플리케이션 실행을 담당하는 구조가 일반적입니다.
WSGI는 웹 서버가 Python 웹 애플리케이션의 동적 요청을 처리하도록 연결해주는 표준 인터페이스입니다.
WSGI 서버(Gunicorn 등)는 웹 서버로부터 요청을 받아,
최종적으로 Django 같은 WSGI 애플리케이션이 실제 응답을 생성하도록 중계합니다.
결과적으로, 동적 웹 페이지 처리는 항상 WSGI 애플리케이션이 담당합니다.
🧠 그리고 WSGI는 구조적으로 동기 방식이기 때문에, 고성능 비동기 처리를 위해서는
ASGI 같은 새로운 인터페이스가 필요합니다
ASGI(Asynchronous Server Gateway Interface)는
오랫동안 사용되어 온 WSGI(Web Server Gateway Interface)의 정신적 후속 모델
비동기 Python으로의 전환을 위한 인터페이스.
특징
구성 요소
asyncio 기반 비동기 실행async def app(scope, receive, send)“WSGI를 그냥 업그레이드하면 안 돼?”라는 질문은 자주 제기되었지만,
다음과 같은 구조적 한계 때문에 어렵습니다:
ASGI는 다음 세 가지 인자를 받는 비동기 함수(async callable)로 구성됩니다:
async def application(scope, receive, send):
scope: 연결에 대한 정보가 담긴 딕셔너리receive: 클라이언트로부터 이벤트를 비동기적으로 수신send: 클라이언트로 이벤트를 비동기적으로 전송이 구조는 다음을 가능하게 합니다:
HTTP 요청 수신 이벤트 예시
{
"type": "http.request",
"body": b"Hello World",
"more_body": False
}
WebSocket 메시지 전송 예시
{
"type": "websocket.send",
"text": "Hello world!"
}
모든 이벤트는 type 키를 가지며, 이 키를 통해 해당 이벤트의 구조를 알 수 있습니다.
uvloop은 libuv 기반이며, libuv는 JavaScript 엔진 V8(Node.js) 에서도 사용되는 비동기 처리 모듈.ASGI는 WSGI의 상위 호환(superset)으로 설계되었습니다.
즉, WSGI 애플리케이션도 ASGI 서버 내에서 실행할 수 있습니다.