본문으로 건너뛰기

안동민 개발노트

본문 시작

파일 입출력 및 JSON 처리

IPlatformFile과 FFileHelper로 파일을 다루고 JSON 객체를 C++에서 파싱하고 생성하는 방법을 익힙니다.

이전 절에서 우리는 언리얼 엔진의 SaveGame 시스템을 통해 게임 데이터를 쉽고 편리하게 저장하고 불러오는 방법을 알아보았습니다.

SaveGame 시스템은 대부분의 게임 저장 요구 사항을 충족하지만, 때로는 게임 데이터가 아닌 다른 종류의 파일(예: 사용자 로그, 커스텀 설정 파일, 외부 데이터)을 직접 다루거나, 특정 형식(JSON, XML 등)으로 데이터를 저장해야 할 필요가 생깁니다.

이번 절에서는 언리얼 엔진에서 제공하는 기본적인 파일 입출력(File I/O) 기능과 함께, 웹 서비스 통신이나 설정 파일에 널리 사용되는 JSON(JavaScript Object Notation) 데이터 형식을 C++에서 처리하는 방법에 대해 알아보겠습니다.

SaveGame 밖의 데이터는 목적에 맞는 통로를 선택한다

사용자 로그, 커스텀 설정, 외부 데이터, JSON/XML 같은 교환 형식은 SaveGame만으로 묶기보다 파일 접근 계층과 데이터 형식을 분리해 읽습니다.

  1. FFileHelper

    텍스트 파일 `SaveStringToFile`과 `LoadFileToString`으로 프로젝트 Saved 경로의 문자열 파일을 빠르게 저장하고 불러옵니다.

  2. IPlatformFile

    바이트 제어 파일 핸들을 직접 열어 바이트 배열을 쓰거나 읽습니다. 부분 접근과 더 정교한 스트림 제어가 필요할 때 사용합니다.

  3. Json / JsonUtilities

    구조화 데이터 웹 API, 설정 파일, 로컬 캐시처럼 필드와 배열이 있는 데이터는 JSON 파싱과 직렬화로 C++ 구조와 연결합니다.

  4. 경로와 인코딩을 먼저 고정

    FPaths `ProjectSavedDir`, `ProjectContentDir`, `ProjectDir`로 플랫폼별 경로 차이를 직접 다루지 않습니다. ForceUTF8 설정 파일이나 로그가 한글을 포함한다면 저장 인코딩을 명시해 환경별 깨짐을 줄입니다.

  5. 언리얼 내부 게임 데이터는 별도 판단

    Data Table 아이템, 스킬, 밸런스처럼 행 단위로 늘어나는 값은 표 구조가 디자이너 수정에 유리합니다. Primary Asset 개별 에셋 참조와 비동기 로딩이 중요하면 `UPrimaryDataAsset`을 검토합니다.


언리얼 엔진의 파일 입출력

언리얼 엔진은 플랫폼 독립적인 파일 입출력을 위해 여러 유틸리티 클래스를 제공합니다.

주로 IPlatformFile 인터페이스와 FFileHelper 클래스가 사용됩니다.

IPlatformFile

IPlatformFile은 저수준의 파일 시스템 접근을 위한 인터페이스입니다.

파일을 열고, 읽고, 쓰고, 닫는 등의 기본적인 파일 작업을 수행할 수 있습니다.

이는 특정 플랫폼의 파일 시스템 API를 추상화하여 개발자가 플랫폼별 코드를 작성할 필요 없게 합니다.

IPlatformFile 사용 예제
#include "HAL/PlatformFilemanager.h" // IPlatformFile을 위해 포함
#include "Misc/FileHelper.h"        // FFileHelper를 위해 포함
#include "Misc/Paths.h"             // FPaths를 위해 포함

void MyCustomFileWriter()
{
    // 1. 파일 경로 설정
    // FPaths::ProjectSavedDir() : 프로젝트의 Saved 디렉토리 (C:\Users\[User]\AppData\Local\[ProjectName]\Saved\)
    // FPaths::ProjectContentDir() : 프로젝트의 Content 디렉토리
    // FPaths::ProjectDir() : 프로젝트의 루트 디렉토리
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("MyCustomData.txt");

    // 2. 파일 쓰기 (텍스트)
    FString ContentToWrite = TEXT("Hello, Unreal Engine File I/O!");
    // FFileHelper::SaveStringToFile: 가장 쉬운 방법으로 문자열을 파일에 저장
    if (FFileHelper::SaveStringToFile(ContentToWrite, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully wrote to file: %s"), *FilePath);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to write to file: %s"), *FilePath);
    }

    // 3. 파일 읽기 (텍스트)
    FString LoadedContent;
    // FFileHelper::LoadFileToString: 파일에서 문자열을 읽어옴
    if (FFileHelper::LoadFileToString(LoadedContent, *FilePath))
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully read from file: %s"), *LoadedContent);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to read from file: %s"), *FilePath);
    }

    // 4. (고급) IPlatformFile을 직접 사용하여 파일 스트림 제어
    IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

    // 파일 생성 및 쓰기 (바이트 단위)
    TArray<uint8> BytesToWrite;
    FString BinaryContent = TEXT("Binary Data Example");
    BytesToWrite.SetNum(BinaryContent.Len() * sizeof(TCHAR)); // TCHAR 크기에 맞게 배열 크기 설정
    FMemory::Memcpy(BytesToWrite.GetData(), *BinaryContent, BytesToWrite.Num());

    IFileHandle* WriteHandle = PlatformFile.OpenWrite(*FilePath + TEXT(".bin"));
    if (WriteHandle)
    {
        WriteHandle->Write(BytesToWrite.GetData(), BytesToWrite.Num());
        delete WriteHandle; // 핸들 닫기
        UE_LOG(LogTemp, Warning, TEXT("Successfully wrote binary data to: %s"), *(FilePath + TEXT(".bin")));
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to open binary file for writing: %s"), *(FilePath + TEXT(".bin")));
    }

    // 파일 읽기 (바이트 단위)
    TArray<uint8> BytesRead;
    IFileHandle* ReadHandle = PlatformFile.OpenRead(*FilePath + TEXT(".bin"));
    if (ReadHandle)
    {
        BytesRead.SetNum(ReadHandle->Size()); // 파일 크기만큼 배열 설정
        ReadHandle->Read(BytesRead.GetData(), BytesRead.Num());
        delete ReadHandle; // 핸들 닫기

        FString ReadBinaryContent;
        ReadBinaryContent.Append((TCHAR*)BytesRead.GetData(), BytesRead.Num() / sizeof(TCHAR)); // TCHAR 개수만큼
        UE_LOG(LogTemp, Warning, TEXT("Successfully read binary data: %s"), *ReadBinaryContent);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to open binary file for reading: %s"), *(FilePath + TEXT(".bin")));
    }
}
  • FFileHelper: FFileHelperIPlatformFile 위에 구축된 고수준 유틸리티 클래스로, 문자열이나 바이트 배열을 파일로 저장하거나 파일에서 로드하는 것을 매우 간단하게 만듭니다. 대부분의 간단한 파일 입출력 시나리오에서는 FFileHelper를 사용하는 것이 좋습니다.
  • FPaths: 파일 경로를 구성할 때 프로젝트의 표준 디렉토리 경로를 얻는 데 사용됩니다. 이는 플랫폼 간 호환성을 보장합니다.
  • IPlatformFile 직접 사용: 더 정교한 제어(예: 파일 포인터 이동, 부분 읽기/쓰기, 비동기 I/O)가 필요할 때 IPlatformFile을 직접 사용할 수 있습니다. 그러나 이는 복잡하며, 대부분의 경우 FFileHelper로 충분합니다.

JSON (JavaScript Object Notation) 처리

파일에서 UStruct까지 데이터 경로

런타임 저장 데이터는 파일 시스템 접근, 문자열 로딩, JSON 파싱, UStruct 변환, 검증 단계를 거쳐 게임 로직에 들어간다.

  1. 1
    경로 결정

    Saved, Config, Content 중 런타임 쓰기 가능 위치를 먼저 고른다. 경로

  2. 2
    파일 읽기

    IPlatformFile 또는 FileHelper로 문자열을 가져온다. load

  3. 3
    JSON 파싱

    Reader와 Serializer로 JsonObject를 만들고 실패 여부를 확인한다. parse

  4. 4
    UStruct 변환

    필드 이름과 타입을 맞춰 구조체나 UObject 데이터로 옮긴다. 지도

  5. 5
    검증 후 사용

    필수 필드, 범위, 기본값을 확인하고 게임 시스템에 전달한다. validate

if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
    return false;
}

JSON은 데이터를 구조화하여 표현하는 경량의 데이터 교환 형식입니다.

인간이 읽고 쓰기 쉬우며, 기계가 파싱하고 생성하기도 용이하여 웹 서비스 API, 설정 파일 등에 널리 사용됩니다.

언리얼 엔진은 Json 모듈을 통해 JSON 데이터를 C++에서 파싱하고 생성할 수 있는 기능을 제공합니다.

JSON 모듈 활성화

프로젝트의 .Build.cs 파일에 JsonJsonUtilities 모듈을 추가해야 합니다.

MyProject.Build.cs
// ...
PublicDependencyModuleNames.AddRange(
    new string[] {
        "Core",
        "CoreUObject",
        "Engine",
        "InputCore",
        "Json",         // JSON 모듈 추가
        "JsonUtilities" // JSON 유틸리티 모듈 추가
        // ...
    });
// ...

JSON 데이터 읽기 (파싱)

JSON 문자열을 파싱하여 C++에서 사용할 수 있는 데이터 구조로 변환합니다.

JSON 데이터 읽기 예제
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

void ParseJsonData()
{
    FString JsonString = TEXT(R"({"name": "Unreal Guy", "health": 100, "inventory": ["Sword", "Shield"], "stats": {"strength": 10, "dexterity": 8}})");

    // 1. JSON 리더 생성
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);

    // 2. JSON 객체 파싱
    TSharedPtr<FJsonObject> JsonObject;
    if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Successfully parsed JSON."));

        // 3. 데이터 접근
        // 문자열 가져오기
        FString Name;
        if (JsonObject->TryGetStringField(TEXT("name"), Name))
        {
            UE_LOG(LogTemp, Warning, TEXT("Name: %s"), *Name);
        }

        // 숫자 가져오기 (정수 또는 부동소수점)
        double Health;
        if (JsonObject->TryGetNumberField(TEXT("health"), Health))
        {
            UE_LOG(LogTemp, Warning, TEXT("Health: %f"), Health);
        }

        // 배열 가져오기
        TArray<TSharedPtr<FJsonValue>> InventoryArray;
        if (JsonObject->TryGetArrayField(TEXT("inventory"), InventoryArray))
        {
            UE_LOG(LogTemp, Warning, TEXT("Inventory Items:"));
            for (TSharedPtr<FJsonValue> ItemValue : InventoryArray)
            {
                UE_LOG(LogTemp, Warning, TEXT("- %s"), *ItemValue->AsString());
            }
        }

        // 중첩된 JSON 객체 가져오기
        TSharedPtr<FJsonObject> StatsObject = JsonObject->GetObjectField(TEXT("stats"));
        if (StatsObject.IsValid())
        {
            double Strength;
            if (StatsObject->TryGetNumberField(TEXT("strength"), Strength))
            {
                UE_LOG(LogTemp, Warning, TEXT("Stats - Strength: %f"), Strength);
            }
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to parse JSON."));
    }
}
  • TJsonReaderFactory: FString로부터 TJsonReader를 생성합니다.
  • FJsonSerializer::Deserialize: TJsonReaderTSharedPtr<FJsonObject>를 사용하여 JSON 문자열을 실제 FJsonObject로 파싱합니다.
  • FJsonObject: JSON 객체({})를 나타내는 핵심 클래스입니다. TryGetStringField, TryGetNumberField, TryGetArrayField, GetObjectField 등의 함수를 사용하여 필드에 접근합니다.

JSON 데이터 쓰기 (생성)

C++ 데이터를 JSON 문자열로 변환합니다.

JSON 데이터 쓰기 예제
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"

void CreateJsonData()
{
    // 1. FJsonObject 생성
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());

    // 2. 필드 추가
    JsonObject->SetStringField(TEXT("gameTitle"), TEXT("My Awesome Game"));
    JsonObject->SetNumberField(TEXT("version"), 1.0);
    JsonObject->SetBoolField(TEXT("isDebugMode"), true);

    // 배열 필드 추가
    TArray<TSharedPtr<FJsonValue>> LevelNamesArray;
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_01"))));
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_02"))));
    LevelNamesArray.Add(MakeShareable(new FJsonValueString(TEXT("Level_Boss"))));
    JsonObject->SetArrayField(TEXT("levels"), LevelNamesArray);

    // 중첩된 JSON 객체 추가
    TSharedPtr<FJsonObject> SettingsObject = MakeShareable(new FJsonObject());
    SettingsObject->SetNumberField(TEXT("volume"), 0.7);
    SettingsObject->SetStringField(TEXT("resolution"), TEXT("1920x1080"));
    JsonObject->SetObjectField(TEXT("gameSettings"), SettingsObject);

    // 3. JSON 문자열로 직렬화
    FString OutputString;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
    if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer, true)) // true는 Pretty Print (들여쓰기)
    {
        UE_LOG(LogTemp, Warning, TEXT("Generated JSON:\n%s"), *OutputString);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to serialize JSON."));
    }

    // 4. 생성된 JSON 문자열을 파일로 저장 (FFileHelper 사용)
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfig.json");
    if (FFileHelper::SaveStringToFile(OutputString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
        UE_LOG(LogTemp, Warning, TEXT("JSON saved to: %s"), *FilePath);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to save JSON file."));
    }
}
  • MakeShareable(new FJsonObject()): TSharedPtr를 사용하여 FJsonObject를 생성합니다. 언리얼 엔진은 스마트 포인터(TSharedPtr, TSharedRef, TWeakPtr)를 사용하여 메모리 관리를 효율적으로 수행합니다.
  • SetStringField, SetNumberField, SetBoolField, SetArrayField, SetObjectField: FJsonObject에 다양한 타입의 필드를 추가합니다.
  • FJsonSerializer::Serialize: FJsonObjectFString으로 직렬화합니다. 세 번째 인자를 true로 설정하면 가독성을 위해 들여쓰기가 적용됩니다.
JSON은 모듈 추가, 읽기, 필드 접근, 쓰기, 실패 처리로 연결한다

Deserialize와 Serialize가 성공해도 필드 누락과 파일 저장 실패는 별도 분기로 남겨야 한다.

구간사용 API확인할 것
Module.Build.cs에 Json 모듈을 추가한다FJsonObject와 reader/writer를 사용할 수 있다
ReadTJsonReaderFactory와 Deserialize로 문자열을 객체로 바꾼다결과 IsValid와 실패 로그
FieldTryGetStringField, 배열 접근 등 타입별 getter를 쓴다필드 누락과 타입 불일치 분기
WriteSetStringField, SetArrayField 후 Serialize와 SaveStringToFile로 저장한다저장 반환값과 경로 오류

UStruct를 JSON으로 직렬화/역직렬화

JsonUtilities 모듈은 UStruct와 JSON 간의 변환을 더욱 편리하게 해주는 헬퍼 함수를 제공합니다.

이는 복잡한 데이터를 UStruct로 정의하고 이를 JSON 파일로 저장하거나 불러올 때 매우 유용합니다.

UStruct를 JSON으로 직렬화/역직렬화 예제
#include "Serialization/Json/JsonArchiveOutputFormat.h" // 추가 필요
#include "Serialization/Json/JsonArchiveInputFormat.h"  // 추가 필요
#include "Serialization/Json/JsonSerializer.h"          // 추가 필요
#include "UObject/TextProperty.h" // FText 처리를 위해 필요

// USTRUCT로 정의하여 JSON으로 직렬화할 데이터 구조
USTRUCT(BlueprintType)
struct FGameConfigData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    FString LastPlayedPlayerName;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    float MasterVolume;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    int32 MaxFps;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
    TArray<FString> EnabledFeatures;

    FGameConfigData()
        : LastPlayedPlayerName(TEXT("Default")), MasterVolume(0.8f), MaxFps(60)
    {}
};

void SerializeUStructToJson()
{
    FGameConfigData ConfigData;
    ConfigData.LastPlayedPlayerName = TEXT("AwesomeGamer");
    ConfigData.MasterVolume = 0.65f;
    ConfigData.MaxFps = 120;
    ConfigData.EnabledFeatures.Add(TEXT("HighResTextures"));
    ConfigData.EnabledFeatures.Add(TEXT("RayTracing"));

    FString OutputString;
    // FJsonObject를 통한 직렬화 (JsonUtilities 사용)
    TSharedPtr<FJsonObject> JsonObject = FJsonObjectConverter::UStructToJsonObject(FGameConfigData::StaticStruct(), &ConfigData);
    if (JsonObject.IsValid())
    {
        TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
        FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer, true);
        UE_LOG(LogTemp, Warning, TEXT("UStruct to JSON:\n%s"), *OutputString);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to convert UStruct to FJsonObject."));
    }

    // 파일로 저장
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfigStruct.json");
    if (FFileHelper::SaveStringToFile(OutputString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8))
    {
    UE_LOG(LogTemp, Warning, TEXT("UStruct JSON saved to: %s"), *FilePath);
    }
}

void DeserializeJsonToUStruct()
{
    FString FilePath = FPaths::ProjectSavedDir() + TEXT("GameConfigStruct.json");
    FString InputString;
    if (!FFileHelper::LoadFileToString(InputString, *FilePath))
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to load JSON file: %s"), *FilePath);
        return;
    }

    FGameConfigData LoadedConfigData;
    // JSON 문자열을 FJsonObject로 파싱
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(InputString);
    TSharedPtr<FJsonObject> JsonObject;
    if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    {
        // FJsonObject를 UStruct로 역직렬화 (JsonUtilities 사용)
        if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), FGameConfigData::StaticStruct(), &LoadedConfigData))
        {
            UE_LOG(LogTemp, Warning, TEXT("JSON to UStruct Loaded:"));
            UE_LOG(LogTemp, Warning, TEXT("  LastPlayedPlayerName: %s"), *LoadedConfigData.LastPlayedPlayerName);
            UE_LOG(LogTemp, Warning, TEXT("  MasterVolume: %f"), LoadedConfigData.MasterVolume);
            UE_LOG(LogTemp, Warning, TEXT("  MaxFps: %d"), LoadedConfigData.MaxFps);
            UE_LOG(LogTemp, Warning, TEXT("  EnabledFeatures: %s"), *FString::Join(LoadedConfigData.EnabledFeatures, TEXT(", ")));
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("Failed to convert FJsonObject to UStruct."));
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to parse JSON for UStruct deserialization."));
    }
}
  • FJsonObjectConverter::UStructToJsonObject: USTRUCT의 인스턴스를 FJsonObject로 변환합니다.
  • FJsonObjectConverter::JsonObjectToUStruct: FJsonObjectUSTRUCT의 인스턴스로 변환합니다.

이 방법은 특히 게임 설정, 로컬 캐시 데이터, 또는 웹 API와 통신할 때 매우 유용합니다.


Data Asset / Data Table 기반 데이터 주도 설계 (C++ 실전)

Data Table과 Data Asset 선택 기준

둘 다 데이터 주도 설계에 쓰이지만, 표 형태 대량 데이터인지 개별 에셋 단위 구성인지에 따라 편집과 로딩 방식이 달라진다.

  1. Data Table 후보

    아이템 목록 Data Table 후보 행마다 같은 필드를 갖고 CSV, JSON, 엑셀식 관리가 편하다. Row Struct

  2. Primary Data Asset 후보

    스킬 정의 Primary Data Asset 후보 이펙트, 사운드, 아이콘, 클래스 참조처럼 에셋 연결이 많을 때 좋다. Asset Manager

  3. SaveGame 또는 JSON 저장

    플레이어 세이브 SaveGame 또는 JSON 저장 런타임에 변하는 값은 에디터 데이터 에셋과 분리한다. mutable data

  4. 로드 직후 필드 검증

    검증 규칙 로드 직후 필드 검증 없는 RowName, 잘못된 Soft Reference, 음수 수치 같은 오류를 초기에 잡는다. 시작 점검

파일 I/O와 JSON이 범용 데이터 교환에 강하다면, 언리얼 내부 게임 데이터 운용에서는 UDataAsset/UPrimaryDataAsset/UDataTable 조합이 더 안전하고 생산적일 때가 많습니다.

특히 밸런스 수치, 아이템 정의, 스킬 파라미터처럼 디자이너가 자주 수정하는 데이터에 적합합니다.

1) Data Table 행 구조 정의

ItemDataRow.h
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ItemDataRow.generated.h"

USTRUCT(BlueprintType)
struct FItemDataRow : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FName ItemId;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FText DisplayName;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    int32 MaxStack = 1;
};

2) Primary Data Asset 정의

ItemDefinitionAsset.h
#pragma once

#include "CoreMinimal.h"
#include "Engine/PrimaryDataAsset.h"
#include "ItemDefinitionAsset.generated.h"

UCLASS(BlueprintType)
class UItemDefinitionAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FName ItemId;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FText Description;
};

3) C++에서 로드/접근 + 검증

GameDataService.cpp (일부)
#include "ItemDataRow.h"
#include "ItemDefinitionAsset.h"
#include "Engine/DataTable.h"
#include "UObject/SoftObjectPtr.h"

bool UGameDataService::TryGetItemRow(FName ItemId, FItemDataRow& OutRow) const
{
    if (!ItemTable)
    {
        UE_LOG(LogTemp, Error, TEXT("ItemTable is null"));
        return false;
    }

    const FString Context = TEXT("TryGetItemRow");
    if (const FItemDataRow* Row = ItemTable->FindRow<FItemDataRow>(ItemId, Context))
    {
        OutRow = *Row;
        return true;
    }

    UE_LOG(LogTemp, Warning, TEXT("Item row not found: %s"), *ItemId.ToString());
    return false;
}

bool UGameDataService::ValidateItemRow(const FItemDataRow& Row) const
{
    if (Row.ItemId.IsNone()) return false;
    if (Row.MaxStack <= 0) return false;
    return true;
}
운영 팁
  • 런타임 로딩은 TSoftObjectPtr + 비동기 로딩(FStreamableManager)을 우선 검토합니다.
  • 데이터 검증은 에디터 저장 시(Validation) + 게임 시작 시(런타임 가드)를 이중으로 두는 것이 안전합니다.
  • 아이템/스킬/UI 표시 텍스트가 한 데이터를 공유하도록 설계하면 핫픽스와 유지보수가 쉬워집니다.

파일 I/O, JSON, Data Asset/Data Table은 서로 대체재라기보다 쓰임이 다른 데이터 통로입니다.

SaveGame은 플레이어별 진행 상태, JSON은 외부 교환과 설정, Data Asset과 Data Table은 에디터에서 관리되는 게임 정의 데이터에 맞춰 선택하면 C++ 코드가 다루는 데이터 경계가 훨씬 선명해집니다.

데이터 통로는 저장 위치보다 수정 주체와 수명으로 고릅니다

SaveGame, 파일 I/O, JSON, Data Asset은 모두 데이터를 다루지만, 플레이어별 상태인지, 외부 교환 형식인지, 에디터에서 관리되는 정의 데이터인지에 따라 선택 기준이 달라집니다.

  1. 플레이어 진행 상태

    SaveGame 슬롯별 점수, 위치, 인벤토리, 퀘스트 완료 여부를 보존합니다.

  2. 텍스트 로컬 파일

    FFileHelper 로그, 텍스트 설정, 작은 캐시처럼 직접 경로를 다룰 때 적합합니다.

  3. 외부 교환 형식

    JSON 웹 API, 설정 파일, 도구 출력처럼 사람이 읽고 파싱하기 쉬운 구조입니다.

  4. 게임 정의 데이터

    Data Asset 아이템, 스킬, 밸런스 값처럼 디자이너가 에디터에서 조정합니다.

  5. runtime
    C++ 코드에서 만나는 경계

    runtime 읽기 LoadGameFromSlot , LoadFileToString , JSON 파싱 결과는 모두 실패 가능성을 반환값으로 확인합니다. 쓰기 SaveGameToSlot 과 SaveStringToFile 은 저장 성공 여부를 로그와 사용자 피드백으로 남깁니다.

  6. editor
    데이터 주도 설계에서 확인

    editor 런타임 가드 Data Table 행과 Primary Data Asset은 저장 시 검증과 게임 시작 시 런타임 가드를 함께 둡니다. 공유 아이템 이름, 설명, 스택 수, UI 표시 텍스트가 같은 데이터 정의를 바라보면 유지보수가 쉬워집니다.

마지막으로 데이터 통로를 선택한 뒤에는 누가 수정하고 언제 읽으며 실패 시 어떻게 회복할지까지 함께 고정해야 합니다.

데이터 통로 선택 기준

SaveGame, 파일 I/O, JSON, Data Asset은 모두 유용하지만 누가 고치고 언제 읽으며 실패하면 어떻게 복구할지가 선택 기준입니다.

  1. 플레이어별 상태

    SaveGame 진행도, 위치, 인벤토리처럼 슬롯마다 달라지는 값에 맞습니다.

  2. 텍스트 로컬 기록

    File 로그, 텍스트 캐시, 임시 설정처럼 직접 경로를 다룰 때 씁니다.

  3. 외부 교환

    JSON 웹 API, 툴 출력, 사람이 읽는 설정 파일처럼 키와 값을 보존해야 할 때 맞습니다.

  4. 게임 정의 데이터

    Asset 아이템, 스킬, 밸런스처럼 에디터에서 관리할 값에 적합합니다.

  5. owner
    수정 주체

    owner 런타임 진행 상태는 SaveGame으로 분리해 프로필마다 다르게 둡니다. 밸런스와 표시 텍스트는 Data Table 또는 Data Asset으로 모읍니다.

  6. guard
    실패 회복

    guard 파일 없음, JSON 파싱 실패, 데이터 행 누락은 기본값으로 회복합니다. 저장 실패는 재시도 가능 여부와 사용자 안내를 따로 처리합니다.

언리얼 엔진의 파일 입출력 및 JSON 처리 기능은 SaveGame 시스템으로 해결할 수 없는 더 넓은 범위의 데이터 저장 및 교환 요구 사항을 충족시킵니다.

FFileHelper를 사용하여 간단한 파일 작업을 수행하고, JsonJsonUtilities 모듈을 통해 JSON 데이터를 효율적으로 파싱하고 생성하는 방법을 익히면 게임 내외부의 다양한 데이터를 유연하게 관리할 수 있게 됩니다.

이는 특히 설정 파일 관리, 외부 데이터 연동, 그리고 향후 웹 서비스와의 통신을 구현하는 데 중요한 기반이 됩니다.

파일 입출력 및 JSON 처리는 파일 경로, 접근 권한, JSON 스키마, 실패 복구, 저장 시점을 기준으로 점검합니다.

특히 외부 파일은 누가 쓰고 언제 다시 읽는지까지 정해야 SaveGame, 설정 파일, 서버 연동 데이터가 서로 섞이지 않습니다.