분반 강의 세션

주소리·2026년 8월 12일

TIL

목록 보기
41/48

Unreal C++ 학습 정리 — 컴파일부터 리플렉션 시스템까지

언리얼 엔진을 공부하다 보면 컴파일, 빌드, 쿠킹, 패키징, 로드처럼 비슷해 보이는 용어를 자주 접하게 된다.

또 C++ 코드를 작성하다 보면 UCLASS(), GENERATED_BODY(), .generated.h처럼 일반 C++에서는 볼 수 없었던 코드도 등장한다.

이번에는 이 개념들이 각각 무엇을 의미하고, 언리얼의 리플렉션(Reflection) 시스템과 어떻게 연결되는지 정리해 보았다.


1. 컴파일, 쿠킹, 패키징, 로드는 서로 다르다

처음에는 모두 "내가 만든 것을 게임에서 사용할 수 있게 만드는 과정"처럼 보이지만 역할이 다르다.

컴파일(Compile)

컴파일은 작성한 코드나 블루프린트를 실행할 수 있는 형태로 변환하는 과정이다.

C++에서는 작성한 소스 코드를 컴퓨터가 실행할 수 있는 형태로 변환하는 과정이 필요하고, 블루프린트 역시 노드와 변수, 함수, 이벤트 그래프 등을 검사하고 실행 가능한 형태로 만드는 컴파일 과정이 존재한다.

쿠킹(Cook)

쿠킹은 코드보다는 게임 콘텐츠를 대상 플랫폼에서 사용할 수 있는 형태로 변환하는 과정에 가깝다.

예를 들면 다음과 같은 콘텐츠가 대상이 된다.

  • Texture
  • Mesh
  • Sound
  • Blueprint
  • Level

PC, 콘솔, 모바일 등 실제 게임이 실행될 플랫폼에 맞게 콘텐츠를 준비하는 과정이라고 이해할 수 있다.

패키징(Package)

패키징은 컴파일된 코드와 쿠킹된 콘텐츠를 실제 배포 가능한 형태로 묶는 과정이다.

언리얼의 전체 패키징 과정에서는 Build, Cook, Stage, Package, Deploy, Run 등의 단계가 사용된다.

따라서 단순하게 보면,

Compile → 코드를 실행할 수 있도록 준비
Cook → 콘텐츠를 플랫폼에 맞게 준비
Package → 준비된 것들을 배포 가능한 게임으로 묶기

정도로 구별할 수 있다.

로드(Load)

로드는 앞의 과정과 성격이 조금 다르다.

게임 실행 중 필요한 에셋이나 레벨 등의 데이터를 저장장치에서 메모리로 가져오는 과정이다.

예를 들어 SSD에 저장되어 있는 맵이나 텍스처가 필요하다면 게임이 이를 메모리로 가져와 사용할 수 있도록 해야 한다.

그런데 게임에 존재하는 모든 것을 한 번에 로드한다면 메모리와 로딩 시간 측면에서 비효율적이다.

그래서 실제 게임에서는 필요한 데이터를 어떻게, 언제 가져올 것인지가 중요하며 동기 로딩과 비동기 로딩 같은 개념도 등장한다.


2. Unreal C++의 빌드는 일반 C++과 조금 다르다

언리얼 C++을 공부하다 보면 다음과 같은 매크로를 계속 만나게 된다.

UCLASS()
USTRUCT()
UPROPERTY()
UFUNCTION()

이 코드는 단순히 C++ 컴파일러에게 넘겨서 처리하는 것으로 끝나지 않는다.

언리얼에는 UHT(Unreal Header Tool)UBT(Unreal Build Tool)가 있기 때문이다.

UHT와 UBT

대략적인 흐름을 단순화하면 다음과 같이 이해할 수 있다.

내가 작성한 Unreal C++ 코드
        ↓
UBT가 빌드 과정 관리
        ↓
UHT 실행
        ↓
UCLASS / UPROPERTY / UFUNCTION 등 분석
        ↓
UObject 시스템에 필요한 코드 생성
        ↓
C++ 컴파일
        ↓
실행 가능한 코드 생성

즉,

언리얼 C++은 일반적인 C++ 컴파일뿐 아니라 언리얼의 UObject 및 리플렉션 시스템을 지원하기 위한 코드 생성 과정이 추가된다.

이 점이 일반적인 C++ 프로젝트와 Unreal C++ 프로젝트를 이해할 때 중요한 차이 중 하나다.


3. 블루프린트도 컴파일한다

블루프린트는 노드를 연결하면 곧바로 실행되는 것처럼 보이지만 블루프린트 역시 컴파일 과정이 존재한다.

블루프린트 에디터에서 Compile 버튼을 누르면 엔진은 작성된 블루프린트의 변수, 함수, 이벤트 그래프 등의 내용을 검사하고 실행 가능한 형태로 준비한다.

다만 여기서 주의할 점이 있다.

"블루프린트는 컴파일하기 때문에 C++보다 느리다"라고 단순하게 결론 내리는 것은 적절하지 않다.

블루프린트와 C++의 런타임 성능 차이는 실행 방식과 구현 내용에 따라 달라질 수 있기 때문에, 실제 최적화에서는 어떤 로직을 얼마나 자주 실행하는지를 함께 확인해야 한다.


4. Material과 Shader도 별도의 컴파일 과정이 있다

언리얼에서 "컴파일"이라고 하면 C++만 생각하기 쉽지만 Material과 Shader에도 별도의 컴파일 과정이 존재한다.

예를 들어 필요한 Shader Map이 캐시에 존재하지 않는 머티리얼이 필요해지면 엔진에서 셰이더 컴파일 작업이 발생할 수 있다.

이러한 작업은 Shader Compile Worker를 통해 병렬로 처리될 수 있다.

중요한 것은 이것이 메시나 텍스처 자체를 로드하는 작업과는 다르다는 점이다.

셰이더 컴파일은 GPU가 사용할 셰이더 코드를 준비하는 과정으로 이해하면 된다.

TA를 공부하는 입장에서는 C++ 컴파일뿐만 아니라 셰이더 컴파일 역시 구별해서 알아둘 필요가 있다.


5. 리플렉션(Reflection)이란?

언리얼 C++을 공부하면서 상당히 중요한 개념이 리플렉션 시스템이다.

리플렉션을 간단하게 표현하면,

프로그램이 자신의 클래스, 변수, 함수 등에 관한 정보를 다룰 수 있도록 만드는 체계

라고 이해할 수 있다.

예를 들어 일반적인 C++ 클래스가 있다고 하자.

class PlayerData
{
public:
    int HP;
    float MoveSpeed;
};

사람이 이 코드를 읽으면 PlayerData 안에 HPMoveSpeed가 있다는 것을 쉽게 알 수 있다.

하지만 언리얼 엔진과 에디터가 이 정보를 자신들의 시스템에서 활용하려면 별도의 메타데이터가 필요하다.

예를 들어 엔진은 다음과 같은 작업을 해야 한다.

  • 특정 클래스가 어떤 클래스인지 파악
  • 프로퍼티 정보를 에디터에서 활용
  • C++ 기능을 블루프린트에 노출
  • C++ 클래스를 블루프린트와 연결
  • 직렬화 및 저장에 필요한 정보 활용
  • 네트워크 복제 시스템에서 프로퍼티 정보 활용
  • 런타임에서 타입 정보 확인

이러한 여러 엔진 기능의 기반이 되는 것이 언리얼의 리플렉션 시스템이다.

따라서 리플렉션을 단순히

"디테일 패널에 변수를 표시하는 기능"

이라고 이해하면 범위가 너무 좁다.

디테일 패널 노출은 리플렉션을 이용해서 구현되는 여러 기능 중 하나라고 보는 것이 더 정확하다.


6. #pragma once는 매크로가 아니다

언리얼 C++ 헤더를 열면 거의 항상 위쪽에 다음 코드가 있다.

#pragma once

처음 보면 UCLASS() 같은 언리얼 매크로와 비슷해 보일 수 있지만 #pragma once는 언리얼 리플렉션 매크로가 아니다.

이는 전처리 지시문이다.

역할은 간단하다.

하나의 헤더 파일이 같은 컴파일 단위에서 중복으로 포함되는 것을 방지한다.

따라서 다음은 구별해야 한다.

#pragma once
→ 전처리와 관련된 지시문

UCLASS()
UPROPERTY()
UFUNCTION()
→ Unreal Header Tool이 처리하는 언리얼 리플렉션 관련 매크로

7. *.generated.h는 왜 존재할까?

언리얼 C++ 헤더에서는 다음과 같은 코드를 볼 수 있다.

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

여기에서 특히 중요한 것이

#include "TestActor.generated.h"

이다.

이 파일은 내가 직접 작성하는 파일이 아니다.

UHT가 헤더를 분석하면서 언리얼의 리플렉션 및 UObject 시스템을 지원하기 위해 필요한 코드를 생성하며, 이 과정에서 만들어지는 코드와 연결되는 헤더다.

그리고 중요한 규칙이 하나 있다.

*.generated.h는 해당 헤더 파일의 include 목록에서 마지막에 위치해야 한다.

따라서 다음처럼 작성해야 한다.

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

그 아래에 다시 다른 헤더를 #include하는 방식은 피해야 한다.


8. UCLASS()는 무엇인가?

다음과 같은 Unreal C++ 코드를 살펴보자.

UCLASS()
class FRONTLINEFPS_API ATestActor : public AActor
{
    GENERATED_BODY()
};

여기서

UCLASS()

는 언리얼의 리플렉션과 관련된 매크로다.

UHT가 이 선언을 분석하고 해당 클래스가 언리얼의 UObject 시스템에서 필요한 처리를 받을 수 있도록 한다.

AActor 역시 UObject 계열의 클래스이므로 Unreal Object System과 연결되어 있다.

따라서

class ATestActor : public AActor

ATestActorAActor를 상속받는다.

라는 일반 C++의 상속 관계를 나타내고,

UCLASS()

는 이 클래스가 언리얼의 UObject 및 리플렉션 체계 안에서 처리될 수 있도록 필요한 정보를 제공하는 역할을 한다.

두 코드는 서로 다른 역할을 담당한다.


9. UCLASS() 안에는 옵션을 넣을 수 있다

UCLASS()의 괄호 안에는 클래스 지정자(Class Specifier)를 작성할 수 있다.

예를 들어,

UCLASS(Blueprintable)

처럼 사용할 수 있다.

Blueprintable

해당 클래스를 기반으로 블루프린트 클래스를 만들 수 있도록 지정하는 데 사용한다.

UCLASS(Blueprintable)
class AMyActor : public AActor
{
    GENERATED_BODY()
};

BlueprintType

해당 타입을 블루프린트에서 변수 타입 등으로 사용할 수 있도록 노출하는 데 관련된 지정자다.

UCLASS(BlueprintType)

BlueprintableBlueprintType은 이름이 비슷하지만 동일한 역할을 하는 옵션은 아니다.

따라서 실제 사용 시에는 해당 클래스의 부모 클래스와 지정자의 상속 여부까지 확인하는 것이 좋다.


10. MYPROJECT_API는 무엇인가?

Unreal C++ 클래스에서는 다음과 같은 형태도 볼 수 있다.

class MYPROJECT_API ATestActor : public AActor

여기서

MYPROJECT_API

모듈 API 지정자(Module API Specifier)다.

프로젝트 이름에 따라 이름이 달라질 수 있다.

언리얼 프로젝트는 하나의 거대한 코드 덩어리로만 구성되는 것이 아니라 여러 모듈로 나뉘어 빌드될 수 있다.

예를 들면 엔진에는 다음과 같은 모듈들이 있다.

Core
CoreUObject
Engine
UMG
GameplayAbilities
...

게임 프로젝트에도 자체 게임 모듈이 존재한다.

모듈 API 지정자는 클래스나 함수 등의 심볼을 다른 모듈에서 사용할 수 있도록 노출하는 것과 관련된 표시다.

따라서 이것을 public, private 같은 C++ 접근 지정자와 완전히 같은 것으로 보면 안 된다.

public/private클래스 내부에서 누가 멤버에 접근할 수 있는가를 제어한다면,

MYPROJECT_API와 같은 모듈 API 지정자는 모듈 또는 DLL 경계를 넘어 해당 심볼을 사용할 수 있도록 노출하는 것과 관련된다.


11. DLL이란?

DLL은 Dynamic Link Library의 약자다.

쉽게 표현하면 프로그램에서 사용할 코드와 기능을 별도의 라이브러리 파일로 만들어 둔 것이다.

언리얼의 모듈 시스템을 공부하다 보면 DLL과 모듈 API 지정자 개념이 함께 등장할 수 있다.

Unreal Project
│
├─ Module A
│   └─ Class A
│
├─ Module B
│   └─ Class B
│
└─ Module C

모듈이 분리되어 있다면 어떤 클래스나 함수를 다른 모듈에서 사용할 수 있도록 내보내야 하는 상황이 생길 수 있다.

이때 모듈 API 지정자가 중요한 역할을 한다.


전체 내용 정리

이번 학습에서 가장 중요했던 내용을 연결하면 다음과 같다.

[Unreal C++ 코드 작성]
        │
        ├── UCLASS
        ├── UPROPERTY
        ├── UFUNCTION
        │
        ↓
[UHT가 헤더 분석]
        ↓
[리플렉션/UObject용 코드 생성]
        ↓
[UBT가 빌드 과정 관리]
        ↓
[C++ 컴파일]
        ↓
[게임 코드]

그리고 게임 전체를 배포하는 관점에서는 다시,

코드 작성
   ↓
Build / Compile
   ↓
Cook
   ↓
Stage
   ↓
Package
   ↓
Deploy / Run

와 같은 더 큰 흐름으로 이어진다.

결국 UCLASS(), UPROPERTY(), generated.h 등을 각각 외우는 것보다 중요한 것은

"일반 C++ 코드를 언리얼 엔진이 이해하고 활용할 수 있도록 연결해 주는 별도의 시스템이 존재한다."

는 구조를 이해하는 것이다.

특히 리플렉션은 단순히 디테일 패널에 버튼이나 변수를 보여주기 위한 기능이 아니다.

C++로 작성한 클래스와 프로퍼티, 함수 등의 정보를 Unreal Engine의 에디터와 런타임 시스템에서 활용할 수 있도록 만드는 기반 체계라고 이해하는 편이 정확하다.


개인 학습 메모

이번 내용을 공부하면서 UCLASS, UPROPERTY, UFUNCTION 등을 단순히 "언리얼에서 반드시 붙여야 하는 매크로"로 외우기보다 왜 필요한지를 먼저 이해하는 것이 중요하다고 느꼈다.

앞으로 Unreal C++ 코드를 읽을 때는 다음과 같이 구분해서 살펴볼 생각이다.

  1. 일반 C++ 문법인가?
  2. 언리얼 리플렉션 시스템과 관련된 코드인가?
  3. 빌드 시스템과 관련된 코드인가?
  4. 모듈 경계를 처리하기 위한 코드인가?

이 네 가지를 구별할 수 있다면 처음 보는 Unreal C++ 헤더도 이전보다 구조적으로 읽을 수 있을 것 같다.

profile
게임개발 및 아트, TA

0개의 댓글