CSV - Json Parser
CSV -> Json -> C++ 파이프라인
팀 프로젝트 TextRPG에서
CSV -> Json으로 파싱한 후
그것을 C++로 읽어 들이면 데이터 기반 프로그래밍이 되지 않을까 싶어
간략하게 ChapGPT의 힘을 빌려 간단하게 제작하는 것이 이번 목표였다
그리고 이번 제작 중 각종 이슈가 있었기에
가볍게 TIL을 남겨볼까 한다
선요약
-
내부의 ‘코어 프로젝트’는 UTF-8(BOM) (std::string)을 고정하되
OS/ 콘솔 / 파일 경계 등 필요할 때 UTF-16(std::wstring)으로 변환 - 소스파일(h/cpp)의 인코딩은 UTF-8로 저장(보통 65001)
MSVC (Microsoft Visual C++ 컴파일러) 에게 /utf-8 명령어를 사용
(프로젝트 속성 → C/C++ → 명령줄 → 추가 옵션: /utf-8)
- BOM: 파일 맨 앞의 마커. UTF-8(EF BB BF), UTF-16LE(FF FE), UTF-16BE(FE FF)
- BOM: 파일 맨 앞의 마커. UTF-8(EF BB BF), UTF-16LE(FF FE), UTF-16BE(FE FF)
-
콘솔은 SetConsoleOutputCP(CP_UTF8); 후 std::cout만 쓴다(혼용 금지)
(같이 사용하면 출력시 글자가 ‘깨지기’ 쉬움) - 입력 파일에서 BOM 감지한 후, 제거하여 UTF-8 정규화 후 파싱
CSV->Json->C++ 파이프라인 제작 이유? (들어가기 앞서)
기본적으로 ‘기획자 / 밸런스 디자이너’분들과의 협업을 고려한 방식이다
- CSV (스프레드 시트)가 접근성이 좋으며 익숙함
- 게임 코드 쪽에서 Json 기반이 파싱하기 쉬운 편
그렇기에 ‘툴’을 이용하여
기획 측 데이터(CSV) -> 게임 쪽 사용 데이터(Json) -> 실제 게임(C++)로 이용
장점?
-
비개발자 친화
(CSV만 수정하면 되기에 비개발자가 C++ 코드를 만질 필요 x)
(데이터 수정에 따른 재빌드가 없어짐) -
다양한 타이밍의 검증이 가능
(데이터 입력 실수 -> 데이터 수치가 이상 -> 실제 인게임에서 밸런스 테스트 하며 검증)
(단순한 실수 등을 ‘에러’ 반환으로 피드백이 가능) -
데이터 버전 관리
(CSV,Json 파일의 변경까지 Git으로 관리 가능)
인코딩과 문자열
구분 | 내용 | 언제 쓰나 | 장점 | 주의점 |
---|---|---|---|---|
UTF-8 | 가변 길이, ASCII 호환 | CSV/JSON/네트워크/내부 데이터 | 이식성·툴 호환 최고 | 콘솔/OS 호출 시 변환 필요 |
UTF-16 | 2바이트 기반(Win 친화) | Win API, UE/Unity, 한글 경로/콘솔 | 한글 확실, Win API 직결 | 크로스플랫폼 부담, 메모리↑ |
std::string |
바이트 컨테이너(보통 UTF-8) | 프로젝트 코어 전반 | 단순, 생태계 친화 | 출력/OS 경계에서 변환 필요 |
std::wstring |
Windows에선 UTF-16 | 파일경로/콘솔/GUI 등 Win W API | 한글 안전, Win API 호환 | 팀 협업·이식성 저하 가능 |
-
Windows의 파일 경로/ 콘솔/ GUI / 레지스트리 등의 Win32 W API 호출 시
Wstring 사용을 고려해야 함
(+ 엔진이 UTF-16 기반일때도 (UE/Unity)) -
CSV/Json/ 네트워크/ 직렬화 등의 일반적인 데이터 로직에선
string (UTF-8) 고려
다행히도
게임 엔진 쪽에서는 ‘엔진이 제공하는 문자열 타입 및 IO’ 사용 시
이러한 인코딩을 직접 신경쓸 일은 별로 없음
엔진 | 주 문자열 타입 | 내부 인코딩/개념 | 개발자가 보통 쓰는 것 | 변환을 신경쓸 타이밍 |
---|---|---|---|---|
Unreal Engine | FString /FText /TCHAR |
플랫폼 추상(윈도우에선 사실상 UTF-16계열) | TEXT("…") , FString , UE_LOG , FFileHelper::LoadFileToString 등 |
OS API 직접 호출, Raw char* 라이브러리(UTF-8)와 연동, 네트워크/툴에서 바이트 다룰 때 |
Unity | C# string |
UTF-16 | File.ReadAllText(path, Encoding.UTF8) , JsonUtility /Newtonsoft |
네이티브 플러그인(C/C++)와 마샬링할 때, 바이너리/바이트 버퍼 직접 다룰 때 |
기타(사내/커스텀) | 엔진 타입(각자 다름) | 대개 문자열 추상 제공 | 엔진의 FS/로깅/리소스 API | OS·서드파티·툴과 “경계”에서 |
그래도 다음과 같은 상황에서는 고려해볼 것
- OS 의 API를 직접 호출하거나
- 외부 라이브러리 / 플러그인 사용(char* = UTF-8 기대) : 엔진 문자열과 인코딩이 다를 수 있음
- CSV/Json 같은 텍스트 파일을 raw 바이트로 직접 읽을 때 : 파일의 인코딩이 여러가지 섞여있을 수 있음
(요번처럼 BOM 감지 후, UTF-8 로 정규화 후 파싱 권장) - 콘솔/로그 를 엔진 바깥으로 내보낼때
이슈와 해결
-
- 한글이 깨지는 경우
- 콘솔, vs 내부에서 표시 인코딩이 ANSI 되었다
처음에는 데이터를 잘못 파싱한줄 알았으나
‘표시 인코딩’의 불일치로 인한 것
- 한글이 깨지는 경우
-
- 일부 문자열이 출력이 스킵되는 경우
- cout와 wcout의 혼용으로 인한 문제
처음에는 ‘한글’을 wstring으로 저장하였기에
해당 부분을 출력하고자 wcout와 cout 를 같이 사용했었음
- 일부 문자열이 출력이 스킵되는 경우
=> SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8); 를 사용하여 해결
(+ MSVC - /utf-8 명령어 추가)
(원래는 ANSI 경로 방식이 아닌
WIDE 방식을 사용하는 것이 정석)
ANSI 경로 vs Wide 경로
구분 | 의미 | 대표 API/스트림 | 인코딩 해석 |
---|---|---|---|
ANSI 경로 | char 기반, 코드페이지에 의존 |
WriteConsoleA , ReadConsoleA , CreateFileA , printf , scanf , std::cout/std::cin |
현재 코드페이지(CP) 로 바이트를 문자로 해석 |
Wide 경로 | wchar_t (UTF-16) 기반 |
WriteConsoleW , ReadConsoleW , CreateFileW , std::wcout/std::wcin |
UTF-16 직통 (코드페이지 무관) |
결과
- CSV
- Json
[{"Effect":"Heal","Idx":"1","Name":"회복포션","Type":"Consume","Value":"50"},
{"Effect":"Refrain","Idx":"2","Name":"마나포션","Type":"Consume","Value":"30"}]
- C++
void DataManager::LoadItemsJson(const JsonValue& root)
{
ItemDataVector.clear();
// 우리가 쓰는 스키마: 최상위가 배열
if (root.type != JsonValue::Type::Array)
return;
ItemDataVector.reserve(root.arr.size());
for (const auto& obj : root.arr) {
if (obj.type != JsonValue::Type::Object)
continue;
ItemBase it;
const JsonValue* pIdx = obj.get("Idx");
if (pIdx)
{
if (pIdx->type == JsonValue::Type::String)
it.idx = static_cast<int>(std::strtol(pIdx->str.c_str(), nullptr, 10));
else if (pIdx->type == JsonValue::Type::Number)
it.idx = static_cast<int>(pIdx->number);
}
const JsonValue* pName = obj.get("Name");
if (pName && pName->type == JsonValue::Type::String)
it.name = (pName->str);
const JsonValue* pEffect = obj.get("Effect");
if (pEffect && pEffect->type == JsonValue::Type::String)
it.effect = (pEffect->str);
const JsonValue* pType = obj.get("Type");
if (pType && pType->type == JsonValue::Type::String)
{
it.type = ParseItemType(pType->str);
}
const JsonValue* pValue = obj.get("Value");
if (pValue)
{
if (pValue->type == JsonValue::Type::String)
it.value = static_cast<int>(std::strtol(pValue->str.c_str(), nullptr, 10));
else if (pValue->type == JsonValue::Type::Number)
it.value = static_cast<int>(pValue->number);
}
ItemDataVector.push_back(it); // 이후 Move를 통해 실제 관리할 Manager가 가져감
}
}
- 실제 출력
출력 코드
void ItemManager::PrintAllItems()
{
for (auto& item : ItemDatas)
{
std::cout << "==========================" << '\n';
std::cout << "아이템 이름 : " << item.name << '\n';
std::cout << "아이템 효과 : " << item.effect << '\n';
std::cout << "아이템 수치 : " << item.value << '\n';
std::cout << "아이템 인덱스 : " << item.idx << '\n';
}
}
여담
구체적으로 데이터가 깨지는 원인을 찾아보자
- 일단 핵심적인 요소는 크게 2개
-
- C/C++의 명령줄 ‘/utf-8’
- 소스 코드 파일(h,cpp)을 UTF-8로 해석,
또한 내부의 실행 문자셋(“리터럴”)을 UTF-8로 해석
- C/C++의 명령줄 ‘/utf-8’
-
- Main의 ‘SetConsoleOutputCP(CP_UTF8);’
- 콘솔의 출력하는 코드 페이지를 UTF-8로 설정
(콘솔이 출력할 문자열 등을 UTF-8로 해석하여 출력한다)
- Main의 ‘SetConsoleOutputCP(CP_UTF8);’
-
두 옵션 모두 적용하지 않은 경우
-
입력한 문자열이 깨지는 모습이다
현재 데이터 매니저 쪽에서는 json 파싱 데이터를
utf-8 인코딩으로 받아 들이는 상황 -
그런데 출력하는 콘솔쪽의 코드 페이지는
(ANSI : CP949) 이므로
UTF-8로 인코딩한 ‘한글’ 데이터가 깨지는 모습이다
/utf-8 명령만 적용한 경우
-
이번엔 영어와 숫자 말곤 깨져버린다!
-
현재 내부의 ‘모든 리터럴’ 문자열과
데이터 모두 UTF-8로 인코딩이 되어 있는데
콘솔 출력은 여전히 ANSI 이기에
죄다 깨져버리는 상황이다…
SetConsoleOutputCP(CP_UTF8) 만 적용한 경우
-
이번에는 입력 데이터들은 괜찮은데 다른 한글이 깨진다?!
-
이미 예상하였듯이
이번에는 ‘리터럴’ 문자열들이 문제가 된다
void ItemManager::PrintAllItems() const
{
for (const auto& pair : itemMapByIdx)
{
const ItemData& item = pair.second;
std::cout << "==========================" << '\n';
std::cout << "아이템 이름 : " << item.name << '\n';
std::cout << "아이템 효과 : " << item.effect << '\n';
std::cout << "아이템 수치 : " << item.value << '\n';
std::cout << "아이템 인덱스 : " << item.idx << '\n';
std::cout << "아이템 가격 : " << item.price << '\n';
std::cout << "아이템 소모 여부 : " << item.isConsumable << '\n';
std::cout << "아이템 스택 여부 : " << item.isStackable << '\n';
}
}
-
해당 리터럴 문자열들은 기본적으로 ANSI로 인코딩이 되어있다
(사실 리터럴과 콘솔 페이지의 Default가 ANSI 인코딩이다) -
‘입력 데이터’들은 UTF-8, 그리고 콘솔의 코드 페이지도 UTF-8이라
입력 데이터들만 괜찮은 상황이다
TMI : 혹시 ANSI로 csv-json-cpp 파싱을 했으면 그냥 이런 설정 안해도 되는거 아냐?
-
Json은 유니코드 데이터를 가정하기에
ANSI로 파싱하게 되면 많은 라이브러리들과 비호환이 발생 가능 -
물론 여기서는 별도의 라이브러리 대신
GPT의 파싱 코드를 사용하였지만
ANSI로 파싱하려면 추가적인 별도 구현이 필요 -
또한 Git 등의 버전 관리 툴 등도 유니코드가 전제되기에
이러면 데이터들이 Git에서 볼때 깨진다…
최종본(Final)
-
결국 ‘데이터’의 인코딩과
‘리터럴’, 그리고 ‘콘솔’의 코드 페이지 까지
전부 UTF-8로 통일하고서야 정상적으로 출력이 되는 모습이다 -
인코딩 문제는 게임을 구현할때는 고려하기 힘든 사안이지만
더 편리한 데이터 관리를 위해서는 한번쯤 고려해야하는 문제가 아닐까 싶기도 하다
댓글남기기