본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 18. 10:45

Godot의 GDScript await 키워드를 활용한 콜백 지옥 해결 방법

요약

Godot 4의 `await` 키워드는 신호-콜백 체인을 선형적인 상하 구조의 코드로 변환하여 코드 가독성과 유지보수성을 크게 향상시키는 핵심 GDScript 기능입니다. 이는 타이머 대기, 애니메이션 순차 실행, 사용자 입력 대기 등 다양한 비동기 작업을 간결하게 처리할 수 있게 합니다. `await`는 신호가 방출될 때까지 제어권을 양보하거나, 내부적으로 `await`를 사용하는 함수 호출을 코루틴으로 만들어 여러 단계의 로직을 중첩된 콜백 없이 순차적으로 실행하는 데 사용됩니다.

핵심 포인트

  • Godot 4에서 `await`는 비동기 작업을 선형적인 코드 흐름(상하 구조)으로 변환하여 '콜백 지옥' 문제를 해결합니다.
  • `await`의 두 가지 형태: 신호 자체를 기다리거나, 내부적으로 `await`가 포함된 함수 호출을 코루틴처럼 사용합니다.
  • 타이머 대기나 애니메이션 순차 실행 시 복잡한 상태 머신이나 중첩 콜백 없이 간결하게 로직을 구성할 수 있습니다.
  • `yield` 키워드는 Godot 3의 방식이며, 최신 4.x 코드에서는 `await`를 사용해야 합니다.

Godot 4에서 await는 코드베이스에서 가장 지저분한 부분을 평탄하게 만들어주는 단 하나의 GDScript 기능입니다. 이는 신호-콜백 (signal-callback) 체인을 선형적인 상하 구조의 코드로 대체하지만, 대부분의 튜토리얼은 다음 노드 트리 (node-tree) 스크린샷으로 넘어가기 위해 이 부분을 그냥 지나치곤 합니다. 다음은 await가 실제로 효과를 발휘하는 지점에 대한 빠른 탐색이며, 오늘 바로 2D 프로젝트에 붙여넣을 수 있는 코드를 포함하고 있습니다.

GDScript에서 await의 형태

공식 GDScript 레퍼런스(reference)는 키워드 목록에서 await를 정확히 한 번 언급합니다: "신호 (signal) 또는 코루틴 (coroutine)이 완료될 때까지 기다립니다." 이것이 API의 전부입니다. 두 가지 형태가 있습니다:

형태 1: 신호를 직접 await 하기

await get_tree().create_timer(1.0).timeout

형태 2: 자체적으로 await를 사용하는 다른 함수를 await 하기

await play_intro_sequence()

형태 1은 신호가 방출 (emit)될 때까지 제어권을 양보 (yield)합니다. 형태 2는 내부에 await가 포함된 모든 함수를 호출자 또한 await 할 수 있는 코루틴 (coroutine)으로 변환하며, 이를 통해 콜백을 중첩하지 않고도 다단계 애니메이션과 전환 (transition)을 구성할 수 있습니다.

Godot 3의 조상 격인 yield는 여전히 키워드 목록에 "전환 (transition)용"으로 남아 있습니다. 새로운 4.x 코드에서는 사용하지 마십시오. 모든 현대적인 API는 await를 기대합니다.

await가 실제 코드를 절약해주는 다섯 가지 사례

1. 별도의 Timer 노드 없는 시간 지연
가장 흔한 패턴입니다:

func flash_warning():
    label.text = "Watch out!"
    await get_tree().create_timer(0.5).timeout
    label.text = ""

이는 다른 튜토리얼에서 보여주는 12줄짜리 상태 머신 (state-machine)을 대체합니다. 타이머는 생성되고, 한 번 await 된 후, 함수가 반환될 때 가비지 컬렉션 (garbage-collected)됩니다. Godot 타이머 문서에서는 이 정확한 패턴을 "실행 후 방치 (fire and forget)" 지연을 수행하는 지원되는 방식으로 명시하고 있습니다.

2. 애니메이션 순서 지정

func play_intro():
    await $AnimationPlayer.animation_finished
    $AnimationPlayer.play("zoom_in")
    await $AnimationPlayer.animation_finished
    $AnimationPlayer.play("fade_out")
    await $AnimationPlayer.animation_finished
    queue_free()

중첩된 콜백 없이, 단 8줄의 코드로 세 개의 애니메이션을 순차적으로 실행합니다.

이 코드의 await 적용 전 버전은 대부분의 "Godot에서 애니메이션을 체이닝하는 방법" 튜토리얼에서 제공하는 방식이며, 코드는 두 배 더 길고 버그가 발생할 가능성(bug surface)은 세 배나 높습니다.

  1. 코루틴 (coroutine) 내부에서 플레이어 입력 대기하기
func wait_for_jump():
    while true:
        var event = await Input.input_event
        if event.is_action_pressed("jump"):
            return

이것이 바로 튜토리얼 게임과 대화 시스템(dialogue systems)이 실제로 필요로 하는 기능입니다. 즉, 플레이어가 특정 키를 누를 때까지 실행을 일시 중단했다가 다시 재개하는 것입니다. await가 없다면, 이는 _input 핸들러와 상태 플래그(state flag), 그리고 폴링(polling) 체크를 모두 구현해야 함을 의미합니다.

  1. 턴제 게임 흐름 (Turn-based game flow)

Reddit 사용자 heyitsdoodler는 제가 설명하는 것보다 더 명확하게 사례를 기술하며 godot-proposals#13597을 제출했습니다: "턴 순서의 연속을 계속하기 전에, 여러 캐릭터가 각기 다른 길이의 작업을 마칠 때까지 기다려야 합니다."

그들의 작동하는 해결책은 각 캐릭터의 task_finished 시그널(signal)에 대해 일련의 await를 사용하는 것입니다. 해당 제안은 마지막 공백을 메워줄 내장 all()any() 헬퍼(helpers)를 요청하고 있습니다. 이 기능들이 도입되기 전까지는 다음과 같은 작은 유틸리티를 작성하십시오:

func await_all(signals: Array) -> void:
    for s in signals:
        await s

순서는 임의적이지만, 이 함수는 모든 시그널이 발생한 후에만 반환됩니다.

  1. 콜백 없는 비동기 HTTP 요청 (Async HTTP requests)
func fetch_high_scores() -> Array:
    var http = HTTPRequest.new()
    add_child(http)
    http.request("https://api.example.com/scores")
    var result = await http.request_completed
    http.queue_free()
    return JSON.parse_string(result[3].get_string_from_utf8())

두 번의 yield, 하나의 반환 값, 그리고 클래스 수준의 상태 머신(state machine)이 필요 없습니다. HTTPRequest는 네 개의 요소로 구성된 배열과 함께 request_completed를 방출(emit)하며, 해당 시그널을 await 하면 정확히 그 배열을 돌려받게 됩니다.

모든 튜토리얼이 생략하는 주의사항 (The gotchas)

해제된(freed) 객체의 시그널을 await 하면 영원히 멈춰버립니다. 만약 some_node.signal_nameawait 하고 있는데, 시그널이 발생하기 전에 해당 노드를 queue_free() 해버리면 코루틴은 절대 재개되지 않습니다. 중요한 await 구문은 any() 스타일의 헬퍼를 사용하여 타임아웃 패턴으로 감싸거나, await가 반환된 후 is_instance_valid()를 통해 확인하십시오.

코루틴 (Coroutines)은 yield를 가로질러 예외 (exceptions)를 포착하지 않습니다. await 이전의 push_error()는 정상적으로 실행되지만, 재개된 절반(resumed half)에서 발생하는 런타임 크래시 (runtime crash)는 불완전한 스택 트레이스 (stack trace)와 함께 보고됩니다. 줄 번호를 신뢰하기 전에 Godot Profiler 탭을 사용하여 의심스러운 시퀀스를 프로파일링하십시오. _ready() 내부에서의 await는 작동하지만, 당신이 완료되기 전에 부모 노드가 로딩을 마칩니다. 만약 다른 스크립트가 당신의 _ready가 여전히 구축 중인 상태를 읽는다면, 그 스크립트가 읽는 값은 await 이전의 값입니다. _ready 내의 첫 번째 await 이전에 기본값을 설정하십시오.

왜 AI 어시스턴트는 계속해서 콜백 지옥 (callback hell)을 작성하는가? 만약 당신이 일반적인 AI 어시스턴트에게 "Godot에서 3초를 기다린 다음 애니메이션을 재생해줘"라고 요청한다면, 대부분은 Timer 노드, timeout 시그널 (signal) 연결, 그리고 콜백 함수 (callback function)를 생성합니다. 틀린 것은 아니지만, 장황하며 10년은 뒤처진 방식입니다. 이 패턴은 단일 예시보다 더 깊게 뿌리 박혀 있습니다. 웹 튜토리얼로 학습된 AI 도구들은 JavaScript의 콜백 패턴, Python의 asyncio.create_task, 또는 C#의 이벤트 (events)를 사용하려는 경향이 있습니다. GDScript의 await는 Python의 await나 C#의 await와 더 유사하지만, 웹 예제들이 다루지 않는 Godot 전용 시그널 타입 (signal types)을 포함하고 있습니다. Godot 에디터 내부에 존재하는 Ziva와 같은 도메인 특화 도구들은 실제 시그널을 확인하고 await 기반의 코드를 생성하지만, 일반적인 도구들은 학습 데이터셋에서 가장 흔한 패턴으로 회귀합니다.

await를 건너뛰어야 할 때 두 가지 경우:

  1. 매 프레임 실행되는 핫 코드 (Hot per-frame code). 매 프레임 실행되어야 하는 작업에는 _process 또는 _physics_process를 사용하십시오. await는 애니메이션 커브 (animation curves)를 위한 것이 아니라, 순차적인 일회성 로직 (one-shot logic)을 위한 것입니다.
  2. 씬 간 통신 (Cross-scene communication). 서로 관련 없는 두 노드가 반응형으로 통신해야 할 때는 connect()를 통해 연결된 시그널이 여전히 정답입니다. await는 하나의 코루틴 내부에서 작동하지만, connect는 시스템 간의 발행-구독 (pub-sub) 모델입니다.

유효한 멘탈 모델 (mental model): await는 위에서 아래로 읽히지만 대기가 필요한 코드를 위한 것입니다. connect는 무언가 발생할 때마다 반응하는 코드를 위한 것입니다.

요약: 타이머, 애니메이션, 입력 프롬프트, 턴 순서, 그리고 HTTP에는 await를 사용하십시오. 중요한 await는 타임아웃 헬퍼 (timeout helpers)로 감싸십시오.

해제된 노드 (freed nodes)를 주의하십시오. _ready 함수 내에서 await를 사용하기 전에 기본값 (defaults)을 설정하십시오. 만약 여러분의 AI 어시스턴트가 이러한 패턴에 대해 여전히 connect("pressed", _on_pressed) 체인을 생성한다면, 그것은 2021년 튜토리얼을 읽고 있는 것입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0