타입스크립트에서 코루틴이란?


Intro.
타입스크립트에서 코루틴은 전통적인 코루틴의 의미가 아니지만 async/await
와 Async Generator
를 통해 유사하게 구현할 수는 있다. 이 글에서 코루틴이 무엇이고, 타입스크립트에서 비동기 처리 매커니즘과 함께 어떻게 코루틴 스타일 프로그래밍을 구현할 수 있는지 살펴본다.
코루틴?
코루틴은 함수 실행을 일시 중단(suspend
)하고, 상태를 보존한 채 이후 다시 중단된 지점부터 재개(resume
)할 수 있는 함수이다. 일반 함수는 끝날 때까지 연속 실행되지만, 코루틴은 실행 도중에 멈췄다가 나중에 이어서 실행할 수 있단 것이다.
그렇다면 타입스크립트에서 코루틴을 제공할까? 엄밀히 말하면 제공하진 않지만 유사한 기능을 만들어낼 수는 있다. 그걸 가능하게 하는 것이 async/await
와 Async Generator
를 활용한 비동기 흐름이다.
코루틴과 유사한 비동기 처리 매커니즘
타입스크립트에서 코루틴과 유사한 비동기 처리 매커니즘에는 아래 두 가지가 있다. 하나씩 간략하게 알아보자.
async/await
Promise
를 기반으로 하지만, 비동기 코드를 동기 코드처럼 위에서 아래로 실행되는 구조를 제공한다. 중단과 재개가 가능하다는 점에서 코루틴과 가장 유사하다.
async function loadUser() {
try {
const res = await fetch('/api/users')
const data = await res.json()
console.log(data)
} catch (error) {
console.error(error)
}
}
await
는 해당 Promise
가 해결될 때까지 실행을 중단하고, 중단된 시점의 컨텍스트를 보존한다. 그 이후에 이벤트 루프가 재개 시점을 예약하여 다시 이어서 실행된다. 이는 코루틴의 일시 중단 및 재개 구조와 유사하다.
Async Generator
비동기 처리와 Generator
를 결합한 형태이다. for await ... of
구문을 통해 스트리밍 또는 반복되는 비동기 데이터를 처리할 수 있고, 실질적인 코루틴 구조와 가장 가까운 형태이다.
async function* asyncCounter() {
for (let i=0; i<3; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
yield i
}
}
for await (const num of asyncCounter()) {
console.log(num)
}
여기서 각 yield
는 next()
호출 시점까지 대기하며 중단된다. await
로 비동기 작업 역시 포함할 수 있고, 반복적으로 suspend
/resume
을 수행함으로써 코루틴을 모방하고 있다.
어떤 게 더 코루틴스럽지?
항목 | async/await | Async Generator |
흐름 제어 단위 | 함수 단위 | 반복(iteration) 단위 |
중단 방식 | await 로 일시 정지 | yield await 로 다단계 정지 |
상태 보존 | 단일 재개 | 반복 재개 가능 (multi-yield) |
유사성 | 표현이 간결하고 직관적 | 구조적으로 더 코루틴에 가까움 |
어떤 게 더 코루틴스러운가를 따지기 보단 실무에서 마주한 문제가 어떤 패턴이 더 어울릴까?가 올바른 질문 되시겠다. 단일 작업 흐름에서는 async/await
가 간단하고 실용적이겠고, 다단계 반복이나 스트리밍 제어에서는 Async Generator
를 쓰면 될거 같다.
이벤트 루프와의 관계
코루틴의 핵심은 제어권의 양도이다. 타입스크립트의 비동기 로직도 이벤트 루프를 통해 이 제어권 이동을 구현하는 것이다.
async 함수의 중단과 재개
console.log('A')
async function run() {
console.log('B')
await Promise.resolve()
console.log('C')
}
run()
console.log('D')
출력은 A->B->D->C 이다. await
을 만나면 함수 실행은 중단되고, 후속 작업은 마이크로태스크 큐에 등록된다. 콜 스택이 비어야 마이크로태스크가 먼저 실행되므로 C
가 출력된다.
코드 위치 | 실행 시점 | 출력 |
console.log('A') | 동기 → 즉시 실행 | A |
console.log('B') | run() 진입 직후 | B |
await Promise.resolve() | 일시 중단 지점 | (중단) |
console.log('D') | 동기 → 이어서 실행 | D |
console.log('C') | 마이크로태스크 처리 | C |
Async Generator의 반복 중단
async function* counter() {
yield await Promise.resolve(1)
yield await Promise.resolve(2)
}
(async () => {
const gen = counter()
console.log('Start')
console.log(await gen.next())
console.log(await gen.next())
console.log('Done')
})()
Start
{ value: 1, done: false }
{ value: 2, done: false }
Done
next()
호출이 있을 때마다 다음 yield
까지만 실행된다. 실행되고 나서 상태가 보존되고, 실행 흐름은 이벤트 루프를 통해 스케줄링 된다. 이벤트 루프가 suspend
/resume
구조를 가능하게 하는 실질적 기반이 되는 것이다.
코드 위치 | 실행 시점 설명 | 출력 |
const gen = counter() | Generator 객체 생성, 내부 코드 실행 안 됨 | (출력 없음) |
console.log('Start') | 동기 코드, 즉시 실행 | Start |
await gen.next() | - counter() 실행 시작 | |
- await Promise.resolve(1) → 마이크로태스크 예약 | ||
- yield 1 반환 | { value: 1, done: false } | |
await gen.next() | - 이전 지점부터 재개 | |
- await Promise.resolve(2) → 마이크로태스크 예약 | ||
- yield 2 반환 | { value: 2, done: false } | |
console.log('Done') | Generator 종료 후 실행 | Done |
Conclusion.
타입스크립트에서 코루틴을 구현할 공식 키워드는 없지만, async/await
는 단일 suspend
/resume
흐름을, Async Generator
는 반복 suspend
/resume
구조를 통해 코루틴이 가진 제어 흐름 분리, 상태 보존, 협력 실행 모델을 흉내낼 수 있다.
따라서, 코루틴에 가까운 구조를 구현하고자 한다면 비동기 작업의 순차 실행은 async/await
, 스트리밍과 반복 비동기 흐름 제어는 Async Generator
를 쓰면 될 것 같다.
결론적으로, 타입스크립트로도 잘 설계하면 충분히 코루틴 못지않은 구조를 구현할 수 있다. 그리고 코루틴의 개념을 이해한 개발자는 타입스크립트뿐 아니라 파이썬, 코틀린, 러스트 같은 언어에서도 더 나은 비동기 코드를 작성할 수 있다.
Subscribe to my newsletter
Read articles from 김한결 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
