· 13 min read

Datra — 게임 데이터를 코드로 다루는 방법

C# Source Generator 기반 게임 데이터 관리 시스템. CSV, JSON, YAML을 보일러플레이트 없이 타입 세이프하게.

게임 데이터의 고질적인 문제

게임 개발에서 데이터 관리는 늘 번거롭다. 캐릭터 스탯, 스킬 테이블, 퀘스트 정보 — 기획자는 엑셀(CSV)로 작업하고, 프로그래머는 그걸 코드에서 읽어야 한다.

보통 이런 흐름이다:

  1. 기획자가 CSV를 수정한다
  2. 변환 스크립트를 돌린다
  3. 프로그래머가 직렬화/역직렬화 코드를 작성한다
  4. 필드가 바뀌면 2~3을 반복한다

이 과정에서 타입 불일치, 필드 누락, 런타임 에러가 끝없이 발생한다. 리플렉션 기반이면 성능도 문제다. ScriptableObject를 쓰면 Unity에 묶이고, 서버에서는 못 쓴다.

Datra는 이 문제를 C# Source Generator로 해결한다.

Datra가 하는 일

데이터 모델 클래스에 어트리뷰트를 붙이면, 컴파일 타임에 직렬화 코드가 자동 생성된다. 런타임 리플렉션이 없고, 보일러플레이트도 없다.

[TableData("Characters.csv", Format = DataFormat.Csv)]
public partial class CharacterData : ITableData<string>
{
    public string Id { get; set; }

    [FixedLocale]
    public LocaleRef Name => LocaleRef.CreateFixed(
        nameof(CharacterData), Id, nameof(Name));

    public int Level { get; set; }
    public int Health { get; set; }
    public CharacterGrade Grade { get; set; }       // enum
    public StatType[] Stats { get; set; }            // 배열
    public int[] UpgradeCosts { get; set; }          // 배열
    public PooledPrefab TestPooledPrefab { get; set; } // 중첩 구조체
}

이걸 빌드하면 Source Generator가 CSV 파서, 타입 변환기, 컨텍스트 클래스를 전부 생성한다. 런타임에는 이렇게 쓴다:

var context = new GameDataContext(provider);
await context.LoadAllAsync();

var hero = context.Character.GetById("hero_001");
Console.WriteLine($"{hero.Name} Lv.{hero.Level}");

Source Generator 파이프라인

Datra의 핵심은 컴파일 타임 코드 생성이다. 빌드할 때 다음 과정이 자동으로 일어난다.

SyntaxReceiver[TableData], [SingleData], [AssetData] 어트리뷰트가 붙은 클래스를 수집한다. DataModelAnalyzer가 각 클래스의 프로퍼티를 분석해서 타입, 배열 여부, DataRef 여부, 중첩 타입 여부 등 메타데이터를 추출한다.

이 정보를 바탕으로 세 가지 코드가 생성된다:

  1. DataContextGeneratorGameDataContext 클래스. Repository 프로퍼티와 LoadAllAsync() 메서드
  2. DataModelGenerator — 각 모델의 포맷별 Serializer (CSV, JSON, YAML)
  3. LocalizationGeneratorLocalizationContext (로컬라이제이션 활성화 시)

모든 직렬화 코드가 컴파일 타임에 확정되므로 런타임 리플렉션이 없고, 타입 안전성이 보장된다.

아키텍처

4개 레이어로 구성된다.

Application Layer — 자동 생성된 GameDataContext가 진입점. context.Character.GetById() 같은 타입 세이프 API를 제공한다.

Repository Layer — 데이터 접근을 추상화한다. TableRepository는 키-값 테이블, SingleRepository는 단일 설정 객체, AssetRepository는 파일 기반 에셋을 담당한다. 모든 Repository는 변경 추적과 SaveAsync()를 지원한다.

Serialization Layer — Source Generator가 생성한 포맷별 직렬화 코드. CSV, JSON, YAML 각각의 Serializer가 모델 구조에 맞게 최적화되어 있다.

Data Provider LayerIRawDataProvider 인터페이스로 데이터 출처를 추상화한다. Resources/, 파일시스템, Unity AssetDatabase, Addressables 등 런타임 환경에 따라 Provider만 교체하면 된다.

멀티 포맷 지원

CSV, JSON, YAML을 모두 지원한다. 파일 확장자로 자동 감지하거나, Format을 명시할 수 있다.

CSV — 기획 데이터

기획자가 가장 익숙한 포맷. 배열은 | 구분자, 중첩 타입은 점 표기법을 사용한다:

Id,Level,Health,Grade,Stats,UpgradeCosts,TestPooledPrefab.Path,TestPooledPrefab.InitialCount
hero_001,10,1000,Common,Attack|Defense|HealthRegen,100|200|400,Assets/Prefabs/Slash.prefab,3
hero_002,8,600,Rare,Attack|ManaRegen|CriticalDamage,150|300|600,Assets/Prefabs/Fireball.prefab,5

JSON — 복잡한 중첩 구조

설정 파일이나 다형성이 필요한 데이터에 적합:

{
  "GameName": "Epic Adventure",
  "MaxLevel": 100,
  "ExpMultiplier": 1.5,
  "AvailableModes": ["Easy", "Normal", "Hard", "Expert"],
  "StartingItems": [1001, 1002, 2001]
}

YAML — 다형성 데이터

스킬 이펙트처럼 타입이 분기되는 데이터에 사용. $type 필드로 구체 타입을 구분한다:

- Id: skill_fireball
  ManaCost: 25
  Cooldown: 3.0
  Effects:
    - $type: DamageEffect
      BaseDamage: 50
      DamageType: Fire
      IgnoreDefense: false
    - $type: CrowdControlEffect
      ControlType: Stun
      Chance: 0.5
      Duration: 2.0

타입 세이프 데이터 참조

테이블 간 참조를 문자열 ID 대신 DataRef<T>로 표현한다:

public StringDataRef<SkillData> MainSkill { get; set; }
public IntDataRef<ItemData> RewardItem { get; set; }

CSV에는 ID만 저장되고, 런타임에 Evaluate(context)로 실제 데이터를 가져온다. 존재하지 않는 ID를 참조하면 컴파일 타임에 잡을 수 있다. Unity Editor에서는 드롭다운 피커로 참조할 데이터를 선택한다.

다형성 지원

게임에서 흔한 패턴인 스킬 이펙트, 버프, 조건 시스템 등을 지원한다:

public abstract class SkillEffect
{
    public string Id { get; set; }
    public float Duration { get; set; }
}

public class DamageEffect : SkillEffect
{
    public int BaseDamage { get; set; }
    public DamageType DamageType { get; set; }
    public bool IgnoreDefense { get; set; }
}

public class HealEffect : SkillEffect
{
    public int BaseHeal { get; set; }
    public bool IsPercentage { get; set; }
}

public class BuffEffect : SkillEffect
{
    public string BuffId { get; set; }
    public int Stacks { get; set; }
    public SkillStatType AffectedStat { get; set; }
}

SkillData에서 List<SkillEffect>로 참조하면, JSON/YAML에서 $type 필드 기반으로 구체 타입이 자동 역직렬화된다. 스킬 하나에 데미지, 스턴, 버프를 조합하는 식의 설계가 데이터만으로 가능하다.

로컬라이제이션

LocaleRef로 다국어 키를 관리한다. [FixedLocale]를 붙이면 데이터 ID 기반으로 키가 자동 생성된다:

[FixedLocale]
public LocaleRef DisplayName { get; set; }
// → 자동 키: "CharacterData.hero_001.DisplayName"

[DatraConfiguration]에서 로컬라이제이션을 활성화하면 LocalizationContext가 함께 생성된다. 언어별 CSV 파일(en.csv, ko.csv, ja.csv)로 번역 데이터를 관리하고, Editor에서 언어를 전환하며 편집할 수 있다.

“Sync FixedLocale Keys” 기능은 새로 추가된 데이터 항목의 로컬라이제이션 키를 자동으로 감지하고 생성해 준다. 예를 들어 hero_003을 추가하면 CharacterData.hero_003.Name 키가 누락된 것을 감지하고, 한 번에 모든 언어 파일에 빈 키를 생성한다.

Unity Editor

Datra는 Unity UI Toolkit 기반 데이터 에디터를 제공한다. Window > Datra > Data Editor로 접근한다.

테이블 뷰

데이터를 스프레드시트 형태로 보여준다. 중첩 타입은 점 표기법으로 컬럼이 확장된다 (TestPooledPrefab.Path, TestPooledPrefab.InitialCount). 행을 더블 클릭하면 폼 뷰로 전환된다. 대규모 데이터셋도 가상화 스크롤링으로 부드럽게 처리한다.

폼 뷰

개별 항목의 모든 프로퍼티를 상세 편집한다:

  • 기본 타입 — int, float, string, bool 필드 에디터
  • Enum — 드롭다운 선택
  • 배열/리스트 — 항목 추가/삭제/재정렬 UI
  • 중첩 타입 — 접을 수 있는 섹션으로 표시
  • DataRef — 참조 대상 데이터를 드롭다운으로 선택
  • 에셋 경로[AssetType], [FolderPath] 어트리뷰트에 따라 폴더/타입 제한이 걸린 에셋 피커
  • LocaleRef — 인라인 팝업으로 다국어 값 편집

변경 추적

수정된 항목은 주황색 인디케이터(●)로 표시된다. 네비게이션 패널의 데이터 타입 옆, 폼 뷰의 항목 헤더에 수정 여부가 실시간으로 반영된다. Save는 현재 선택된 데이터만, Save All은 수정된 모든 데이터를 저장한다. Reload는 디스크에서 다시 읽어온다 (미저장 변경이 있으면 경고).

MVVM 아키텍처

에디터는 MVVM 패턴으로 설계되어 있다:

  • ViewDatraEditorWindow와 각 패널 (Toolbar, Navigation, Inspector, Localization)
  • ViewModelDatraEditorViewModel. 순수 C#으로 Unity 의존성 없이 테스트 가능
  • ServiceIDataEditorService, IChangeTrackingService, ILocaleEditorService. 모킹 가능한 인터페이스

ViewModel이 모든 상태 관리와 비즈니스 로직을 담당하고, View는 UI 렌더링만 한다. 이 구조 덕분에 에디터 로직의 유닛 테스트가 가능하다.

에셋 데이터

파일 기반 에셋을 [AssetData]로 관리할 수 있다:

[AssetData("Scripts/", Pattern = "*.json")]
public partial class ScriptData : ITableData<string>
{
    public string Id { get; set; }
    public string Content { get; set; }
}

각 에셋 파일 옆에 .datrameta 메타 파일이 생성되어 GUID를 유지한다. 파일을 이름 변경하거나 이동해도 GUID가 유지되므로, 참조가 깨지지 않는다.

.NET + Unity 동시 지원

코어 라이브러리는 .NET Standard 2.1이다. Unity 프로젝트에서도, 순수 .NET 서버에서도 같은 데이터 모델을 공유할 수 있다. 클라이언트와 서버가 같은 데이터 정의를 쓰는 것 — 게임 개발에서 이게 얼마나 큰 장점인지는 해본 사람이 안다.

// Unity 클라이언트
var provider = new ResourcesRawDataProvider("Data");
var context = new GameDataContext(provider);

// .NET 서버 — 같은 GameDataContext, 같은 모델
var provider = new FileRawDataProvider("./data");
var context = new GameDataContext(provider);

Provider만 교체하면 된다. 데이터 모델, 직렬화 로직, 비즈니스 로직은 100% 공유된다.

기술 스택

영역기술
언어C# 11+
타겟.NET Standard 2.1
코드 생성C# Source Generators
정적 분석Roslyn Analyzers
직렬화CsvHelper, YamlDotNet, Newtonsoft.Json
게임 엔진Unity 2020.3+
에디터 UIUnity UI Toolkit
에셋 로딩Addressables (옵션)
테스트NUnit, .NET Unit Tests

현재 상태

9개 프로젝트로 구성된 솔루션. 48개 이상의 유닛 테스트. 샘플 데이터로 50개 캐릭터 엔트리, 다형성 스킬 이펙트, 에셋 데이터 등을 포함.

최근 작업:

  • 에셋 데이터 GUID 안정성 (.datrameta)
  • YAML 직렬화 완전 지원 (CSV, JSON과 동등)
  • 에디터 MVVM 아키텍처 리팩토링
  • ChangeSummary API — 트랜잭션 패턴의 변경 추적
  • 테이블 컬럼 폭 영속화
  • 익명 타입 직렬화 지원

프로젝트는 GitHub에서 오픈소스로 공개되어 있다.

Back to Blog

Related Posts

View All Posts »