GDScript의 정적 타이핑(Static typing)

이 가이드에서 다음 내용을 배울 것입니다:

  • GDScript에서 타입을 사용하는 방법

  • 정적 타입은 버그를 방지하는 데 도움이 될 수 있습니다

이 새로운 언어 기능을 어디에서 어떻게 사용하는지는 전적으로 여러분에게 달려 있습니다. 일부 민감한 GDScript 파일에서만 사용하거나, 모든 곳에서 사용하거나, 항상 하던 것처럼 코드를 작성할 수 있습니다!

정적 타입은 변수, 상수, 함수, 매개 변수, 반환 타입에 사용될 수 있습니다.

참고

타입형 GDSsciprt(Typed GDScript)는 Godot 3.1부터 사용할 수 있습니다.

정적 타이핑에 대한 간단한 설명

타입형 GDScript를 사용하면 Godot는 코드를 작성할 때 더 많은 오류를 감지할 수 있습니다! 메서드를 호출할 때 인수 타입이 표시되기 때문에 작업하는 동안 여러분과 팀원에게 더 많은 정보를 제공합니다.

인벤토리 시스템을 프로그래밍하고 있다고 상상해보세요. Item 노드를 코딩한 다음 Inventory를 코딩합니다. 인벤토리에 항목을 추가하려면 코더들이 항상 Item타입만 Inventory.add 메서드에 전달해야 합니다. 타입을 사용하면 다음을 강제할 수 있습니다:

# In 'Item.gd'.
class_name Item
# In 'Inventory.gd'.
class_name Inventory


func add(reference: Item, amount: int = 1):
    var item = find_item(reference)
    if not item:
        item = _instance_item_from_db(reference)

    item.amount += amount

타입형 GDScript의 또 다른 중요한 장점은 새로운 경고 시스템입니다. 버전 3.1부터 Godot는 코드를 작성할 때 코드에 대한 경고를 제공합니다. 엔진은 실행 시간에 문제를 일으킬 수 있는 코드 섹션을 식별하지만 여러분이 코드를 그대로 둘지 여부를 결정할 수 있습니다. 잠시 후에 자세히 알아봅시다.

정적 타입은 더 나은 코드 완성(code completion) 옵션도 제공합니다. 아래에서 PlayerController라는 클래스에 대한 동적 및 정적 타입 코드 완성 옵션의 차이점을 볼 수 있습니다.

이전에 여러분은 아마도 변수에 노드를 저장하고, 변수이름 뒤에 점을 입력했으나 자동 완성 제안이 표시되지 않았던 경험이 있었을 것입니다:

code completion options for dynamic

이것은 동적 코드 때문입니다. Godot는 함수에 전달하는 노드 또는 값 타입을 알 수 없습니다. 그러나 타입을 명시적으로 작성하면 노드에서 모든 공용 메서드와 변수를 가져옵니다:

code completion options for typed

향후에 타입형 GDScript는 또한 코드의 성능도 개선할 것입니다. JIT(Just-In-Time) 컴파일과 다른 컴파일러 성능 향상이 이미 로드맵에 대기 중입니다!

전반적으로 타입형 프로그래밍은 보다 구조화된 경험을 제공합니다. 오류를 방지하고 스크립트의 자체 문서화 부분을 개선하는 데 도움이 됩니다. 이는 팀과 작업하거나 장기 프로젝트를 작업할 때 특히 유용합니다. 연구에 따르면 개발자는 대부분의 시간을 다른 사람의 코드나 과거에 작성하고 잊어버린 스크립트를 읽는 데 보낸다고 합니다. 코드가 더 명확하고 구조화될수록 더 빨리 이해할 수 있고 더 빨리 앞으로 나아갈 수 있습니다.

정적 타입 사용법

변수나 상수의 타입을 정의하려면 변수 이름 뒤에 콜론을 쓰고 그 뒤에 타입을 씁니다. 예) var health: int. 이렇게 하면 변수의 타입이 항상 동일하게 강제할 수 있습니다:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0

Godot은 여러분이 콜론을 입력했지만 타입을 생략한 경우 타입을 추론하기 위해서 노력합니다:

var life_points := 4
var damage := 10.5
var motion := Vector2()

현재 세 개의 타입을 사용할 수 있습니다:

  1. 내장 타입

  2. 핵심 클래스와 노드 (Object, Node, Area2D, Camera2D, 등등)

  3. Your own custom classes. Look at the new class_name feature to register types in the editor.

참고

상수의 경우 Godot가 할당된 값에서 자동으로 설정하기 때문에 타입 힌트를 작성할 필요가 없습니다. 그러나 여전히 코드의 의도를 더 명확하게 하기 위해 그렇게 할 수 있습니다.

커스텀 변수 타입

여러분이 만든 클래스를 포함한 모든 클래스를 타입으로 사용할 수 있습니다. 스크립트에서 사용할 수 있는 두 가지 방법이 있습니다. 첫 번째는 사용하려는 스크립트를 상수 타입으로 미리 불러오는 방법입니다:

const Rifle = preload("res://player/weapons/Rifle.gd")
var my_rifle: Rifle

두 번째는 생성할 때 class_name 키워드를 사용하는 방법입니다. 위의 예제에서 Rifle.gd는 다음과 같습니다:

extends Node2D
class_name Rifle

class_name을 사용하면 Godot는 Rifle 타입을 에디터에 전역적으로 등록하기 때문에 상수에 미리 불러올 필요 없이 어디서나 사용할 수 있습니다:

var my_rifle: Rifle

변수 캐스팅(casting)

타입 캐스팅(Type casting)은 타입 언어의 핵심 개념입니다. 캐스팅은 어떤 값의 타입을 다른 타입으로 변환하는 것입니다.

게임에 Area2D를 확장(extend)하는 적이 있다고 상상해보세요. 적이 PlayerController 스크립트가 붙은 플레이어 KinematicBody2D에 충돌하게 하고 싶습니다. on_body_entered 시그널을 사용하면 콜리전을 감지할 수 있습니다. 타입형 코드에서, 감지한 물체는 _on_body_entered 콜백의 PlayerController가 아니라 일반 PhysicsBody2D가 됩니다.

as 캐스팅 키워드로 PhysicsBody2D가 플레이어인지 확인할 수 있으며, 콜론 :을 다시 사용해서 변수가 이 타입을 사용하도록 강제할 수 있습니다. 이렇게 하면 변수가 PlayerController 타입에 고정됩니다:

func _on_body_entered(body: PhysicsBody2D) -> void:
    var player := body as PlayerController
    if not player:
        return

    player.damage()

커스텀 타입을 다룰 때 bodyPlayerController를 확장하지 않으면 player변수가 null로 설정됩니다. 이를 사용해 본체가 플레이어인지 확인할 수 있습니다. 우리는 이 캐스트 덕분에 플레이어 변수에 대한 완전한 자동 완성도 얻습니다.

참고

내장 타입으로 캐스트를 시도하다가 실패한다면 Godot는 오류를 내보낼 것입니다.

안전한 라인(safe line)

안전한 라인임을 보장하기 위해 캐스팅을 사용할 수도 있습니다. 안전한 라인은 코드의 모호한 줄이 타입 안전한지 알려주는 Godot 3.1의 새로운 도구입니다. 때때로 여러분이 타입형 코드와 동적 코드를 혼합하고 일치시킬 수 있기 때문에 Godot에는 명령어가 실행 시간에 오류를 유발하는지 여부를 알 수 있는 충분한 정보가 없습니다.

이는 자식 노드를 가져올 때 발생합니다. 타이머를 예로 들어 보겠습니다. 동적 코드를 사용하면 $Timer로 노드를 가져올 수 있습니다. GDScript는 duck-typing을 지원하므로 타이머가 Timer 유형인 경우에도 NodeObject의 두 개의 클래스를 확장합니다. 동적 GDScript를 사용하면 호출에 필요한 메서드가 있는 한 여러분은 노드 타입을 신경 쓰지 않아도 됩니다.

캐스팅을 사용하면 ($Timer as Timer), ($Player as KinematicBody2D) 등 노드를 가져올 때 예상되는 타입을 Godot에게 알릴 수 있습니다. Godot는 타입이 작동하는지 확인하고 만약 그렇다면 , 스크립트 에디터 왼쪽 줄 번호가 초록색으로 바뀝니다.

Unsafe vs Safe Line

안전하지 않은 라인(line 7) vs 안전한 라인(line 6 and 8)

참고

에디터 설정에서 안전한 라인을 끌 수 있고 혹은 색상을 바꿀 수도 있습니다.

-> 화살표로 함수의 반환 타입 정의하기

함수의 반환 타입을 정의하려면 함수 선언 뒤에 대시와 오른쪽 꺾쇠 괄호 ->를 쓰고 그 뒤에 반환 타입을 적으세요:

func _process(delta: float) -> void:
    pass

void 타입은 함수가 아무것도 반환하지 않는 것을 의미합니다. 변수와 마찬가지로 어떤 타입도 반환 타입으로 사용할 수 있습니다:

func hit(damage: float) -> bool:
    health_points -= damage
    return health_points <= 0

또한 여러분의 노드를 반환 타입으로 사용할 수 있습니다:

# Inventory.gd

# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
    var item: Item = find_item(reference)
    if not item:
        item = ItemDatabase.get_instance(reference)

    item.amount += amount
    return item

타입형 혹은 동적: 하나의 스타일을 고수하기

타입형 GDScript와 동적 GDScript는 같은 프로젝트에 공존할 수 있습니다. 하지만 코드베이스의 일관성과 동료들을 위해, 둘 중 하나의 스타일을 고수하는 것을 권장합니다. 같은 가이드라인을 따른다면 모두가 함께 일하기 더 쉬워지고, 다른 사람의 코드를 더 빠르게 읽고 이해하게 될 것입니다.

타입형 코드는 조금 더 작성해야 할 것이 많지만 위에서 말한 이득을 얻을 수 있습니다. 여기에 동일한 빈 스크립트 예제가 있습니다. 동적 스타일은 이렇습니다:

extends Node


func _ready():
    pass


func _process(delta):
    pass

그리고 정적 타입형 스타일은 이렇습니다:

extends Node


func _ready() -> void:
    pass


func _process(delta: float) -> void:
    pass

보시다시피, 엔진의 가상 메서드로 타입형 스타일을 사용할 수도 있습니다. 시그널 콜백도 다른 메서드처럼 타입형 스타일을 사용할 수 있습니다. 동적 스타일의 body_entered 시그널입니다:

func _on_Area2D_body_entered(body):
    pass

그리고 타입 힌트를 쓴 같은 콜백입니다:

func _on_area_entered(area: CollisionObject2D) -> void:
    pass

예를 들어 CollisionObject2D 타입을 여러분만의 자체 타입으로 바꾸어 자동으로 매개변수를 캐스팅하도록 할 수 있습니다:

func _on_area_entered(bullet: Bullet) -> void:
    if not bullet:
        return

    take_damage(bullet.damage)

bullet 변수는 어떤 CollisionObject2D 값이든 가질 수 있지만, 여기서는 이 변수가 우리의 프로젝트에서 만든 노드인 Bullet 임을 확실히 합니다. Area2DBullet 을 확장하지 않는 다른 노드인 경우, bullet 변수는 null 이 될 것입니다.

경고 시스템(Warning system)

참고

GDScript의 경고 시스템에 대한 관한 문서는 GDScript 내보내기(Export)로 옮겨졌습니다.

타입을 지정할 수 없는 경우

이 소개를 마무리하며, 타입형 힌트를 사용할 수 없는 몇몇 경우를 알아봅시다. 밑의 모든 예제는 오류를 유발합니다.

열거형(Enum)은 타입으로 사용할 수 없습니다:

enum MoveDirection {UP, DOWN, LEFT, RIGHT}
var current_direction: MoveDirection

배열에서 각 멤버의 타입을 지정할 수 없습니다. 그렇게 할 경우 오류를 유발합니다:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]

for 루프에서 타입의 할당을 강제할 수 없습니다, for 키워드 루프에서 각 요소는 이미 다른 타입을 갖고 있기 때문입니다. 따라서 아래와 같은 코드는 작성할 수 없습니다:

var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
    pass

두 개의 스크립트가 서로에게 의지하는 사이클을 만들 수 없습니다:

# Player.gd

extends Area2D
class_name Player


var rifle: Rifle
# Rifle.gd

extends Area2D
class_name Rifle


var player: Player

요약

타입형 GDScript는 강력한 툴입니다. Godot 3.1 버전부터 이용할 수 있으며, 일반적인 오류들을 방지하면서 더 구조화된 코드와 확장성 있는 체계를 만드는 것을 도와줍니다. 미래에는 정적 타입도 향후 컴파일러 최적화로 좋은 퍼포먼스 향상을 제공할 것입니다.