본문으로 건너뛰기
SheetForge

핵심 개념

시트는 단일 진실 공급원이다

데이터의 정본 형태는 정확히 하나, 시트뿐이다. 그 외의 모든 것은 파생된 것이다.

  • 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를 위한 머신 좌표로도 렌더링된다.

자동 임포트 체인

스키마가 새롭거나 변경된 경우, 임포트를 한 번 실행하면 내부적으로 다음을 수행한다.

  1. 생성된 코드 기록 → Unity 컴파일 → 도메인 리로드.
  2. 리로드 이후, 체인이 스스로 재개되어 베이크를 완료한다.

수동으로 무언가를 다시 트리거할 필요는 전혀 없다. 컴파일이 실패하면(예: 게임 코드가 방금 이름이 변경된 필드를 참조하는 경우), 체인은 반복하는 대신 조치 가능한 콘솔 문장과 함께 안전 중단한다(시도 횟수 제한 3회, 재개 로그).

강타입, 런타임 파싱 없음

코드젠은 @name / @type / @desc를 읽어, 탭 Foo마다 다음을 만들어낸다.

  • FooDefinition — 컬럼당 하나의 필드를 갖는 강타입 레코드 클래스. @desc는 XML 문서 주석과 인스펙터 툴팁이 된다.
  • FooDatabase : DefinitionDatabaseRecords, 지연 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.01은 값이 동일하므로 허용된다.
  • 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는 결코 도메인 패키지를 참조하지 않는다. 이 단방향 의존성은 컴파일러에 의해 강제된다. 그 정본 목록 — 그리고 그 개수 — 은 플러그인 작성에 있다.

관련 페이지