1. 핵심 개념
- GC 로컬 이슈
- 프로젝트 후기
2. 상세 내용
2.1. GC 참조 해제 이슈
void UUIPlayer::NativeConstruct()
{
Super::NativeConstruct();
auto UIInventoryPresenter = NewObject<UUIInventoryPresenter>(this);
if (UIInventoryPresenter && PlayerChar)
{
UIInventoryPresenter->Bind(UIInventory, PlayerChar);
}
// ....
체력바나 경험치바, 인벤토리 같은 플레이어 UI를 다루는 UIPlayer 클래스에서 내부적으로 뷰와 모델을 바인딩하는 코드를 작성했다.
이 과정에서 바인딩을할 때 NewObject로 직접 바인딩을 담당하는 presenter 클래스를 지역 변수로 선언해서 GC가 참조를 해제하는 일이 발생했다.
GC가 메모리(바인딩)을 해제하는 타이밍이 실제 게임에서는 인벤토리에 어떤 때에는 아이템이 잘 입수되고 어떤 때에는 아이템이 전혀 입수되지 않아서 디버깅이 상당히 어려웠다.
// h 파일
// ...
class VAMPIRESURVIVAL_API UUIPlayer : public UUIBase
{
protected:
// 변수 위에 UPROPERTY()를 달아서 참조 중에 GC에게 해제 되지 않도록 함
UPROPERTY()
TObjectPtr<class UUIInventoryPresenter> InventoryPresenter;
// ...
}
// cpp 파일
void UUIPlayer::NativeConstruct()
{
Super::NativeConstruct();
// 캐싱해서 사용
PlayerPresenter = NewObject<UUIPlayerPresenter>(this);
// ...
// 사용한 바인드 코드
void UUIInventoryPresenter::Bind(UUIBase* InUI, UObject* InModel)
{
InventoryView = Cast<UUIInventory>(InUI);
InventoryModel = Cast<APlayerCharacter>(InModel);
if (!InventoryModel || !InventoryView) return;
UEquipmentComponent* InvComp = InventoryModel->GetEquipmentComponent();
if (!InvComp) return;
// 초기 데이터 반영
RefreshInventoryUI();
// 델리게이트 바인딩 (중복 방지)
InvComp->OnOwnedEquipChanged.RemoveAll(this);
InvComp->OnOwnedEquipChanged.AddDynamic(this, &UUIInventoryPresenter::OnEquipLevelChanged);
}
위와 같이 바인드할 때 사용한 프레젠터 클래스를 UPROPERTY()로 선언해서 관리하니 GC에 의해 바인딩이 풀리지 않아서 인벤토리에 잘 반영되었다.
3. 질문 및 해결 (Q&A)
UI 작업 중 아쉬웠던 점
작업
- 풀리퀘에서 revert를 만들어서 브랜치가 불필요하게 많아졌다.
- 깃에서 작업할 때 커밋을 제때 하지 않고 모아서 해서 pull 오류를 상당히 자주 겪었다.
- UI 특성상 블루프린트와 작업을 병행하는 일이 잦은데 블루프린트에서 바인딩을 하드 코딩해서 BattleMapUI에서 UIPlayer을 호출하는 코드를 찾는데 어려움을 겪었다.
- CommonUI에서 포커싱이 어떻게 인풋시스템과 동작하는지 이해하지 못해서 플레이어 조작을 UI 레이어에서 인터셉트하는데 어려움을 겪었다.
- 언리얼에서는 웹 기준에서의 패딩, margin을 활용한 구조적 디자인이 필요한데 경험이 아예 전무해서 어려움을 겪었다. 따로 공부할 필요가 있어보인다.
- HUD의 생성 타이밍에서 Gamemode와 바인딩을 하는데 액터의 라이프 사이클에 의존해서 동기적인 초기화가 불가능했다. 그래서 팀원에게 불필요한 델리게이트를 만들게 요구했다.
- 로컬에서 받은 플러그인을 perforce에 서밋했을 때 적용이 안되어서 빌드 에러가 발생하는 리비전을 만들었다.
- 월드좌표와 스크린 좌표 간 변화를 제대로 공부하지 못하고 AI 받아서 사용해서 바이브 코딩과 다를바가 없었다.
- 로컬에서 포인터 선언을 하면 미사용 시 GC가 수거하는걸 고려못해서 팀원이 UI 코드를 디버깅하는 일이 있었다(책임분리실패)
- 클래스가 너무 많고 구조가 꽤 복잡해서 스스로 필요한 코드를 찾아가는게 어려웠다.(어느정도 문서화가 필요)
- 설계한 UI 요소가 실제 화면에서 어떻게 반영되는지 제대로 이해하지 못했다.
설계
- 직접 MVP 패턴을 구현하는 것과 MVVM 패턴을 도입하는 것이 소규모 인디 프로젝트에서 어느쪽이 더 실용적인지 충분한 판단을 내리지 못하고 설계를 들어갔다.
- UIManager에 큐잉, 인덱싱 기반 UI 등장으로 확장하지 못했다.
- UIManager를 순수한 View 클래스와 함께 인터페이스를 두고 모듈로 분리(컴파일 이점, 확장성)하는 과정을 공부해보면 좋을 것 같다.
- UI를 cpp로 뼈대를 설계하고 블루프린트에서 작업하는 식으로 진행해야 했는데 작업이 미숙해서 이 둘을 구분하는 명확한 기준을 잡지 못했다.
- UI 특성상 대부분의 코드가 블루프린트에서의 호출을 염두해야 하는데 경로를 열지 못한 경우가 많았다.
- UIManager가 자동 바인딩을 해주기는 하지만 바인딩 맵을 결국 데이터 에셋으로 수기 등록하는 방식이여서 의미가 퇴색되었다.
- 프리로드하지 않는 UI는 UIOrchestraManager로 이벤트 생성을 담당했는데 설계시간이 부족해서 UIManager와 연계가 잘안되었다.
- UI 내부적으로 CloseUI가 있는데 이것을 호출하면 UIManager 내부에 존재하는 PopWidget에서의 바인드 해제가 동작하지 않아서 런타임 에러가 발생할 수 있다.
- UI 전용 Pooling을 위한 매니저가 2개 있는데 둘이 거의 구조적으로 동일하지만 추상화하지 못했다.
- GameplayTag로 이벤트버스에서 UI를 특정하는데 내부적으로 FTEXT로 수기 입력하는 과정이 있어서 휴먼 에러를 예방할 조치가 필요했다.
- 동적으로 로드하는 리소스 파일을 파일 경로로 하드 코딩하였다.
- MVP 패턴의 강점인 순수한 View를 활용한 Mock 객체 주입으로 디버깅을 하는 응용을 해보지 못했다.
4. 관련 문서 (Links)
'내일배움캠프 > TIL' 카테고리의 다른 글
| TIL260526 - CPP (0) | 2026.05.26 |
|---|---|
| TIL260522 - Unreal (0) | 2026.05.22 |
| TIL260518 - VFX (0) | 2026.05.18 |
| TIL260515 - Unreal (0) | 2026.05.15 |
| TIL260514 - perforce (0) | 2026.05.14 |