· 13 min read
Datra — 게임 데이터를 코드로 다루는 방법
C# Source Generator 기반 게임 데이터 관리 시스템. CSV, JSON, YAML을 보일러플레이트 없이 타입 세이프하게.
게임 데이터의 고질적인 문제
게임 개발에서 데이터 관리는 늘 번거롭다. 캐릭터 스탯, 스킬 테이블, 퀘스트 정보 — 기획자는 엑셀(CSV)로 작업하고, 프로그래머는 그걸 코드에서 읽어야 한다.
보통 이런 흐름이다:
- 기획자가 CSV를 수정한다
- 변환 스크립트를 돌린다
- 프로그래머가 직렬화/역직렬화 코드를 작성한다
- 필드가 바뀌면 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 여부, 중첩 타입 여부 등 메타데이터를 추출한다.
이 정보를 바탕으로 세 가지 코드가 생성된다:
- DataContextGenerator —
GameDataContext클래스. Repository 프로퍼티와LoadAllAsync()메서드 - DataModelGenerator — 각 모델의 포맷별 Serializer (CSV, JSON, YAML)
- LocalizationGenerator —
LocalizationContext(로컬라이제이션 활성화 시)
모든 직렬화 코드가 컴파일 타임에 확정되므로 런타임 리플렉션이 없고, 타입 안전성이 보장된다.
아키텍처
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 Layer — IRawDataProvider 인터페이스로 데이터 출처를 추상화한다. 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,5JSON — 복잡한 중첩 구조
설정 파일이나 다형성이 필요한 데이터에 적합:
{
"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 패턴으로 설계되어 있다:
- View —
DatraEditorWindow와 각 패널 (Toolbar, Navigation, Inspector, Localization) - ViewModel —
DatraEditorViewModel. 순수 C#으로 Unity 의존성 없이 테스트 가능 - Service —
IDataEditorService,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+ |
| 에디터 UI | Unity UI Toolkit |
| 에셋 로딩 | Addressables (옵션) |
| 테스트 | NUnit, .NET Unit Tests |
현재 상태
9개 프로젝트로 구성된 솔루션. 48개 이상의 유닛 테스트. 샘플 데이터로 50개 캐릭터 엔트리, 다형성 스킬 이펙트, 에셋 데이터 등을 포함.
최근 작업:
- 에셋 데이터 GUID 안정성 (
.datrameta) - YAML 직렬화 완전 지원 (CSV, JSON과 동등)
- 에디터 MVVM 아키텍처 리팩토링
ChangeSummaryAPI — 트랜잭션 패턴의 변경 추적- 테이블 컬럼 폭 영속화
- 익명 타입 직렬화 지원
프로젝트는 GitHub에서 오픈소스로 공개되어 있다.
