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

김한결김한결
4 min read

Intro.

타입스크립트에서 코루틴은 전통적인 코루틴의 의미가 아니지만 async/awaitAsync Generator를 통해 유사하게 구현할 수는 있다. 이 글에서 코루틴이 무엇이고, 타입스크립트에서 비동기 처리 매커니즘과 함께 어떻게 코루틴 스타일 프로그래밍을 구현할 수 있는지 살펴본다.


코루틴?

코루틴은 함수 실행을 일시 중단(suspend)하고, 상태를 보존한 채 이후 다시 중단된 지점부터 재개(resume)할 수 있는 함수이다. 일반 함수는 끝날 때까지 연속 실행되지만, 코루틴은 실행 도중에 멈췄다가 나중에 이어서 실행할 수 있단 것이다.

그렇다면 타입스크립트에서 코루틴을 제공할까? 엄밀히 말하면 제공하진 않지만 유사한 기능을 만들어낼 수는 있다. 그걸 가능하게 하는 것이 async/awaitAsync 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)
}

여기서 각 yieldnext() 호출 시점까지 대기하며 중단된다. await로 비동기 작업 역시 포함할 수 있고, 반복적으로 suspend/resume을 수행함으로써 코루틴을 모방하고 있다.


어떤 게 더 코루틴스럽지?

항목async/awaitAsync 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를 쓰면 될 것 같다.

결론적으로, 타입스크립트로도 잘 설계하면 충분히 코루틴 못지않은 구조를 구현할 수 있다. 그리고 코루틴의 개념을 이해한 개발자는 타입스크립트뿐 아니라 파이썬, 코틀린, 러스트 같은 언어에서도 더 나은 비동기 코드를 작성할 수 있다.

0
Subscribe to my newsletter

Read articles from 김한결 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

김한결
김한결