핵심 개념
시트는 단일 진실 공급원이다
데이터의 정본 형태는 정확히 하나, 시트뿐이다. 그 외의 모든 것은 파생된 것이다.
- IR(불변 Definition)은 시트를 검증하고 조립한 형태다.
- 생성된 C# 클래스는 IR의 스키마를 강타입으로 만든 것이다.
- 베이크된 ScriptableObject는 IR의 값을 로드 가능하게 만든 것이다 — 조회용 캐시일 뿐, 결코 독립적인 진실이 아니다.
모든 수정은 시트를 거쳐야 하며, 실제로 반영되려면 재임포트 검증을 통과해야 한다. 베이크된 SO를 직접 편집하면 두 번째 진실이 생겨나 검증을 우회하게 된다 — 제품은 의도적으로 이를 워크플로우로 지원하지 않는다.
(인스펙터의 "테스트 편집" 토글은 임시 런타임 실험을 위해 존재한다. 이는 결코 다시 기록되지 않으며 재임포트가 이를 지운다.)
이것이 중요한 이유: SO를 진실 공급원으로 취급하는 프로젝트는 결국 검증되지 않은 데이터가 시트로부터 벗어나 표류하게 되고, 이를 조정할 방법이 없어진다. 여기서는 조정이 구조적이다 — 언제나 시트로부터 재생성한다.
IR — 불변의 검증된 조립체
IR은 검증이 만들어내는 결과물이다. 각 탭마다 SheetTable(스키마 + 레코드)이 생성되며, 그 셀은 이미 타입이 지정된 값(int, float, enum 값, 레코드 참조, 에셋 참조, 리스트, 커스텀 플러그인 타입)이다.
주요 속성:
- 부분 조립 없음. 어디에라도 단 하나의 에러가 있으면 IR은 만들어지지 않는다(
ImportResult.Success == false ⇔ Registry == null— 하드 불변식). - null 없음. 비어 있는 선택적 셀은 즉시 타입 기본값을 구체화하며,
IsDefaulted로 표시된다 — 소비자 측에서 null 체크를 할 필요가 없다. - 불변. IR은 조립 이후 읽기 전용이다. 출구(코드젠, 베이크, export)는 이를 읽기만 할 뿐, 결코 변경하지 않는다.
파이프라인
fetch → parse markers/schema → parse cells → validate (keys, references,
@overlap, asset keys, domain rules) → assemble IR → codegen (.cs) → bake (SO)
└──────────────── collect ALL diagnostics ────────────────┘- 검증은 모든 것을 수집한다. 한 번 실행으로 문제의 완전한 목록을 얻는다 — 에러마다 어디서/무엇이/왜/어떻게가 함께 — 재임포트마다 에러 하나씩 고치는 대신.
- 코드젠은 마지막 단계다. 검증과 값 조립 이후에 실행되는데,
.cs파일을 기록하는 행위가 도메인 리로드를 트리거하기 때문이다. 파이프라인은 이 리로드가 안전하도록, 그리고 리로드 이후 체인이 자동으로 재개되도록 구성되어 있다. - 에러는 구조화된 객체이며, 문장으로 렌더링된다. 각 에러는 탭, 1부터 시작하는 행 번호, 컬럼 문자 그리고 필드명, 문제가 된 값, 위반된 규칙, 조치 가능한 제안(오타에 대한 최근접 일치 제안 포함)을 담고 있다. 동일한 객체는 로그/CI를 위한 머신 좌표로도 렌더링된다.
자동 임포트 체인
스키마가 새롭거나 변경된 경우, 임포트를 한 번 실행하면 내부적으로 다음을 수행한다.
- 생성된 코드 기록 → Unity 컴파일 → 도메인 리로드.
- 리로드 이후, 체인이 스스로 재개되어 베이크를 완료한다.
수동으로 무언가를 다시 트리거할 필요는 전혀 없다. 컴파일이 실패하면(예: 게임 코드가 방금 이름이 변경된 필드를 참조하는 경우), 체인은 반복하는 대신 조치 가능한 콘솔 문장과 함께 안전 중단한다(시도 횟수 제한 3회, 재개 로그).
강타입, 런타임 파싱 없음
코드젠은 @name / @type / @desc를 읽어, 탭 Foo마다 다음을 만들어낸다.
FooDefinition— 컬럼당 하나의 필드를 갖는 강타입 레코드 클래스.@desc는 XML 문서 주석과 인스펙터 툴팁이 된다.FooDatabase : DefinitionDatabase—Records, 지연 id 조회,SchemaFingerprint를 갖는 탭별 컨테이너 SO.
베이크는 실제 타입 필드를 기록한다 — 런타임 텍스트 파싱 없음, 런타임 리플렉션 없음, 이는 IL2CPP에 안전하다는 것을 의미한다(스트리핑 위험 없음).
주소 기반 로드 — 캐시가 공유 가능하게 유지되는 방법
베이크된 SO는 머신별 GUID를 갖는 머신별 캐시다. 이에 대한 직접적인 씬 참조는 머신 간에 깨질 것이다. 대신:
- 임포트는 각 Database SO를 안정적인 주소
"SheetForge/{tab}"으로 Addressables 그룹SheetForge에 자동 등록한다(재베이크는 새 GUID를 동일한 주소에 다시 연결하며, 삭제된 탭은 정리된다). - 게임 코드는 주소로 로드한다:
SheetForgeDatabases.LoadAsync<FooDatabase>("Foo"). - Addressables 그룹 에셋은 gitignore되며 자가 치유된다(누락 시 임포트가 재생성).
Baseline — 라운드트립이 시트를 보존하는 방법
임포트 시, 각 탭의 구조(마커 행, 컬럼 순서, 주석, 사람이 작성한 텍스트)에 대한 정규화된 스냅샷이 baseline으로 저장된다. Export는 그다음 현재 SO 값을 baseline 구조 안으로 바꿔 넣는다.
따라서 시트 → 임포트 → Export → 시트 라운드트립은 시트를 구조적으로 100% 보존하며, 값을 의미적으로 보존한다:
1.0↔1은 값이 동일하므로 허용된다.- float는 가장 짧은 라운드트립 형식을 사용한다.
- 소수점 구분자는 항상
.이며, 로케일에 독립적이다.
무엇이 커밋되고 무엇이 재생성되는가
| 산출물 | 정책 |
|---|---|
| 시트(로컬 파일) / Google 시트 | 진실. 커밋됨 / 공유됨. |
베이크된 Database SO (Assets/SheetForgeBaked) | gitignore된 머신별 캐시 — 임포트를 실행해 재생성. |
생성된 코드 (Assets/SheetForgeGenerated) | 커밋하는 쪽이 권장된다. 프로젝트 자신의 소스이며, Assets/SheetForge 밖에 있어 제품을 재설치해도 삭제되지 않고, 커밋해 두면 누군가 임포트를 실행하기도 전에 새 클론이 바로 컴파일된다. 산출물은 결정적(deterministic)이라 팀원의 임포트도 동일한 바이트를 만들어낸다. gitignore로 제외하는 것도 유효한 대안이며, 그 경우 다음 임포트가 재생성한다. 이 기본값 이전부터 있던 프로젝트는 Assets/SheetForge/Runtime/Generated가 비워질 때까지 계속 그 자리에 생성한다 — 시작하기 참고. |
Addressables SheetForge 그룹 에셋 | gitignore되며 자가 치유됨. 최초 생성 시 만들어지는 한 줄짜리 설정 diff는 커밋하지 않는다. |
도메인 패키지 자체의 Generated 폴더 | 해당 패키지의 자체 선택. 번들로 제공되는 SheetForge.PluginDemo 샘플은 생성 코드를 커밋하여, 임포트하자마자 데모가 즉시 컴파일되도록 한다. |
| 임포트 설정 에셋 | 사용자가 직접 관리한다. 서비스 계정 키 경로는 저장소 밖에 둔다(SHEETFORGE_SHEETS_KEY 환경 변수 사용). |
수정 없는 확장
등록 계약을 통해 플러그인은 Core 수정 없이 파이프라인에 합류할 수 있다:
- 셀 타입 파서(래퍼 타입 포함), 도메인 검증기, 엣지 기여자;
- 커스텀 구조 마커, "시트 생성" 템플릿, 임포트 소스 프로바이더;
- 데이터 스튜디오의 캔버스 오버라이드·코드 레지스트리·위젯·액션·셀 위젯·컬러 프리셋·UI 문자열.
Core는 결코 도메인 패키지를 참조하지 않는다. 이 단방향 의존성은 컴파일러에 의해 강제된다. 그 정본 목록 — 그리고 그 개수 — 은 플러그인 작성에 있다.
관련 페이지
- 시트 문법 — 파서가 읽는 마커와 타입 문법
- 데이터 스튜디오 — 이 모델 위에서의 작성
- 소스, 내보내기 및 Push — 라운드트립 메커니즘
- 작성 커널 — 작성 창 아래의 엔진