[2025_12_10]SpreadSheet -> Json 파싱 툴
2025. 12. 10. 21:13ㆍTIL

시작 계기
기획자님의 요청으로 SpreadSheet에서 Json으로 파싱하는 툴을 제작하게 되었다.
게임 데이터는 구글 스프레드시트로 관리하지만, Unity에서는 JSON 형식이 필요하다. 매번 수동으로 변환하는 것이 번거로워서 자동화 툴을 만들기로 결정.
구현 방식 고민 과정
1차 아이디어: Unity Editor 툴
처음엔 "Unity 내부에 있으면 되지"라는 생각으로
Unity 프로젝트에 종속된 에디터 툴로 만들려고 했다.
Unity 프로젝트에 종속된 에디터 툴로 만들려고 했다.
문제 발견:
- Unity를 실행하는데 시간이 오래 걸림
- 단순 수치 변경이나 파일 추가할 때마다 Unity 로딩 대기
- "이건 너무 비효율적이다..."
2차 아이디어: C# 콘솔 앱
"그럼 Visual Studio에서 콘솔로 실행할 수 있게 만들자!"
또 다른 문제:
- Visual Studio를 켜야 함
- F5 누르고 콘솔창 열어야 함
- 팀원들한테 공유하려면 사용법 설명이 복잡함
최종 결론: WinForms GUI 앱
"그냥 exe에 UI 요소 달아서 만들자!"
장점:
- 더블클릭만 하면 실행
- 직관적인 UI
- Unity/VS 없이도 사용 가능
- 팀원 누구나 쉽게 사용
요구사항 정리
핵심 기능
- 구글 스프레드시트에서 Json으로 변환
- 타입 지정 지원 (int, float, string, enum, bool, null 등)
- Enum 값을 별도 시트에서 관리하고 자동 매핑
- 독립 실행형 exe로 팀원들과 공유 가능
편의 기능
- Enum 시트 URL 저장 (매번 입력 X)
- 저장 폴더 선택
- 독립 실행형 exe로 팀원들과 공유 가능
- 변환 상태 실시간 표시
기술 스택
- 언어: C# (.NET 8)
- UI: Windows Forms (간단하고 빠른 GUI)
- HTTP 통신: HttpClient (구글 시트 다운로드)
- JSON 처리: Newtonsoft.Json (타입 변환)
- CSV 파싱: 직접 구현 (따옴표 처리 포함)
- 배포: Self-contained single-file exe
데이터 구조 설계
메인 시트 구조
1행: 타입 정의 (int, float, string, enum, bool, null)
2행: 컬럼명
3행~: 데이터
Enum 정의 시트 구조
Index | Enum | Value | Name
10001 | Unit_Type | 1 | Warrior
10002 | Unit_Type | 2 | Archer
10011 | Unit_AttackType | 1 | Melee
10012 | Unit_AttackType | 2 | Ranged
왜 이렇게?
- 스프레드시트에는 읽기 쉬운 Name 입력 ("Warrior")
- JSON에는 메모리 효율적인 숫자 저장 (1)
- Enum 정의는 한 곳에서 중앙 관리
핵심 구현 과정
1단계: URL → CSV 변환
구글 스프레드시트는 CSV export 기능을 제공한다:
핵심 원리:
- IndexOf("문자열")은 찾은 위치를 반환
- +문자열길이를 하면 그 다음 위치를 가리킴
- "/d/" = 3글자 -> +3
- "gid=" = 4글자 -> +4
2단계: CSV 파싱 구현
CSV는 쉼표로 구분하지만, 값 안에 쉼표가 있을 수 있다:
파서 구현:
csharp
private string[] ParseCsvLine(string line)
{
var result = new List<string>();
var current = new StringBuilder();
bool inQuotes = false; // 따옴표 안인지 체크
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '"')
{
// 이중 따옴표 처리: "" -> "
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
{
current.Append('"');
i++; // 다음 따옴표 건너뛰기
}
else
{
inQuotes = !inQuotes; // 따옴표 상태 토글
}
}
else if (c == ',' && !inQuotes)
{
// 따옴표 밖의 쉼표만 구분자
result.Add(current.ToString());
current.Clear();
}
else
{
current.Append(c);
}
}
result.Add(current.ToString());
return result.ToArray();
}
테스트:
입력: "Hello, World",123,"Test"
결과: ["Hello, World", "123", "Test"]
3단계: 타입별 변환
csharp
private JToken ParseValueByType(string value, string type)
{
// null 처리
if (string.IsNullOrEmpty(value) || value.ToLower() == "null")
return JValue.CreateNull();
switch (type.ToLower())
{
case "int":
case "int32":
return int.TryParse(value, out int intValue)
? new JValue(intValue)
: new JValue(0);
case "long":
case "int64":
return long.TryParse(value, out long longValue)
? new JValue(longValue)
: new JValue(0L);
case "float":
return float.TryParse(value, out float floatValue)
? new JValue(floatValue)
: new JValue(0f);
case "double":
return double.TryParse(value, out double doubleValue)
? new JValue(doubleValue)
: new JValue(0.0);
case "bool":
case "boolean":
value = value.ToLower();
// "1", "true", "yes", "y" → true
if (value == "true" || value == "1" || value == "yes" || value == "y")
return new JValue(true);
// "0", "false", "no", "n" → false
if (value == "false" || value == "0" || value == "no" || value == "n")
return new JValue(false);
return new JValue(false);
case "string":
case "text":
default:
return new JValue(value);
}
}
4단계: Enum 매핑 시스템
메인 데이터 변환 시 적용:
csharp
private JToken ParseEnumValue(string enumTypeName, string value)
{
// 이미 숫자면 그대로 (1, 2, 3...)
if (int.TryParse(value, out int numericValue))
return new JValue(numericValue);
// Name -> Value 변환 ("Warrior" → 1)
if (enumMappings.ContainsKey(enumTypeName))
{
if (enumMappings[enumTypeName].ContainsKey(value))
{
int mappedValue = enumMappings[enumTypeName][value];
return new JValue(mappedValue);
}
}
// 매핑 실패 시 null
return JValue.CreateNull();
}
```
변환 예시:
스프레드시트 입력: "Warrior"
Enum 매핑: Unit_Type["Warrior"] = 1
JSON 출력: 1
5단계: 설정 저장 (UX 개선)
Enum 시트 URL을 매번 입력하지 않도록 저장:
csharp
private void SaveConfig(string enumUrl)
{
var config = new Dictionary<string, string>
{
{ "enumSheetUrl", enumUrl },
{ "outputFolder", outputFolder }
};
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText("converter_config.json", json);
}
private void LoadConfig()
{
if (File.Exists(configPath))
{
string json = File.ReadAllText(configPath);
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
if (config.ContainsKey("enumSheetUrl"))
txtEnumUrl.Text = config["enumSheetUrl"];
}
}
결과:
- 첫 실행: Enum 시트 URL 입력 -> 자동 저장
- 다음 실행: 저장된 URL 자동 로드
- 팀원들은 처음 한 번만 설정하면 됨
6단계: exe 배포
빌드 명령어:
bash
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
```
옵션 설명:
- -c Release: 최적화된 릴리즈 빌드
- -r win-x64: Windows 64bit 타겟
- --self-contained true: .NET 런타임 포함 (사용자 PC에 .NET 설치 불필요)
- -p:PublishSingleFile=true: 모든 DLL을 하나의 exe에 포함
결과:
- 파일 크기: 약 400kb
- 더블클릭만으로 실행
- .NET 설치 필요 없음
겪었던 문제와 해결
문제 : Enum 변환 방향 착각
초기 구현:
Value -> Name 변환 (1 -> "Warrior")
JSON에 문자열 저장
문제:
- JSON 파일 크기 증가
- Unity에서 enum 파싱 복잡
- 성능 저하
해결:
Name -> Value 변환 ("Warrior" -> 1)
JSON에 숫자 저장
참고 자료
결론
처음 "Unity 툴로 만들면 되겠지"라는 생각에서 시작해서, "콘솔 앱이 낫겠다", "아니다 GUI가 필요하다"까지 사용자 관점에서 계속 고민하며 개선한 과정이 값졌다.
핵심 교훈:
개발자가 아닌 사용자 입장에서 생각하면 더 나은 도구를 만들 수 있다.
'TIL' 카테고리의 다른 글
| [2025_12_12]FSM에서 StatePattern사용 (0) | 2025.12.12 |
|---|---|
| [2025_12_11] 변동된 데이터 테이블 확인의 중요 (0) | 2025.12.11 |
| [2025_12_09] 기획의 방향성 재정립 (0) | 2025.12.09 |
| [2025_12_08]최종 프로젝트에서 팀 약속 및 기획 설정 (0) | 2025.12.08 |
| [2025_12_05]프로토타입 발표 및 최종 팀 빌딩 (1) | 2025.12.05 |