AI 보조 게임 개발

Claude Code로 게임 밸런스 CSV -> ScriptableObject 임포터와 검증 테스트 한 번에 뽑아내기

kr-gamedev 2026. 6. 12. 09:03

게임 프로젝트가 커질수록 기획 팀과 개발 팀 사이의 데이터 전달 마찰이 쌓인다. 기획자는 Google Sheets나 Excel에서 적 스탯, 아이템 가격, 스킬 쿨다운을 관리하고, 개발자는 그 CSV를 열어 하나하나 Unity ScriptableObject에 손으로 옮긴다. 중간에 오탈자 하나면 버그다.

더 큰 문제는 이 과정이 반복된다는 점이다. 밸런스 패치가 있을 때마다, 열이 추가될 때마다, 새로운 적 타입이 생길 때마다 임포터 코드와 SO 클래스를 같이 수정해야 한다. 이 반복 작업을 AI 도구에 맡기면 어떻게 되는지 단계별로 살펴본다.

Claude Code는 파일 시스템에 직접 접근해 에디터 스크립트를 작성하고 즉시 프로젝트에 배치할 수 있다. CSV 헤더 한 줄만 넘기면 ScriptableObject 클래스, EditorScript 임포터, EditMode 검증 테스트를 차례로 뽑아준다.


1. 시작점: CSV 헤더를 프롬프트로

기획 팀에서 넘어온 enemy_stats.csv 첫 줄이 다음과 같다고 가정한다.

id,name,hp,atk,def,move_speed,drop_exp,spawn_weight

Claude Code에 이렇게 요청한다.

"아래 CSV 헤더로 Unity ScriptableObject 클래스를 만들어줘. id/name은 string, hp/atk/def/drop_exp는 int, move_speed/spawn_weight는 float. 네임스페이스는 GameData."

// EnemyStatData.cs
using UnityEngine;

namespace GameData
{
    [CreateAssetMenu(fileName = "EnemyStatData", menuName = "GameData/Enemy Stat")]
    public class EnemyStatData : ScriptableObject
    {
        public string id;
        public string enemyName;   // 'name'은 UnityEngine.Object 예약 프로퍼티 -> 자동 리네임
        public int    hp;
        public int    atk;
        public int    def;
        public float  moveSpeed;
        public int    dropExp;
        public float  spawnWeight;
    }
}

여기서 주목할 점은 AI가 name 필드를 enemyName으로 자동 리네이밍한다는 것이다. nameUnityEngine.Object의 예약 프로퍼티라 그대로 쓰면 직렬화가 깨진다. Claude Code는 이 Unity 내부 규칙을 알고 있어 별도 확인 없이도 안전한 코드가 나온다.


2. EditorScript 임포터 자동 생성

SO 클래스가 생겼으면 임포터를 요청한다.

"위 EnemyStatData를 CSV에서 읽어 Assets/Data/Enemies/ 폴더에 일괄 생성하는 Editor 메뉴 아이템 스크립트를 작성해줘. 헤더 첫 줄은 스킵, 행 하나당 에셋 하나."

// Editor/EnemyStatImporter.cs
using System.IO;
using UnityEditor;
using UnityEngine;
using GameData;

public static class EnemyStatImporter
{
    private const string CsvPath   = "Assets/RawData/enemy_stats.csv";
    private const string OutputDir = "Assets/Data/Enemies";

    [MenuItem("GameData/Import Enemy Stats")]
    public static void Import()
    {
        if (!Directory.Exists(OutputDir))
            Directory.CreateDirectory(OutputDir);

        string[] lines = File.ReadAllLines(CsvPath);

        for (int i = 1; i < lines.Length; i++)   // 헤더 스킵
        {
            string[] cols = lines[i].Split(',');
            if (cols.Length < 8) continue;

            var so        = ScriptableObject.CreateInstance<EnemyStatData>();
            so.id         = cols[0].Trim();
            so.enemyName  = cols[1].Trim();
            so.hp         = int.Parse(cols[2]);
            so.atk        = int.Parse(cols[3]);
            so.def        = int.Parse(cols[4]);
            so.moveSpeed  = float.Parse(cols[5]);
            so.dropExp    = int.Parse(cols[6]);
            so.spawnWeight = float.Parse(cols[7]);

            string assetPath = $"{OutputDir}/{so.id}.asset";
            AssetDatabase.CreateAsset(so, assetPath);
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log($"[EnemyStatImporter] 임포트 완료: {lines.Length - 1}개");
    }
}

Unity 상단 메뉴에 "GameData > Import Enemy Stats"가 생기고, 기획자가 시트를 교체할 때마다 클릭 한 번으로 전체 적 스탯이 갱신된다. 개발자가 수동으로 값을 옮기는 단계는 완전히 사라진다.


3. 데이터 검증 EditMode 테스트까지 자동으로

임포터가 있어도 데이터 품질 보증이 없으면 런타임 버그는 계속 터진다. 음수 체력, 0 이동속도, 중복 ID 같은 기획 실수를 CI 단계에서 잡으려면 EditMode 테스트가 필요하다.

"임포트된 EnemyStatData 에셋 전체를 로드해서 hp > 0, moveSpeed > 0, id 중복 없음을 검증하는 EditMode NUnit 테스트를 작성해줘."

// Tests/Editor/EnemyStatValidationTest.cs
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEditor;
using GameData;

public class EnemyStatValidationTest
{
    private static EnemyStatData[] LoadAll()
    {
        string[] guids = AssetDatabase.FindAssets(
            "t:EnemyStatData", new[] { "Assets/Data/Enemies" });
        return Array.ConvertAll(guids, g =>
            AssetDatabase.LoadAssetAtPath<EnemyStatData>(
                AssetDatabase.GUIDToAssetPath(g)));
    }

    [Test]
    public void HP_는_양수여야_한다()
    {
        foreach (var stat in LoadAll())
            Assert.Greater(stat.hp, 0, $"{stat.id}: hp={stat.hp}");
    }

    [Test]
    public void MoveSpeed_는_양수여야_한다()
    {
        foreach (var stat in LoadAll())
            Assert.Greater(stat.moveSpeed, 0f, $"{stat.id}: moveSpeed={stat.moveSpeed}");
    }

    [Test]
    public void ID_는_중복되지_않아야_한다()
    {
        var seen = new HashSet<string>();
        foreach (var stat in LoadAll())
            Assert.IsTrue(seen.Add(stat.id), $"중복 ID 발견: {stat.id}");
    }
}

Unity Test Runner에서 EditMode 탭으로 실행하거나, unity -runTests -testPlatform EditMode 커맨드라인으로 GitHub Actions 등 CI에 붙일 수 있다. 기획자가 잘못된 값을 넣은 CSV를 올리면 PR 단계에서 바로 빨간 불이 켜진다.


4. AI 프롬프트 전략: 재현 가능한 패턴

위 예시에서 핵심은 AI에게 무엇을 어떻게 요청했느냐다. 잘 동작하는 패턴 몇 가지를 정리한다.

스키마 우선 제공: "CSV 헤더가 이렇다. 타입은 이렇다"를 먼저 명시하면 AI가 추측을 줄이고 정확한 클래스를 만든다.

경로를 구체적으로: Assets/RawData/enemy_stats.csv -> Assets/Data/Enemies/ 식으로 경로를 프롬프트에 명시하면 의도한 경로가 하드코딩된다. 나중에 const로 추출하는 것도 AI에게 시키면 된다.

Unity 버전 명시: "Unity 2022.3 LTS, C# 10" 처럼 버전을 박으면 최신 언어 기능과 deprecated API 충돌을 피할 수 있다.

단계별로 나눠서 요청: ScriptableObject 클래스, 임포터, 테스트를 한 번에 요청하면 출력이 길어지고 오류가 섞인다. 각 단계를 확인한 뒤 다음을 요청하는 것이 실제로 더 빠르다.


마무리

게임 데이터 파이프라인의 보일러플레이트는 AI 도구가 가장 잘 처리하는 영역 중 하나다. ScriptableObject 클래스 정의, EditorScript 임포터, EditMode 검증 테스트는 각각 수십 분짜리 작업이지만, AI에게 단계별로 맡기면 합산 10분 이내로 줄어든다.

더 중요한 것은 재사용성이다. 아이템, 스킬, 퀘스트 등 새로운 데이터 시트가 생길 때마다 CSV 헤더를 바꿔 같은 프롬프트를 재사용하면 된다. 클래스 구조가 바뀌어도 AI에게 "기존 임포터를 이 스키마에 맞게 수정해줘"라고 하면 차이점만 반영해준다. 반복 작업의 부담이 AI로 이전되는 만큼, 개발자는 실제 게임 로직에 집중할 시간이 늘어난다.