Sparta Unreal 부트캠프 83일차

정찬호·2026년 3월 31일

TIL - 코딩테스트 그리디 & 언리얼 플러그인 에디터 모듈 추가

코딩 테스트 — 프로그래머스 큰 수 만들기

실패한 접근

앞에서부터 돌면서 앞 숫자가 뒷 숫자보다 작고, 버리기 횟수가 목표보다 적다면 해당 숫자를 버리도록 구현했습니다.

#include 
#include 

using namespace std;

string solution(string number, int k) {
    string answer = "";
    int removeCnt = 0;
    
    for(int i = 0; i < number.length(); i++)
    {
        if(removeCnt < k)
        {
            if(i != number.length() - 1 && number[i + 1] > number[i])
            {
                removeCnt++;
                continue;
            }
        }
        answer += number[i];
    }
    
    return answer;
}

이 코드에는 두 가지 문제가 있었습니다.

  1. 여러 숫자를 한 번에 버리지 못함 — 뒤에 더 큰 숫자가 연속으로 이어져도, 바로 앞 숫자 하나만 비교하고 넘어가 버립니다.
  2. 버리기 횟수가 남은 상황에서 남은 수를 그냥 반환 — 자릿수가 어긋난 결과가 나옵니다.

입력값: "4177252841", k = 4
기댓값: "775841"
결과값: "4775841"


성공한 접근 — 스택 활용

#include 
#include 
#include 

using namespace std;

string solution(string number, int k) {
    string answer = "";
    int removeCnt = 0;
    
    stack numSt;
    
    for(int i = 0; i < number.length(); i++)
    {
        while(removeCnt < k)
        {
            if (!numSt.empty() && numSt.top() < number[i])
            {
                removeCnt++;
                numSt.pop();
            }
            else
            {
                break;
            }
        }
        
        numSt.push(number[i]);
    }
    
    vector numbers(numSt.size());
    for(int i = numbers.size() - 1; i >= 0; i--)
    {
        numbers[i] = numSt.top();
        numSt.pop();
    }
    
    while(removeCnt != k)
    {
        int minIdx = 0;
        char minChar = numbers[0];
        for(int i = 1; i < numbers.size(); i++)
        {
            if(numbers[i] < minChar)
            {
                minIdx = i;
                minChar = numbers[i];
            }
        }
        numbers.erase(numbers.begin() + minIdx);
        removeCnt++;
    }
    
    for(char ch : numbers)
    {
        answer += ch;
    }
    return answer;
}

핵심 아이디어

  • 스택을 사용해, 다음 숫자가 스택의 Top보다 크면 Top을 꺼내 버리는 연산을 while로 반복합니다. 이를 통해 연속된 숫자들도 한 번에 처리할 수 있습니다.
  • 순회가 끝난 후에도 버리기 횟수(removeCnt)가 남아 있다면, 남은 배열에서 가장 작은 수를 찾아 추가로 버힙니다. 이렇게 하면 자릿수 문제도 해결됩니다.

과제 10 — 언리얼 플러그인에 ToDoList 기능 추가하기

목표

MyNBCLog 플러그인 안에 ToDoList 기능을 추가합니다.

  • 에디터 패널 UI + 게임 런타임 UI 둘 다 지원
  • 데이터는 .uasset으로 저장

왜 모듈을 두 개로 나눠야 하는가?

기존 MySpartaLog 모듈은 Type이 Runtime이라 게임 실행 중에도 로드됩니다. 에디터 전용 기능(Slate UI, 에디터 메뉴 등)을 여기에 넣으면 게임 패키징 시 빌드가 깨집니다. 때문에 아래처럼 분리해야 합니다.

MyNBCLog 플러그인
├── MySpartaLog       (Type: Runtime)  → UAsset 데이터, 런타임 UI 위젯
└── MySpartaLogEditor (Type: Editor)   → 에디터 패널, 메뉴 등록

Phase 1 — 데이터 구조 (UAsset)

UToDoListAsset이라는 커스텀 에셋 클래스를 만들고, 에디터 콘텐츠 브라우저에서 생성할 수 있게 합니다.

ToDoListAsset.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ToDoListAsset.generated.h"

USTRUCT(BlueprintType)
struct FToDoItem
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="ToDo")
    FString Text;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="ToDo")
    bool bCompleted = false;
};

UCLASS(BlueprintType)
class MYSPARTALOG_API UToDoListAsset : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="ToDo")
    TArray Items;
};

ToDoListAsset.cpp

#include "ToDoListAsset.h"

UDataAsset을 상속받기 때문에 별도 팩토리 없이도 콘텐츠 브라우저에서 우클릭 → Miscellaneous → Data Asset으로 바로 생성할 수 있게 됩니다.


Phase 2 — 에디터 모듈 추가

Step 1. 파일 구조

Plugins/MyNBCLog/
└── Source/
    └── MySpartaLogEditor/
        ├── MySpartaLogEditor.Build.cs
        ├── Private/
        │   └── MySpartaLogEditor.cpp
        └── Public/
            └── MySpartaLogEditor.h

Step 2. 파일 내용 작성

MySpartaLogEditor.Build.cs

using UnrealBuildTool;

public class MySpartaLogEditor : ModuleRules
{
    public MySpartaLogEditor(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
        PublicIncludePaths.Add(ModuleDirectory);
        
        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core",
            "CoreUObject",
            "Engine",
            "UnrealEd",    // 에디터 기능 전반
            "Slate",       // Slate UI 위젯
            "EditorStyle", // 에디터 스타일/폰트/색상
            "ToolMenus",   // 에디터 메뉴 등록 (핵심)
            "MySpartaLog",
        });
    }
}

Public/MySpartaLogEditor.h

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"

class FMySpartaLogEditorModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
    
private:
    void RegisterMenus();
    void OpenToDoListTab();
    TSharedRef OnSpawnToDoTab(const FSpawnTabArgs& Args);
};

Private/MySpartaLogEditor.cpp

#include "MySpartaLogEditor.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
#include "Framework/Docking/TabManager.h"
#include "Widgets/Docking/SDockTab.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Views/SListView.h"
#include "ToDoListAsset.h"

static const FName ToDoTabName("ToDoListTab");

void FMySpartaLogEditorModule::StartupModule()
{
    FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
        ToDoTabName,
        FOnSpawnTab::CreateRaw(this, &FMySpartaLogEditorModule::OnSpawnToDoTab)
    )
    .SetDisplayName(FText::FromString("ToDo List"))
    .SetMenuType(ETabSpawnerMenuType::Hidden);

    UToolMenus::RegisterStartupCallback(
        FSimpleMulticastDelegate::FDelegate::CreateRaw(
            this, &FMySpartaLogEditorModule::RegisterMenus
        )
    );
}

void FMySpartaLogEditorModule::ShutdownModule()
{
    FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(ToDoTabName);
    UToolMenus::UnRegisterStartupCallback(this);
    UToolMenus::UnregisterOwner(this);
}

void FMySpartaLogEditorModule::RegisterMenus()
{
    FToolMenuOwnerScoped OwnerScoped(this);

    UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
    FToolMenuSection& Section = Menu->FindOrAddSection("MySpartaTools");
    Section.Label = FText::FromString("Sparta Tools");

    Section.AddMenuEntry(
        "OpenToDoList",
        FText::FromString("Open ToDo List"),
        FText::FromString("Opens the ToDo List panel"),
        FSlateIcon(),
        FUIAction(FExecuteAction::CreateRaw(
            this, &FMySpartaLogEditorModule::OpenToDoListTab
        ))
    );
}

void FMySpartaLogEditorModule::OpenToDoListTab()
{
    FGlobalTabmanager::Get()->TryInvokeTab(ToDoTabName);
}

TSharedRef FMySpartaLogEditorModule::OnSpawnToDoTab(const FSpawnTabArgs& Args)
{
    return SNew(SDockTab)
        .TabRole(ETabRole::NomadTab)
        [
            SNew(SVerticalBox)
            + SVerticalBox::Slot()
            .AutoHeight()
            .Padding(10.f)
            [
                SNew(STextBlock)
                .Text(FText::FromString("My ToDo List"))
            ]
            + SVerticalBox::Slot()
            .AutoHeight()
            .Padding(10.f)
            [
                SNew(SButton)
                .Text(FText::FromString("Add Item (WIP)"))
            ]
        ];
}

IMPLEMENT_MODULE(FMySpartaLogEditorModule, MySpartaLogEditor);

각 함수 분석

1. 탭 ID 등록

static const FName ToDoTabName("ToDoListTab");

탭은 언리얼 에디터 안에서 이름으로 구분됩니다. FName은 언리얼의 가볍고 빠른 문자열 타입으로, 탭 ID처럼 식별자 용도로 많이 사용됩니다. static으로 선언한 이유는 이 파일 안에서만 쓰는 상수이기 때문입니다.


2. StartupModule — 탭 스포너 등록

FGlobalTabManager는 언리얼 에디터의 모든 탭을 관리하는 매니저입니다. 위 코드는 "ToDoListTab이라는 ID의 탭이 열릴 때, OnSpawnToDoTab 함수를 호출해서 내용을 만들어라" 는 의미입니다.

  • NomadTab: 도킹 가능한 독립 탭. 에디터 어디에든 붙이거나 떼어낼 수 있습니다.
  • CreateRaw: 일반 C++ 객체의 멤버 함수를 델리게이트에 바인딩할 때 사용합니다. IModuleInterfaceUObject가 아니기 때문에 CreateRaw를 써야 합니다. UObject였다면 CreateUObject를 사용해야 합니다.

3. StartupModule — 메뉴 등록 콜백

StartupModule에서 바로 등록하지 않고 콜백으로 예약하는 이유는, StartupModule이 호출되는 시점에 에디터 메뉴 시스템(ToolMenus)이 아직 준비되지 않았을 수 있기 때문입니다. "에디터가 완전히 준비되면 그때 RegisterMenus를 호출해줘" 라고 예약해두는 겁니다.


4. ShutdownModule — 정리

FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(ToDoTabName);
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);

플러그인이 언로드될 때 등록했던 것들을 전부 되돌리는 작업입니다. 이 작업을 해주지 않으면 플러그인을 껐다 키거나 에디터를 종료할 때 크래시가 날 수 있습니다. 등록/해제는 항상 쌍으로 작성하는 습관이 중요합니다.


5. RegisterMenus — 메뉴 항목 추가

  • FToolMenuOwnerScoped OwnerScoped(this) : 이 모듈이 추가하는 메뉴 항목들의 소유권을 this로 설정합니다. ShutdownModule에서 UnregisterOwner(this)를 호출하면 한 번에 전부 제거됩니다.
  • ExtendMenu("LevelEditor.MainMenu.Tools") : 에디터 상단 Tools 메뉴를 이름으로 가져옵니다.
  • FindOrAddSection : 메뉴 안에 섹션(구분선으로 나뉜 그룹)을 추가하거나 기존 것을 가져옵니다.

6. OnSpawnToDoTab — Slate UI 구조

SDockTab            ← 탭 창 자체
  └─ SVerticalBox   ← 세로로 쌓는 레이아웃
       ├─ STextBlock ← 텍스트 표시
       └─ SButton    ← 버튼

SNew(위젯타입)으로 위젯을 생성하고, [ ] 안에 자식 위젯을 넣는 구조입니다. + SVerticalBox::Slot()은 새 슬롯(칸)을 추가해 세로로 쌓습니다.


전체 흐름 요약

StartupModule 호출
  │
  ├─ 탭 스포너 등록 (탭이 열릴 때 쓸 함수 예약)
  │
  └─ 메뉴 콜백 등록 (에디터 준비되면 RegisterMenus 호출 예약)
                          │
                          └─ RegisterMenus 실행
                               │
                               └─ Tools 메뉴에 "Open ToDo List" 항목 추가
                                        │
                                        └─ 클릭 시 OpenToDoListTab 호출
                                                  │
                                                  └─ OnSpawnToDoTab으로 탭 UI 생성

Step 3. .uplugin 수정

{
  "FileVersion": 3,
  "Version": 1,
  "VersionName": "1.0",
  "FriendlyName": "My NBC Log Plugin",
  "Description": "과제 10 교안용 로그 플러그인",
  "Category": "Sparta",
  "CreatedBy": "Eunil",
  "CanContainContent": true,
  "Modules": [
    {
      "Name": "MySpartaLog",
      "Type": "Runtime",
      "LoadingPhase": "Default"
    },
    {
      "Name": "MySpartaLogEditor",
      "Type": "Editor",
      "LoadingPhase": "PostEngineInit"
    }
  ]
}

Phase 2 진행 중 오류 발생

다음과 같은 패턴의 링크 오류가 발생했습니다.

LNK2019: 확인할 수 없는 외부 기호
"FCurveSequence", "SVerticalBox", "SBoxPanel", "STextBlock",
"SButton", "FSlateIcon", "EVisibility" ...

컴파일 오류 vs 링크 오류의 차이

오류 종류원인해결 위치
컴파일 오류헤더를 못 찾음#include 문제
링크 오류헤더는 찾았지만 실제 구현(.dll/.lib)을 못 찾음Build.cs 의존성 문제

분석된 원인

UE 5.4 이후 Slate 위젯들의 구현체가 여러 모듈에 분산되었고, SDockTab, FGlobalTabmanager 등 에디터 탭 관련 심볼들은 EditorFramework 모듈에 별도로 존재합니다.

Slate           → 위젯 선언부 (헤더/인터페이스)
SlateCore       → Slate 핵심 타입
AppFramework    → 위젯 구현체 상당수
ApplicationCore → EVisibility, FSlateIcon 같은 기반 타입
EditorFramework → SDockTab, TabManager 등 에디터 전용 탭 시스템

시도한 해결 방법

Build.cs에 누락된 모듈들을 추가했습니다.

PublicDependencyModuleNames.AddRange(new string[]
{
    "Core", "CoreUObject", "Engine",
    "UnrealEd", "Slate", "SlateCore",
    "EditorStyle", "ToolMenus", "MySpartaLog",
    "EditorFramework",  // SDockTab, FGlobalTabmanager
    "AppFramework",     // SWidget 구현체 일부
    "ApplicationCore",  // EVisibility, FSlateIcon 등
});

언리얼 엔진은 모듈이 매우 잘게 쪼개져 있고, "필요한 것만 링크" 하도록 설계되어 있습니다. 때문에 사용하는 심볼이 어느 모듈에 속하는지 파악하고 Build.cs에 명시해야 합니다.


Phase 3 — 런타임 UI (예정)

게임 플레이 중 UMG 위젯으로 ToDoList를 화면에 표시한다. 내일 이어서 진행할 예정입니다.

현재 구현 결과

Tool에 추가된 OpenToDoList

텅 빈 ToDoList 탭

profile
게임 개발 지망생입니다.

0개의 댓글