Godot 4 시그널 패턴 — 완전 가이드

1. 소개 — Godot 4에서 무엇이 바뀌었나

Godot 4는 시그널의 작동 방식을 완전히 개편했습니다. Godot 3에서 넘어왔다면, 기존의 문자열 기반 object.connect("signal_name", target, "method_name") 문법은 이제 사라졌습니다. 그 대신 Godot 4는 타입 안전하고 리팩터링에 유리하며 런타임이 아니라 컴파일 시점에 오류를 잡아내는 Callable 기반 API를 사용합니다.

이 가이드는 Godot 4.4+에서 시그널에 관해 알아야 할 모든 것을 다룹니다: 선언, 연결, 연결 해제, 발생, await, 그리고 실전에서 가장 유용한 패턴까지.

핵심 정리

Godot 4에서 시그널은 일급 객체(first-class object)입니다. 프로퍼티처럼 접근하고(예: button.pressed) 그 위에서 메서드를 호출합니다(.connect(), .emit(), .disconnect()). 더 이상 문자열 기반 API는 필요 없습니다.

2. 커스텀 시그널 선언하기

스크립트 맨 위에서 signal 키워드로 시그널을 선언합니다. 문서화와 에디터 자동 완성을 위해 파라미터 이름과 타입을 선택적으로 지정할 수 있습니다.

player.gd
# Simple signal with no parameters
signal game_over

# Signal with typed parameters
signal health_changed(new_health: int)

# Signal with multiple parameters
signal item_picked_up(item_name: String, quantity: int)

# Signal with no type hints (works, but typed is recommended)
signal something_happened(data)

시그널 파라미터에는 항상 타입 힌트를 붙이세요. 그러면 에디터에서 자동 완성이 활성화되고 코드가 자체 문서화됩니다. 타입 힌트가 있는 시그널은 노드 독의 Signals 탭에서도 파라미터 정보를 보여줍니다.

3. 시그널 연결하기 (새로운 방식)

Godot 4의 가장 큰 변화는 시그널을 연결하는 방식입니다. 문자열을 넘기는 대신, 시그널을 프로퍼티로 사용하고 Callable(함수에 대한 참조)을 인자로 .connect()를 호출합니다.

기본 연결

GDScript
# Godot 3 (OLD — no longer works in Godot 4)
# button.connect("pressed", self, "_on_button_pressed")

# Godot 4 — callable-based connection
button.pressed.connect(_on_button_pressed)

func _on_button_pressed() -> void:
    print("Button was pressed!")

bind()로 추가 인자를 넘기는 연결

.bind()를 사용하면 시그널과 함께 추가 데이터를 넘길 수 있습니다. 여러 시그널을 같은 메서드에 연결할 때 유용합니다.

GDScript
# Pass extra data via bind()
buy_button.pressed.connect(_on_item_button.bind("sword"))
sell_button.pressed.connect(_on_item_button.bind("shield"))

func _on_item_button(item_id: String) -> void:
    print("Selected item: ", item_id)

람다(인라인) 연결

짧은 핸들러의 경우 람다 함수를 직접 사용할 수 있습니다. 이렇게 하면 간단한 로직을 연결 지점 가까이에 둘 수 있습니다.

GDScript
# Lambda — great for one-liners
button.pressed.connect(func(): print("Button pressed!"))

# Lambda with parameters
health_changed.connect(func(hp: int): health_label.text = str(hp))

# Multi-line lambda
enemy.died.connect(func():
    score += 100
    score_label.text = "Score: %d" % score
    print("Enemy defeated!")
)
주의

람다 연결은 익명 함수에 대한 참조가 없기 때문에 쉽게 연결을 해제할 수 없습니다. 나중에 연결을 해제해야 한다면 Callable을 변수에 저장하거나 이름이 있는 메서드를 대신 사용하세요.

에디터에서 연결하기 (노드 독)

여전히 Godot 에디터 UI를 통해 시그널을 연결할 수 있습니다. 노드를 선택하고 노드 독 > Signals 탭을 열어 시그널을 더블 클릭한 뒤, 대상 노드와 메서드를 선택합니다. 그러면 에디터가 스크립트에 _on_button_pressed() 같은 메서드를 자동 생성합니다. 이는 코드에서 .connect()를 호출하는 것과 동일합니다 — 단지 타이핑을 줄여줄 뿐입니다.

4. 시그널 연결 해제하기

더 이상 시그널을 받고 싶지 않을 때 연결을 해제합니다. 노드가 해제되거나 게임 상태가 전환될 때 오류를 방지하기 위해 중요합니다.

GDScript
# Disconnect a signal
button.pressed.disconnect(_on_button_pressed)

# Always check before disconnecting to avoid errors
if button.pressed.is_connected(_on_button_pressed):
    button.pressed.disconnect(_on_button_pressed)
모범 사례

씬을 넘나드는 시그널 연결(예: Autoload에 연결)의 경우, 노드가 해제될 때 댕글링 참조를 방지하기 위해 항상 _exit_tree()에서 연결을 해제하세요:

GDScript
func _ready() -> void:
    EventBus.player_died.connect(_on_player_died)

func _exit_tree() -> void:
    if EventBus.player_died.is_connected(_on_player_died):
        EventBus.player_died.disconnect(_on_player_died)

5. 시그널 발생시키기

Godot 4에서는 기존의 emit_signal() 대신 .emit()을 사용합니다. 시그널은 객체이므로 그 위에서 직접 메서드를 호출합니다.

player.gd
signal health_changed(new_health: int)
signal died

var health: int = 100

func take_damage(amount: int) -> void:
    health -= amount
    health_changed.emit(health)  # Emit with argument

    if health <= 0:
        died.emit()  # Emit with no arguments
참고

기존의 emit_signal("signal_name")은 Godot 4에서도 기술적으로 여전히 동작하지만 더 이상 권장되지 않습니다(deprecated). 새 코드에서는 항상 signal_name.emit()을 사용하세요. 문자열 기반 버전은 향후 Godot 릴리스에서 제거될 수 있습니다.

6. 시그널 await 하기

Godot 4는 yield()await로 대체했습니다. 이를 통해 시그널이 발생할 때까지 함수를 일시 정지할 수 있어, 컷신, 튜토리얼, 순차 애니메이션, 시간 기반 이벤트에 완벽하게 어울립니다.

GDScript
func play_cutscene() -> void:
    # Wait for a timer
    await get_tree().create_timer(2.0).timeout

    # Wait for an animation to finish
    animation_player.play("intro")
    await animation_player.animation_finished

    # Wait for player input (custom signal)
    dialogue_label.text = "Press any key to continue..."
    await player_pressed_continue

    # Continue execution after the signal fires
    print("Cutscene complete!")

await한 시그널에서 값 얻기

시그널이 인자를 발생시키면 await가 그 인자를 반환합니다. 인자가 하나면 값이 그대로 반환되고, 여러 개면 배열이 반환됩니다.

GDScript
# Single parameter — returns the value directly
var final_health: int = await health_changed
print("Health is now: ", final_health)

# Multiple parameters — returns an array
var result = await item_picked_up
var item_name: String = result[0]
var quantity: int = result[1]

7. 시그널 연결 플래그

Godot는 .connect()의 두 번째 인자로 연결 플래그를 제공하며, 이를 통해 연결의 동작을 변경할 수 있습니다.

CONNECT_ONE_SHOT

시그널이 한 번 발생하면 연결이 자동으로 제거됩니다. 죽음 애니메이션이나 업적 잠금 해제 같은 일회성 이벤트에 완벽합니다.

GDScript
# Auto-disconnects after firing once
enemy.died.connect(_on_first_kill, CONNECT_ONE_SHOT)

func _on_first_kill() -> void:
    unlock_achievement("first_blood")

CONNECT_DEFERRED

연결된 메서드는 즉시가 아니라 현재 프레임의 끝(유휴 시간 중)에 호출됩니다. 시그널 핸들러가 씬 트리를 수정할 때 유용한데, 물리 처리나 시그널 처리 중에 씬 트리를 수정하는 것은 안전하지 않기 때문입니다.

GDScript
# Called at end of frame — safe for scene tree changes
button.pressed.connect(_on_restart, CONNECT_DEFERRED)

func _on_restart() -> void:
    get_tree().reload_current_scene()

플래그 조합하기

GDScript
# One-shot AND deferred
trigger.body_entered.connect(_on_trigger, CONNECT_ONE_SHOT | CONNECT_DEFERRED)

8. 자주 쓰는 패턴

EventBus (전역 시그널 허브)

EventBus 패턴은 Autoload 싱글톤을 사용해 노드를 느슨하게 결합합니다. 어떤 노드든 다른 노드에 대한 직접 참조 없이 전역 이벤트를 발생시키거나 수신할 수 있습니다. Godot에서 가장 강력한 패턴 중 하나입니다.

event_bus.gd (Autoload)
extends Node

# Define all global signals in one place
signal player_died
signal score_changed(new_score: int)
signal level_completed(level_id: int)
signal item_collected(item_name: String)
signal settings_changed
player.gd (emitter)
func die() -> void:
    # Any script can emit global signals
    EventBus.player_died.emit()
hud.gd (listener)
func _ready() -> void:
    # Any script can listen to global signals
    EventBus.score_changed.connect(_on_score_changed)
    EventBus.player_died.connect(_on_player_died)

func _on_score_changed(new_score: int) -> void:
    score_label.text = "Score: %d" % new_score

func _on_player_died() -> void:
    game_over_screen.show()
Autoload 설정하기

프로젝트 > 프로젝트 설정 > 자동 로드(Autoload)로 이동해 event_bus.gd 스크립트를 추가하고 이름을 EventBus로 지정하세요. 그러면 자동으로 전역에서 접근 가능한 싱글톤이 됩니다.

시그널 릴레이 (부모-자식 통신)

부모 노드가 자식들의 시그널을 수신하여 정보를 중계하거나 집계합니다. 자식 노드들은 서로의 존재를 알 필요가 없습니다.

inventory.gd
signal inventory_updated

func _ready() -> void:
    # Connect to all slot children
    for slot in get_children():
        if slot.has_signal("item_changed"):
            slot.item_changed.connect(_on_slot_changed)

func _on_slot_changed() -> void:
    inventory_updated.emit()

순차 게임 흐름을 위한 Signal + Await

level_manager.gd
func run_level() -> void:
    spawn_enemies()
    await EventBus.all_enemies_defeated

    show_treasure_chest()
    await EventBus.chest_opened

    play_exit_animation()
    await get_tree().create_timer(1.5).timeout

    load_next_level()

스폰된 노드를 위한 동적 연결

enemy_spawner.gd
func spawn_enemy(pos: Vector2) -> void:
    var enemy = enemy_scene.instantiate()
    enemy.position = pos

    # Connect signals before adding to scene tree
    enemy.died.connect(_on_enemy_died.bind(enemy))
    enemy.health_changed.connect(_on_enemy_health_changed)

    add_child(enemy)

func _on_enemy_died(enemy: Node) -> void:
    enemies_alive -= 1
    enemy.queue_free()

9. 마이그레이션 치트 시트 (Godot 3 → 4)

Godot 3 프로젝트를 포팅할 때 빠르게 참조할 수 있도록 이 표를 북마크해 두세요.

작업 Godot 3 Godot 4
연결 connect("sig", obj, "method") sig.connect(method)
연결 해제 disconnect("sig", obj, "method") sig.disconnect(method)
발생 emit_signal("sig", args) sig.emit(args)
연결 확인 is_connected("sig", obj, "method") sig.is_connected(method)
시그널 대기 yield(obj, "sig") await obj.sig
추가 인자 바인딩 connect("sig", obj, "method", [data]) sig.connect(method.bind(data))
일회성(One-shot) connect("sig", obj, "method", [], CONNECT_ONESHOT) sig.connect(method, CONNECT_ONE_SHOT)

10. 흔한 실수

1. 문자열 기반 connect (Godot 3 스타일) 사용하기

GDScript
# WRONG — Godot 3 syntax, will not compile
button.connect("pressed", self, "_on_button_pressed")

# CORRECT — Godot 4 syntax
button.pressed.connect(_on_button_pressed)

2. .emit() 대신 emit_signal() 사용하기

emit_signal()은 아직 동작하지만 컴파일 시점 검사를 우회하며 공식적으로 더 이상 권장되지 않습니다. .emit()을 사용하세요.

GDScript
# AVOID — deprecated, no compile-time checks
emit_signal("health_changed", health)

# PREFER — type-safe, catches typos at compile time
health_changed.emit(health)

3. 해제된 노드에 연결하기

시그널을 어떤 노드의 메서드에 연결했는데 그 노드가 queue_free()로 해제되면, 다음 시그널 발생 시 크래시가 발생합니다. 해결책:

  • _exit_tree()에서 연결 해제하기
  • 일회성 이벤트에는 CONNECT_ONE_SHOT 사용하기
  • 발생 전에 is_instance_valid(target)로 확인하기
GDScript
# Safe emission pattern
for connection in my_signal.get_connections():
    if is_instance_valid(connection["callable"].get_object()):
        pass  # Connection is still valid
my_signal.emit()  # Godot handles invalid connections gracefully in 4.x

4. 씬을 넘나드는 시그널 연결 해제를 잊기

Autoload 시그널에 대한 연결은 씬 전환을 넘어 유지됩니다. _ready()에서 연결했지만 절대 해제하지 않으면, 씬이 다시 로드될 때 오류나 예기치 않은 동작이 발생합니다.

GDScript
# WRONG — never disconnects, leaks across scene changes
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

# CORRECT — clean disconnect
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

func _exit_tree() -> void:
    EventBus.score_changed.disconnect(_on_score_changed)

5. 같은 시그널을 두 번 연결하기

_ready()가 여러 번 호출되면(예: 노드를 다시 부모에 붙이는 경우), 실수로 같은 메서드를 두 번 연결할 수 있습니다. 그러면 핸들러가 발생마다 두 번씩 실행됩니다.

GDScript
# Guard against double-connection
func _ready() -> void:
    if not EventBus.score_changed.is_connected(_on_score_changed):
        EventBus.score_changed.connect(_on_score_changed)

AI가 시그널 아키텍처를 관리하게 하고 싶으신가요?

Godot MCP Pro는 Claude 같은 AI 어시스턴트를 Godot 에디터에 직접 연결합니다. 프로젝트 전체에 걸쳐 시그널 흐름을 연결, 해제, 감사, 시각화하는 작업을 — 자동으로 수행할 수 있습니다.

analyze_signal_flow find_signal_connections connect_signal disconnect_signal get_signals
Godot MCP Pro 구매하기 — $15