Unity

[Unity] 직렬화 시스템 — [SerializeField], .asset, .meta가 하는 일

2026. 6. 17. 17:55 @CGNY

[Unity] 직렬화 시스템 — [SerializeField], .asset, .meta가 하는 일

들어가며

Unity에서 ScriptableObject를 만들면 .asset 파일이 생긴다. 이 파일을 텍스트 에디터로 열어보면 이런 내용이 보인다:

--- !u!114 &11400000
MonoBehaviour:
  m_Script: {fileID: 11500000, guid: d51f196eb3cf5bb4...}
  m_Name: CATEGORY_ATTACK
  id: 1
  codeName: attack
  displayName: Attack

처음 보면 궁금한 게 많다.

  • MonoBehaviour라고 적혀있나? ScriptableObject인데?
  • guid는 뭔가?
  • [SerializeField]를 붙이면 정확히 어떤 일이 일어나는가?
  • .meta 파일은 왜 생기는가?

이 글에서는 Unity 직렬화 시스템이 무엇을 저장하고, 어디에 저장하고, 어떻게 복원하는지 단계적으로 정리한다.


1. 직렬화가 뭔가

메모리에 있는 객체를 파일로 저장하고, 나중에 다시 복원하는 것이다.

// 메모리에 있는 객체
SkillData fireball = new SkillData();
fireball.damage = 100;
fireball.cooldown = 3.0f;

// 직렬화 = 이 객체의 필드 값을 파일에 저장
// 역직렬화 = 파일에서 값을 읽어서 객체를 복원

Unity는 이 과정을 자동으로 해준다. 규칙에 맞는 필드를 선언만 하면 된다.


2. 뭐가 저장되고 뭐가 안 되나

규칙은 간단하다

public class SkillDataSO : ScriptableObject
{
    // ── 저장됨 ──
    public int damage = 100;                     // public 기본형 → 자동
    public string skillName = "Fireball";        // public string → 자동
    public Sprite icon;                          // Unity Object 참조 → 자동
    public List<string> tags;                    // List<직렬화가능> → 자동

    [SerializeField] private float _cooldown;    // private + 어트리뷰트 → 저장됨

    // ── 저장 안 됨 ──
    private int _cachedDps;                      // private → 무시
    public static int TotalCount;                // static → 무시
    public Dictionary<string, int> dict;         // Dictionary → 미지원
}
조건 저장 여부
public 필드 O
[SerializeField] private O
private (어트리뷰트 없음) X
static X
Dictionary X
delegate / Action X
List<T> (T가 저장 가능) O
[Serializable] 클래스 O

왜 이 규칙인가

Unity 직렬화 시스템은 필드(field) 중심이다. 메서드나 property는 저장 대상이 아니다.

[SerializeField] private int id = 1;    // 필드 → 저장됨. .asset에 들어감.
public int ID => id;                     // 프로퍼티 → 저장 안 됨. getter일 뿐.

property는 코드(getter/setter 메서드)이지 데이터가 아니다. 저장할 수 없다.


3. [SerializeField] vs [Serializable] — 붙이는 대상이 다르다

이 둘이 자주 헷갈린다.

[SerializeField] — 필드에 붙인다

[SerializeField] private int id;          // 이 필드를 저장 대상으로 만든다
[SerializeField] private string codeName;

"private인데 Unity가 저장하게 해줘"라는 의미.

[Serializable] — 클래스/구조체에 붙인다

[System.Serializable]
public class DamageInfo        // 이 타입의 내부 필드까지 저장 가능하게 한다
{
    public int amount;
    public float critRate;
}

public class SkillDataSO : ScriptableObject
{
    [SerializeField] private DamageInfo damageInfo;
    // → DamageInfo 안의 amount, critRate까지 .asset에 저장됨
}
정리:
  필드 하나를 저장 대상으로 → [SerializeField]
  커스텀 타입 내부까지 저장 → [System.Serializable]

4. .asset 파일 안에 뭐가 들어있나

ScriptableObject를 만들면 .asset 파일이 생긴다. 실제로 열어보면:

%YAML 1.1
--- !u!114 &11400000
MonoBehaviour:
  m_Script: {fileID: 11500000, guid: d51f196eb3cf5bb4..., type: 3}
  m_Name: CATEGORY_ATTACK
  categories: []
  icon: {fileID: 21300000, guid: f6e5d4c3b2a10987...}
  id: 1
  codeName: attack
  displayName: Attack
  description: Physical attack skills

한 줄씩 보면:

의미
!u!114 Unity 내부 타입 번호 114 = "사용자 C# 스크립트"
MonoBehaviour: YAML 태그 이름. MB든 SO든 114번은 이 이름을 씀 (역사적 이유)
m_Script: {guid: ...} 이 .asset의 실제 C# 타입을 가리키는 GUID
m_Name 에셋 이름
id: 1 [SerializeField] private int id 의 저장된 값
icon: {guid: ...} 다른 에셋(Sprite)을 GUID로 참조

MonoBehaviour라고 적혀있는데 SO 아닌가?

맞다. 실제 타입은 m_Script의 GUID가 결정한다. MonoBehaviour:는 Unity YAML 포맷이 사용자 스크립트를 통칭하는 태그 이름일 뿐이다.

@startuml
skinparam defaultFontName Malgun Gothic
skinparam backgroundColor #1E1E1E
skinparam defaultFontColor #D4D4D4
skinparam objectBorderColor #569CD6
skinparam objectBackgroundColor #2D2D2D
skinparam packageBorderColor #569CD6
skinparam packageBackgroundColor #252526
skinparam arrowColor #D4D4D4
skinparam noteBorderColor #6A9955
skinparam noteBackgroundColor #2D2D2D
skinparam titleFontColor white
skinparam titleFontSize 20

title .asset 파일 구조

object "CATEGORY_ATTACK.asset" as asset {
  MonoBehaviour: (YAML 태그)
  m_Script: guid → IdentifiedObject.cs
  id: 1
  codeName: "attack"
  icon: guid → fire_icon.png
  categories: []
}

object "IdentifiedObject.cs" as script {
  [SerializeField] int id
  [SerializeField] string codeName
  [SerializeField] Sprite icon
}

object "fire_icon.png" as icon {
  Sprite 이미지 데이터
}

asset -down-> script : m_Script (GUID)
asset -down-> icon : icon 참조 (GUID)

note bottom of asset
  실제 타입은 m_Script GUID가 결정
  MonoBehaviour 태그는 이름만 그렇다
end note

@enduml

5. .meta 파일과 GUID

.meta는 언제 생기나

Unity 프로젝트에 파일이 추가될 때마다 자동 생성된다. 예외 없음.

Category_Attack.asset      ← 에셋 파일
Category_Attack.asset.meta ← 자동 생성된 메타 파일

PlayerController.cs        ← 스크립트 파일
PlayerController.cs.meta   ← 자동 생성

Icons/                     ← 폴더
Icons.meta                 ← 폴더에도 생김

.meta 안에 뭐가 있나

fileFormatVersion: 2
guid: dfa9966517debc542a9ce23e6e10a99b
NativeFormatImporter:
  mainObjectFileID: 11400000

핵심은 guid 한 줄이다. 이 에셋의 영구 신분증.

GUID가 왜 중요한가

Unity는 에셋 참조를 파일명이 아니라 GUID로 연결한다.

SkillData.asset 안에:
  icon: {guid: dfa9966517debc54...}    ← 파일명이 아니라 GUID

fire_icon.png.meta 안에:
  guid: dfa9966517debc54...            ← 같은 GUID

그래서:

파일명 변경: fire_icon.png → attack_icon.png
  → .meta의 GUID는 안 바뀜
  → 참조 유지됨

폴더 이동: Assets/Icons/ → Assets/Art/Icons/
  → .meta가 같이 이동
  → GUID 그대로 → 참조 유지됨

.meta 삭제:
  → Unity가 새 .meta 생성 → 새 GUID 발급
  → 이 파일을 참조하던 모든 곳에서 연결 깨짐!

.meta 파일은 절대 삭제하면 안 되고, 반드시 버전 관리(SVN/Git)에 포함해야 한다.


6. .asset vs .prefab vs .scene

Unity에서 데이터를 저장하는 파일 3종류:

파일 뭘 저장하나 예시
.asset ScriptableObject 인스턴스 Fireball.asset, Category_Attack.asset
.prefab GameObject 구조 (컴포넌트+자식) Player.prefab, Bullet.prefab
.unity 씬 전체 (모든 오브젝트+설정) MainMenu.unity, Level1.unity

[SerializeField] 필드는 그 오브젝트가 어디에 있느냐에 따라 다른 파일에 저장된다:

// MonoBehaviour의 필드
public class PlayerController : MonoBehaviour
{
    [SerializeField] private int maxHP = 100;
}
// → 씬에 있으면 .unity에 저장
// → Prefab이면 .prefab에 저장

// ScriptableObject의 필드
public class SkillDataSO : ScriptableObject
{
    [SerializeField] private int damage = 100;
}
// → 항상 .asset에 저장

7. Instantiate — 직렬화 기반 복사

Instantiate()는 Unity의 복사 함수인데, 내부적으로 직렬화 → 역직렬화로 동작한다.

var clone = Instantiate(original);

// 내부 동작:
// 1. original의 직렬화 데이터를 읽는다
// 2. 새 인스턴스를 만든다
// 3. 읽은 데이터를 새 인스턴스에 쓴다

그래서 직렬화 대상이 아닌 필드는 복사되지 않는다:

public class SkillDataSO : ScriptableObject
{
    [SerializeField] private int damage = 100;   // ✅ 복사됨
    private int _cachedDps = 999;                 // ❌ 복사 안 됨 (기본값 0)
}

복사 규칙 — 타입에 따라 다르다

public class ParentSO : ScriptableObject
{
    [SerializeField] private int level = 10;            // 값 타입
    [SerializeField] private DamageInfo info;            // [Serializable] class
    [SerializeField] private SkillDataSO mainSkill;      // SO 참조
    [SerializeField] private Sprite icon;                // Unity Object 참조
}
필드 타입 복사 결과
level int (값 타입) 독립 복사. 바꿔도 원본 안 바뀜.
info [Serializable] class 새 인스턴스 생성. 독립적.
mainSkill ScriptableObject 참조만 복사. 같은 .asset을 가리킴.
icon Sprite 참조만 복사. 같은 이미지를 가리킴.

핵심 규칙:

UnityEngine.Object를 상속한 타입 → 참조만 복사 (같은 걸 가리킴)
나머지 전부                      → 새로 만들어서 복사 (독립적)

mainSkill을 통해 값을 바꾸면 원본도 영향받는다. 이걸 방지하려면 mainSkill도 별도로 Instantiate하거나 Clone()을 재정의해야 한다.


8. 코드 → 컴파일 → .dll vs 데이터 → 직렬화 → .asset

코드 파이프라인:
  SkillDataSO.cs → [Roslyn 컴파일] → IL → Assembly-CSharp.dll
  "클래스 정의(틀)가 dll에 들어감"
  필드를 추가하려면 → 코드 수정 → 재컴파일

데이터 파이프라인:
  [CreateAssetMenu] → Inspector에서 생성 → Fireball.asset
  "인스턴스의 필드 값이 YAML로 디스크에 저장됨"
  값을 바꾸려면 → Inspector에서 수정 → 재컴파일 불필요

.cs 파일을 수정하면 재컴파일이 필요하지만, .asset 파일의 값을 Inspector에서 바꾸면 재컴파일 없이 즉시 반영된다. 이게 ScriptableObject의 핵심 가치라고 생각한다.


정리

Unity 직렬화 시스템:

  저장 대상 = [SerializeField] 필드 또는 public 필드
  저장 위치 = .asset (SO) / .prefab (Prefab) / .unity (씬)
  참조 방식 = GUID 기반 (.meta 파일에 저장)

  [SerializeField]    → 필드를 저장 대상으로 만든다
  [Serializable]      → 커스텀 타입 내부까지 저장 가능하게 한다
  .meta + GUID        → 파일명/경로가 바뀌어도 참조가 유지된다

  Instantiate 복사:
    UnityEngine.Object → 참조만 복사
    나머지             → 새로 만들어서 복사

  .cs 변경 = 재컴파일 필요
  .asset 변경 = 재컴파일 불필요 (데이터만 바뀜)

참고 자료