본문으로 건너뛰기
SheetForge

소스, 내보내기 및 Push

세 가지 쓰기 경로가 있으며, 각각 다른 대상을 향한다:

  • reflect는 작성 스테이징을 소스에 기록한다.
  • Export는 베이크된 SO 값을 시트 파일로 다시 기록한다.
  • Push는 베이크된 SO 값을 셀 단위로 라이브 Google 시트에 기록한다.

임포트 소스

임포트 소스는 설정 에셋에서 일급으로 취급되는 선택지다. 각 소스는 자신만의 작성 능력(CanAuthor)을 선언한다:

소스읽어들이는 것작성(쓰기 반영)
LocalFile.tsv / .csv / .xlsx 파일이 있는 폴더(바로 아래 자식만 해당, 파일 하나 = 탭 하나, xlsx 워크북은 자신의 시트들을 제공)완전 — 반영, 구조 편집, 키/탭 이름 변경
GoogleSheet · SheetsApi서비스 계정 JWT 인증을 통한 비공개/공유 스프레드시트(설정 가이드)완전 — 정교한 셀 단위 쓰기, 구조 재작성, Push
GoogleSheet · ExportUrlexport URL을 통한 링크 공유 시트 — 인증 불필요읽기 전용(CanAuthor = false) — Push/반영/구조 편집/삭제가 설명과 함께 비활성화됨
커스텀 프로바이더플러그인이 등록하는 모든 것(ISheetSourceProvider — DB, REST, 자체 형식)CanAuthor 플래그를 통한 프로바이더의 선택

참고 사항:

  • ExportUrl은 gid 맵(탭 이름 → #gid= 값)이 필요하다 — gid가 없는 export URL은 조용히 첫 번째 탭만 반환하므로, 맵이 강제된다(GoogleSheetGidMapMissing, 중복된 gid는 거부됨). SheetsApi 모드는 탭을 자동으로 발견하며 맵이 필요 없다.
  • 내장 xlsx 리더/라이터는 직접 작성한 OOXML이다(System.IO.Compression + System.Xml만 사용 — NPOI/ClosedXML 없음, 서드파티 코드 전혀 없음). 그래서 프로젝트의 다른 에셋과 충돌할 수 있는 DLL을 전혀 추가하지 않는다. 이는 하나의 코덱을 공유한다: 같은 리더가 Unity 에디터에서, 그리고 — WebAssembly로 컴파일되어 — 웹 앱에서도 그대로 실행되므로, 두 호스트가 어떤 셀에 대해 서로 다른 판정을 내릴 수 없다. 이는 의도적으로 최소한으로 만들어졌고 그 사실에 정직하다 — 값만 다루며, 재계산은 없다:
    • 수식 셀은 파일에 캐시된 값을 그대로 내놓는다. 캐시된 값이 없는 수식과 에러 셀(#REF!, #DIV/0!)은 거부된다(UnsupportedXlsxCell) — Excel에서 통합 문서를 한 번 저장해 값을 캐시하거나, 수식을 값으로 구체화하라.
    • 날짜 서식 셀은 그 날짜값으로 읽혀 yyyy-MM-dd로 렌더링된다 — ISO 셀 타입과, 스타일이 날짜 서식인 평범한 숫자 모두 해당하며, 1900·1904 두 날짜 체계를 모두 존중한다 — 파일이 저장한 원문 시리얼 숫자 대신이다. 그 외의 숫자 서식, 병합된 셀, 차트는 임포트되지 않는다.
    • 이 해석들 — 캐시된 수식 값, 날짜를 표시 텍스트로, 서식은 무시 — 은 두 호스트 모두에서 리더의 고정된 정책이며, 웹 앱의 가져오기 대화상자는 실제로 일어난 것만 "이 워크북이 어떻게 읽혔는지" 메모에 추가로 명시한다.
    • 셀 안의 탭이나 개행은 거부된다(UnsupportedCellCharacter) — 리스트에는 ;를 사용하라.
    • 리더가 인식하지 못하는 셀 타입 코드는 거부되지 않고, 저장된 원문 텍스트 그대로 읽힌다.
  • 로컬 파일은 유니코드여야 한다. UTF-8 BOM 또는 UTF-16 BOM(LE 또는 BE)은 인식된다. BOM이 없으면 파일은 엄격한 UTF-8로 디코딩된다. CP949나 Shift-JIS 같은 레거시 단일 바이트 인코딩은 추측되지 않고 거부된다(UnsupportedEncoding) — 추측은 머신마다 다르게 디코딩되어 데이터를 조용히 손상시킬 것이다. 파일을 UTF-8로 다시 저장하라.
  • 소스는 부분 출력을 반환할 수 있다 — 하나의 손상된 파일이 읽을 수 있는 탭까지 떨어뜨리지는 않는다. 문제는 진단 정보로 전달된다.
  • 커스텀 소스 프로바이더는 자동으로 발견되며 동일한 설정 드롭다운에 나타난다 — 플러그인 작성 참고.
  • 소스가 사용자 모르게 바뀌었으면 스튜디오가 알려준다. 창에 포커스가 갈 때 — 또는 ⋯ 메뉴에서 요청하면 — 스튜디오는 소스를 다시 읽어 마지막 임포트의 스냅샷과 비교하고, 데이터가 실제로 다를 때만 배지를 표시한다: 다시 저장만 했거나 서식만 바뀐 시트는 조용하다 — 비교 기준이 타임스탬프가 아니라 내용이기 때문이다. 배지를 클릭하면 임포트 실행을 제안한다; 타이머로 폴링하는 것은 없고, 스스로 임포트하는 것도 없으며, 오프라인이거나 인증되지 않은 경우는 그저 배지가 뜨지 않을 뿐이다. 이는 모든 소스 종류에서 동일하게 동작한다 — 로컬 파일, export URL 시트, SheetsApi 모두 마찬가지다.

내보내기 — 라운드트립의 반환 절반

데이터 스튜디오 툴바의 ⋯ ▸ Export 실행은 베이크된 SO 값을 시트 파일로 다시 기록한다.

  • 구조는 baseline에서, 값은 SO에서 가져온다. Export는 시트 구조의 baseline 스냅샷 안으로 현재 값을 바꿔 넣는다 — 마커 행, 컬럼 순서, 주석, 사람이 작성한 텍스트가 100% 보존된다.
  • 의미적 값 라운드트립: 1.01 정규화가 허용된다(동일한 값이기 때문). float는 가장 짧은 라운드트립 형식을 사용하며, 항상 . 소수점을 사용한다.
  • 형식: Tsv / Csv / Xlsx / Json / MatchSource(각 탭은 임포트되었던 형식으로 돌아간다. Google 원본이거나 알 수 없는 경우 Tsv로 폴백한다). Json은 스프레드시트가 아니라 기계를 위한, 내보내기 전용 형식이다: 탭마다 파일 하나, 레코드는 객체로, int / float / bool은 실제 JSON 숫자와 불리언으로, 그 밖의 모든 값 — 참조, 리스트, 색상, 커브, 커스텀 타입 — 은 시트가 담고 있는 정확한 정본 셀 텍스트 그대로 담긴다. 그래서 서버나 외부 도구가 시트 텍스트를 파싱하지 않고도 게임 데이터를 소비할 수 있다. JSON은 임포트 소스가 아니며, JSON 파일은 라운드트립할 시트 구조를 전혀 담지 않는다 — 시트가 계속 정본이다. TSV와 CSV는 탭마다 파일을 하나씩 쓴다; Xlsx는 내보내는 모든 탭을 하나의 워크북(SheetForge.xlsx)에 담아, 각 탭이 탭 순서대로 자신만의 시트가 되게 한다 — 워크북은 여러 시트를 담기 위한 형식이며, 그것들을 한데 묶어 두는 것이 바로 참조 드롭다운이 시트를 가로질러 가리킬 수 있게 하는 것이기도 하다(아래 참고). MatchSource에서는 xlsx 원본 탭들이 그 하나의 워크북으로 모이고, 나머지는 각자의 파일로 돌아간다. 워크북 규칙이 담을 수 없는 시트 이름(너무 길거나 금지된 문자가 있는 경우)은 조정되며 이름과 함께 보고된다 — 결코 조용히 개명되지 않는다.
  • 최신성이 강제된다: 스키마 변경 이후 오래된 베이크로 export하면 ExportSchemaMismatch로 실패한다(베이크된 SchemaFingerprint가 baseline의 것과 일치해야 한다) — 먼저 임포트를 실행하라.
  • 에셋 참조는 GUID가 아니라 시트가 쓰는 주소 텍스트로 export된다 — 키, 또는 서브 에셋이면 parent[sub]; 그룹은 컬럼의 것이다. 타입 지정 컬럼(AssetRef@Group<Type>)도 동일한 방식으로 라운드트립된다. Color, AnimationCurve, Gradient 값은 정규 텍스트 형태로 돌아온다(시트 문법 참고); 키가 없는 커브는 빈 셀로 export되며, 색상은 0…1로 클램프된다(HDR 없음).

Push — Google 시트로의 셀 단위 쓰기 반영

데이터 스튜디오 툴바의 **⋯ ▸ Push 실행 (Google 시트)**는 베이크된 SO 값을 셀 단위로 라이브 시트에 전송한다. 활성 소스가 API 모드의 Google 시트가 아니면 이 항목은 비활성화되며, 그 사유가 명시된다. 이는 다른 누군가가 편집 중인 라이브 시트를 절대 손상시키지 않도록 설계되었다.

세 가지 보증이 이 체인으로부터 따라 나온다:

  • 셀 단위 계획에 대한 사용자의 승인 없이는 아무것도 전송되지 않는다.
  • 임포트 이후 라이브 시트에서 변경된 셀은 건너뛸 뿐 결코 덮어쓰지 않는다.
  • 행 삭제는 라이브 시트가 그 키를 정확히 그 행에 여전히 보여줄 때만 나간다 — 표류한 것은 무엇이든 알림과 함께 건너뛰어지며, 결코 추측으로 처리되지 않는다.

안전 체인은 순서대로 다음과 같다:

  1. SheetsApi 자격 증명이 필요하다 — ExportUrl 모드에서의 Push는 어떤 네트워크 호출보다도 먼저 거부된다(GooglePushRequiresSheetsApi).
  2. Push되는 탭마다 키 컬럼이 필요하다 — Push는 라이브 시트에서 각 행을 키로 재탐색하므로, 행이 이동한 것을 감지해 그 쓰기를 안전하게 건너뛸 수 있다(결코 잘못된 행으로 전송되지 않는다). 키가 없는 탭에 변경 사항이 있으면 거부된다(PushKeylessTabUnsupported).
  3. 계획 + 승인: 셀 단위 diff(baseline 대 현재 SO)가 계획으로 계산된다 — 쓰기, 추가, 행 삭제 — 그리고 무언가 전송되기 전에 명시적 승인을 위해 표시된다; 삭제는 자신만의 구획에 따로 서며, 각각 사라질 키의 이름이 붙는다. 거부 = 전송되는 셀 없음.
  4. 전송 전 라이브 재조회: 전송 직전, 라이브 시트를 다시 조회하여 비교한다. 충돌하는 셀은 덮어쓰이지 않고 건너뛴다(경고로 보고됨):
    • PushConflictCellChanged — 제삼자가 그 셀을 편집함.
    • PushConflictRowMoved — 키가 임포트 당시와 다른 행에서 발견되어, 쓰기는 건너뛰어진다(결코 잘못된 행으로 전송되지 않는다). 재임포트로 다시 동기화한 다음 다시 Push하라.
    • PushConflictRowMissing — 행이 외부에서 삭제됨.
    • PushConflictDuplicateLiveKey / PushConflictAppendKeyExists — 대상이 모호함.
  5. 행 삭제는 전송되기 전에 키로 대조된다. 사용자가 삭제한 레코드는, 전송 전 재조회가 그 키가 여전히 임포트 당시 본 그 행에 정확히 놓여 있음을 확인한 뒤에야 라이브 시트에서 제거된다: 이미 사라진 행은 완료로 친다(재푸시는 아무것도 두 번 삭제하지 않는다). 다른 행에서 그 키가 발견되면 — 시트가 표류한 것이므로 — 알림과 함께 건너뛰어지며, 결코 위치로 삭제되지 않는다. 삭제는 각 탭 안에서 아래에서 위로, 맨 마지막에 나간다 — 그래서 앞선 제거가 뒤따르는 것들의 좌표를 밀어내지 못한다. 행을 삭제할 수 없는 소스(그 능력이 없는 커스텀 프로바이더)는 정직하게 예전 방식으로 되돌아간다: 삭제는 보고되고, 라이브 행은 사용자가 직접 처리하도록 남겨진다.

Push 이후, 리포트의 적용/건너뜀 개수를 확인하라. 셀이 건너뛰어졌다면, 재임포트로 조정한 다음 다시 Push하라.

Google로의 구조 변경

Google 소스에 대한 구조 편집(컬럼, 마커, 순서 변경, 이름 변경)은 대상 탭 전체를 재작성한다 — 먼저 라이브 diff 확인을 거치고, 마지막 임포트 이후 시트에서 변경된 무언가를 덮어쓰기 전에 명시적 승인을 받는다. 값 편집은 정교하게(셀 단위로) 유지된다 — 구조만이 재작성 경로를 사용한다.

반영이 만드는 Addressables 등록

데이터 스튜디오에서 AssetRef@Group 셀에 에셋을 드롭하거나 프로젝트에서 하나를 고르면, 시트뿐 아니라 프로젝트에도 변경을 스테이징할 수 있다: 그룹에 에셋 추가, 다른 그룹에서 이동, 또는 그룹 생성이다. 이 등록들은 반영의 일부이며 체인 안의 고정된 자리에서 실행된다 — 로컬 폴더, Google 시트, 커스텀 소스 프로바이더 모두 같은 자리다:

  1. 사전 검증은 스테이징된 등록을 이미 존재하는 것으로 세어 전체 예상 상태를 검증하므로, 아직 등록되지 않은 에셋을 가리키는 셀은 에러가 아니다.
  2. 시트가 기록된다. 기록이 취소되거나 실패하면 아래 단계는 아무것도 실행되지 않는다: Addressables 설정은 그대로 남고, 등록은 다음 시도를 위해 계속 스테이징된 채로 남는다. 건드린 탭이 전부 건너뛰어져 어떤 탭도 기록할 수 없었던 반영(예: 워크북 기반 탭만 건드린 경우)도 이를 실행하지 않는다. 시트에 기록할 것이 전혀 없는 반영 — 스테이징된 변경이 등록 하나뿐인 경우 — 은 이를 실행하고 재임포트한다; 그 패스에서 다른 스테이징된 편집은 커밋되지 않으므로 되돌리기가 계속 가능하다.
  3. 등록이 실행된다, 순서대로: 그룹이 먼저 생성되고(기본 BundledAssetGroupSchemaContentUpdateGroupSchema로), 그다음 항목이 추가되거나 이동되어 주소를 받으며, 설정은 한 번 저장된다. 각 항목은 실행 직전에 다시 확인되며, 그사이 에셋이 삭제되었을 때, 그 주소를 이미 그 그룹의 다른 에셋이 쓰고 있을 때, 그룹을 생성하거나 찾을 수 없었을 때, 그리고 더 이상 어떤 셀도 그 주소를 참조하지 않을 때 강행되는 대신 건너뛰어진다(등록은 아무도 가리키지 않는 항목을 결코 만들지 않으며, 모든 항목이 건너뛰어진 그룹도 생성되지 않는다). 프로젝트에 아직 Addressables 설정 에셋이 없으면, 이를 위해 하나가 생성된다.
  4. 스테이징 목록은 비워진다 — 적용된 것과 건너뛰어진 것 모두 — 그리고 자동 재임포트가 뒤따르므로, 베이크가 새 항목을 보게 된다. 그래서 건너뛰어진 등록은 그 재임포트에서 그것을 필요로 했던 셀에 UnknownAssetKey로 정직하게 보고된다.

콘솔은 결과마다 한 줄을 남긴다 — 적용된 항목마다 Addressables: 'address' → group 'Group', 건너뛴 항목마다 경고로 Addressables: skipped 'address' (reason) — 그리고 요약 줄 Addressables: N registered, M skipped. 로컬 폴더 소스에서는 반영 완료 대화상자가 같은 요약 줄로 끝난다.

시트에 기록되는 드롭다운

선택지가 유한한 컬럼은 시트에 붙는 데이터 검증 규칙을 받는다 — 그래서 Google 시트나 Excel에서 편집하는 사람은 철자를 외우는 대신 목록에서 고를 수 있다. 따로 켤 필요는 없다: 이 규칙은 모든 Export, Push, 작성 반영마다 계산되며, 대상이 이를 담을 수 있는 곳이면 어디든 적용된다.

컬럼규칙
Enum<T> 스칼라그 enum 멤버들의 고정 목록.
참조 스칼라(RecordId@Tab, 그리고 참조 등가성을 가진 커스텀 타입 — §4.4a)대상 탭의 키 컬럼에 걸친 범위이며, 끝을 열어 두어 대상 탭에 추가된 레코드가 스스로 목록에 합류한다.
List<>, 래퍼 컬럼, 키 컬럼 자신규칙 없음 — 그곳의 한 셀은 여러 값을 담거나, 나열할 대상이 없다.
  • 강제가 아니라 안내다. 모든 규칙은 non-strict다(Google strict:false, xlsx showErrorMessage="0"): 목록 밖의 값은 경고 마커가 붙지만 여전히 허용된다. 강제 거부는 "일단 참조를 쓰고 레코드는 나중에 정의한다"는 평범한 워크플로우를 깨뜨릴 것이고, 임포트 자신의 최근접 일치 제안과도 충돌할 것이다.
  • 이 규칙은 표시 메타데이터일 뿐, 값이 아니다. 결코 셀 안에 나타나지 않으므로 라운드트립에 영향이 없으며, 규칙이 없는 export는 이 기능이 있기 전에 만들어진 것과 바이트 단위로 동일하다.
  • 값과 독립적으로 적용된다. 규칙을 붙이는 것은 셀을 쓰는 것의 부작용이 아니라 그 자체로 하나의 단계다 — 가장 흔한 흐름(enum 멤버 추가, 데이터 변경 없음)은 셀을 전혀 전송하지 않으므로, 부작용이라면 결코 실행되지 않을 것이다. 이는 idempotent하므로 다시 실행해도 아무것도 바뀌지 않는다.
  • 실패는 경고일 뿐, push 실패가 아니다. 값은 전송되었는데 규칙만 붙일 수 없었다면, push는 여전히 성공한 것이다 — 다시 실행하면 규칙만 다시 적용된다.

각 형식이 담을 수 있는 것:

대상메커니즘참고
Google 시트 (Push / 쓰기 반영)setDataValidation, 하나의 요청으로 배치 처리두 규칙 종류 모두. 참조 범위는 끝 행을 생략하므로, 대상 탭이 커지는 것을 따라간다.
xlsx (Export)시트 데이터 뒤의 dataValidations두 규칙 종류 모두. 내보내기가 하나의 워크북이므로, 참조 범위는 같은 파일 안의 대상 시트 키 컬럼을 가리키며, 시트를 따라 끝을 열어 둔다 — Google 범위가 갖는 것과 같은 의미다. 규칙은 여전히 세 가지 정직한 경우에 생략되며, 경고에 그 이름이 함께 실린다: 멤버에 콤마가 포함된 경우(인라인 구분자가 그것을 갈라놓을 것이다), 인라인 목록이 255자 스펙 한도(형식이 제한하는 것은 따옴표로 묶인 목록 전체다)를 넘는 경우, 그리고 규칙이 범위인데 그 대상 탭이 워크북 안에 없는 경우.
TSV / CSV (Export)일반 텍스트에는 이를 담을 곳이 없다.
JSON (Export)데이터 파일이지 스프레드시트가 아니다 — 드롭다운을 붙일 셀 자체가 없다.

빠진 것은 실행마다 하나의 DropdownNotSupportedByFormat 경고로 정직하게 보고된다 — 영향을 받는 모든 컬럼의 이름을 알려주므로, "왜 Google에는 드롭다운이 있는데 내 파일에는 없는가?"의 답은 미스터리가 아니라 리포트 안에 있다. 이것이 에러가 아니라 경고인 이유는 값 자체는 온전히 export되었고, 오직 편집 편의성만 빠졌기 때문이다.

gid 맵

ExportUrl 모드에서만 사용된다. 각 항목은 탭 이름을 시트의 #gid= 값(탭이 선택되었을 때 브라우저 URL에서 보이는 값)에 매핑한다. 설정 인스펙터는 관련이 있을 때만 이 맵을 보여준다.

그 숫자들을 브라우저에서 하나씩 옮겨 적을 필요는 없다. 설정 에셋의 인스펙터에는 맵을 대신 채워주는 Google Sheets 섹션이 있다.

  • ExportUrl 모드에서는 Autofill gid from live가 라이브 스프레드시트의 탭 목록을 읽어 맵 전체를 그로부터 다시 작성한 다음 설정 에셋을 저장한다.
  • SheetsApi 모드에서는 같은 패널이 대신 Fetch live tab list를 제공하는데, 이는 그저 시트가 현재 가진 탭을 보여줄 뿐이다. 그 모드는 gid를 스스로 발견하므로 맵이 전혀 필요 없다.

한 가지 유의점: ExportUrl 임포트 자체는 그렇지 않지만, autofill은 Sheets API와 통신하므로 서비스 계정 키가 설정되어 있어야 한다. 키가 없으면 절반만 채워진 맵을 기록하는 대신 멈추고 이를 알린다.

빌드 최신성 훅 — 오래된 베이크는 빌드를 실패시킨다

모든 빌드 이전에, 빌드 전 훅은 커밋된 생성 Database 타입마다 다음 세 가지를 확인한다:

  • (i) 베이크된 SO가 존재하는지;
  • (ii) 스키마 핑거프린트가 baseline과 일치하는지;
  • (iii) Addressables 등록이 존재하는지.

무엇이든 실패하면 조치 가능한 문장과 함께 빌드를 중단시킨다(예: "Tools/SheetForge/Data Studio를 열고, ↓ Pull from source를 누른 다음 빌드하라"). 바로 이것이 "베이크된 SO는 gitignore된다"를 안전하게 만드는 이유다: 클론 또는 CI 머신은 물리적으로 빈 캐시를 배포할 수 없다.

관련 페이지