유니티 입문2026년 03월 06일· 9 min read

28. Prefab Variant로 공통 설정 유지하며 개별 오버라이드 적용하기

Prefab Variant로 공통 프리팹 설정을 유지하면서 개별 오버라이드를 적용하는 방법을 엔진 직렬화, 오버라이드 저장 구조, Player Loop 관점에서 설명한다. 실습 포함. 154자 내외 조정됨: Prefab Variant로 공통 설정을 유지하면서 개별 오버라이드를 적용하는 방법을 엔진 직렬화와 오버라이드 저장

28. Prefab Variant로 공통 설정 유지하며 개별 오버라이드 적용하기

Prefab Variant로 공통 설정 유지하며 개별 오버라이드 적용하기

게임 만들다 겪는 사고로 가장 흔한 게 ‘적 프리팹 하나 고쳤는데 어떤 적은 바뀌고 어떤 적은 그대로’ 같은 불일치다. 씬에 배치된 오브젝트는 이미 오버라이드가 쌓여 있고, 누군가는 Apply를 눌렀고, 누군가는 Variant를 안 쓰고 복제해 버린다. 이 상태에서 밸런스 패치가 들어오면, 수정 1번이 아니라 ‘누락된 파생본 찾기’ 작업이 된다. Prefab Variant는 이 불일치를 엔진이 저장하는 오버라이드 구조로 해결한다. 왜 Variant가 안전한지, 어디에 저장되는지, 어떤 변경이 깨지는지까지 엔진 관점으로 파고든다.

핵심 개념

Prefab Variant는 ‘기본 프리팹(베이스)’을 참조하면서, 바뀐 값만 별도 데이터로 저장하는 파생 에셋이다. 복제(prefab duplicate)는 베이스와의 연결이 끊긴 독립 에셋이라서, 나중에 공통 수정이 들어오면 사람이 찾아서 고쳐야 한다. Variant는 공통 수정이 베이스에 들어오면 파생본이 자동으로 따라오고, 파생본의 오버라이드만 유지된다.

용어를 정확히 잡아야 디버깅이 된다. 1) Base Prefab: 원본 프리팹 에셋. 2) Variant Prefab: Base를 부모로 갖는 프리팹 에셋. 3) Instance Override: 씬에 배치된 인스턴스에서 생긴 오버라이드(씬 파일에 저장). 4) Property Modification: 어떤 컴포넌트의 어떤 프로퍼티가 바뀌었는지 기록하는 단위(네이티브 오브젝트 ID + 프로퍼티 경로 + 값). 5) Apply/Revert: 오버라이드를 에셋으로 밀어 넣거나 되돌리는 에디터 동작.

왜 Variant가 필요한가. 실무에서 ‘공통 설정’은 생각보다 넓다. 레이어/태그, Rigidbody 제약, 공통 이펙트 소리, 공통 스크립트 필드(예: 체력 계산식), 렌더러 머티리얼 슬롯 같은 것들이 팀 단위로 바뀐다. Variant 없이 복제만 하면, 공통 수정이 들어올 때마다 파생 프리팹 수만큼 누락 위험이 생긴다. Variant는 누락 위험을 데이터 구조로 줄인다.

오버라이드가 많아질수록 유지보수가 어려워지는 이유도 엔진 저장 방식 때문이다. Unity는 ‘전체 스냅샷’을 저장하지 않고 ‘변경된 프로퍼티 목록’을 저장한다. 목록이 커지면 Apply/Revert 판단이 복잡해지고, 어떤 값이 왜 바뀌었는지 추적이 어려워진다. Variant는 공통을 베이스로 밀어 넣고 파생은 최소 오버라이드만 남기는 방향으로 구조를 강제한다.

VariantProbe.cs
1using UnityEngine;
2
3public class VariantProbe : MonoBehaviour
4{
5    [Header("Base에서 공통으로 바꾸는 값")]
6    public float baseMoveSpeed = 5f;
7
8    [Header("Variant에서만 바꾸는 값")]
9    public Color tint = Color.white;
10
11    private Renderer cachedRenderer;
12
13    private void Awake()
14    {
15        cachedRenderer = GetComponentInChildren<Renderer>();
16        Debug.Log($"Awake: speed={baseMoveSpeed}, tint={tint}, name={name}");
17    }
18
19    private void Start()
20    {
21        if (cachedRenderer != null)
22        {
23            cachedRenderer.material.color = tint;
24        }
25        Debug.Log($"Start: materialColor={tint}");
26    }
27}

이 스크립트를 Base Prefab에 붙이고, Variant Prefab에서 tint만 바꾼 뒤 Base의 baseMoveSpeed를 바꾸면 콘솔 로그가 ‘speed는 따라오고 tint는 유지’ 형태로 찍힌다. 같은 씬에서 Base 인스턴스와 Variant 인스턴스를 같이 띄우면, 어떤 값이 에셋 레벨 오버라이드인지(Variant) 씬 레벨 오버라이드인지(Instance) 감이 잡힌다.

엔진 관점에서의 내부 동작

Prefab Variant의 핵심은 런타임이 아니라 에디터 직렬화 파이프라인에 있다. 프리팹 에셋은 YAML(텍스트 직렬화)로 저장되고, Variant는 ‘부모 프리팹 GUID’와 ‘수정 목록’을 가진다. 씬에 배치된 인스턴스도 마찬가지로 ‘어떤 프리팹을 인스턴싱했는지’와 ‘추가로 바뀐 프로퍼티 목록’을 가진다. 런타임에서는 이미 머지된 결과가 로드되기 때문에, 플레이 중에는 Variant라는 개념이 거의 드러나지 않는다.

C#에서 Prefab을 만질 때는 UnityEditor.PrefabUtility 같은 API를 쓰지만, 실제 데이터는 네이티브(C++) 오브젝트 그래프에 붙어 있다. C# 오브젝트(UnityEngine.Object)는 네이티브 오브젝트를 가리키는 핸들(InstanceID 기반)이고, Inspector에서 값을 바꾸면 SerializedObject/SerializedProperty를 통해 변경이 기록된다. 이 변경 기록이 Prefab의 Property Modification으로 내려가면서 ‘오버라이드’가 된다.

Player Loop 관점에서 Prefab Variant는 어디에 끼어드나. 플레이 모드에 들어가기 직전(Enter Play Mode)과 씬 로드 시점에, 에셋 데이터가 네이티브 씬 오브젝트로 디시리얼라이즈 된다. 그 다음에 MonoBehaviour 메시지 호출 순서(Awake → OnEnable → Start)가 이어진다. Variant 머지는 ‘Awake 전에 끝나 있어야’ 한다. Awake에서 읽는 필드 값이 머지 전이면 프레임마다 값이 바뀌는 이상한 현상이 생기는데, Unity는 그런 상태를 허용하지 않는다.

메모리 관점에서 보면, Prefab 머지는 에디터/로딩 시점에 임시 데이터 구조를 만든다. YAML에서 읽은 필드 값은 네이티브 직렬화 버퍼를 거쳐 C# 필드로 복원된다. Variant의 오버라이드가 많을수록 ‘프로퍼티 경로 문자열’과 ‘수정 레코드’가 늘고, 에디터에서는 Apply/Revert UI를 그리기 위해 이 목록을 계속 비교한다. 그래서 오버라이드가 수천 개로 커진 프리팹은 Inspector 클릭만으로도 스파이크가 난다.

왜 이런 API 설계가 되었나. Unity는 컴포넌트 기반이고, 씬/프리팹은 ‘오브젝트 그래프 직렬화’가 본체다. 전체를 매번 복제해서 저장하면 에셋 크기와 머지 비용이 커진다. 변경만 저장하면 디스크 용량과 머지 범위를 줄일 수 있다. Variant는 그 철학을 ‘에셋 상속’ 형태로 제공한 기능이다. C# 언어 상속이 아니라 직렬화 데이터 상속이라서, 필드 추가/삭제나 컴포넌트 순서 변경 같은 구조 변경에서 민감해진다.

처음에 나도 ‘왜 Variant에서 Base 컴포넌트 값을 바꾸면 가끔 오버라이드가 풀릴까’를 몰랐다. 콘솔에는 아무 에러도 안 뜨고, Prefab Override 창에서만 조용히 항목이 사라졌다. 3시간 삽질 끝에 알아낸 건, 컴포넌트의 필드 경로가 바뀌면 Property Modification이 더 이상 적용 대상을 못 찾는다는 점이다. 오버라이드는 ‘컴포넌트 타입 + 순서 + 프로퍼티 경로’에 강하게 의존한다.

PlayerLoopOrderLogger.cs
1using UnityEngine;
2
3public class PlayerLoopOrderLogger : MonoBehaviour
4{
5    private void Awake()
6    {
7        Debug.Log($"{name} Awake frame={Time.frameCount}");
8    }
9
10    private void OnEnable()
11    {
12        Debug.Log($"{name} OnEnable frame={Time.frameCount}");
13    }
14
15    private void Start()
16    {
17        Debug.Log($"{name} Start frame={Time.frameCount}");
18    }
19
20    private void Update()
21    {
22        if (Time.frameCount == 1)
23            Debug.Log($"{name} Update frame={Time.frameCount}");
24    }
25}

이 코드를 Base Prefab에 넣고, Variant 인스턴스를 씬에 배치한 뒤 Play를 누르면 콘솔에 Awake/OnEnable/Start가 첫 프레임 전에 순서대로 찍힌다. 여기서 확인하는 포인트는 ‘Variant 머지가 완료된 상태에서 Awake가 돈다’는 사실이다. Awake에서 읽는 public 필드는 이미 Base+Variant(+Instance) 오버라이드가 적용된 최종값이다.

Enemy_Base.prefab (예시 YAML 일부)
1%YAML 1.1
2%TAG !u! tag:unity3d.com,2011:
3--- !u!1 &100000
4GameObject:
5  m_Name: Enemy_Base
6  m_Component:
7  - component: {fileID: 11400000}
8--- !u!114 &11400000
9MonoBehaviour:
10  m_Script: {fileID: 11500000, guid: 0123456789abcdef0123456789abcdef, type: 3}
11  baseMoveSpeed: 5
12  tint: {r: 1, g: 1, b: 1, a: 1}

텍스트 직렬화를 켜면(Project Settings → Editor → Asset Serialization → Force Text) 프리팹이 이런 형태로 보인다. Variant 프리팹은 여기에 ‘부모 프리팹 참조’와 ‘수정된 필드만’ 추가로 기록된다. 실제 파일 구조는 버전마다 다르지만, 핵심은 ‘변경 목록 기반’이라는 점이다. 그래서 필드명 변경이나 컴포넌트 재정렬이 오버라이드를 흔든다.

실습하기

1단계: 프로젝트 설정

Unity 2022.3 LTS(또는 2021.3 LTS)에서 3D(Core) 템플릿으로 새 프로젝트를 만든다. Project Settings → Editor → Asset Serialization을 Force Text로 바꾼다. Git을 쓰는 팀이면 이 옵션이 Prefab 머지 충돌을 줄이는 쪽으로 작동한다.

Project 창에서 폴더를 만든다. Assets/Prefabs, Assets/Scenes, Assets/Scripts. 씬은 File → New Scene로 만든 뒤 Assets/Scenes/VariantLab.unity로 저장한다. 실습 중 프리팹이 깨졌을 때 되돌리기 쉬운 구조가 된다.

2단계: 씬 구성과 Base Prefab 만들기

Hierarchy 우클릭 → 3D Object → Capsule을 만든다. 이름을 Enemy_Base_Root로 바꾼다. Inspector에서 Capsule Collider, Mesh Renderer가 붙어 있는지 확인한다. 그 다음 Add Component로 VariantProbe, PlayerLoopOrderLogger를 추가한다.

VariantProbe 컴포넌트에서 baseMoveSpeed를 5, tint를 흰색으로 둔다. 이 오브젝트를 Project의 Assets/Prefabs 폴더로 드래그해서 Enemy_Base.prefab을 만든다. Hierarchy에 남아 있는 인스턴스는 씬 오브젝트이고, Project에 생긴 것은 에셋이다.

EnemyMover.cs
1using UnityEngine;
2
3public class EnemyMover : MonoBehaviour
4{
5    public float speed = 5f;
6    public Vector3 axis = Vector3.right;
7
8    private Vector3 startPos;
9
10    private void Awake()
11    {
12        startPos = transform.position;
13    }
14
15    private void Update()
16    {
17        float t = Mathf.Sin(Time.time) * 2f;
18        transform.position = startPos + axis * t * speed * 0.1f;
19    }
20}

EnemyMover는 ‘공통 이동 로직’ 역할이다. 이걸 Base에 붙여두면 Base 설정을 바꿀 때 파생들이 같이 흔들리는지 Game 뷰에서 바로 보인다. Play를 누르면 캡슐이 좌우로 흔들리고, speed 값이 바뀌면 흔들림 폭이 즉시 달라진다.

3단계: Prefab Variant 생성, 오버라이드 확인, Apply/Revert 테스트

Project 창에서 Enemy_Base.prefab을 우클릭 → Create → Prefab Variant를 선택한다. 이름을 Enemy_Fast_Variant.prefab으로 만든다. 그 다음 씬에 Enemy_Fast_Variant를 드래그한다. Inspector 상단에 Prefab Variant 표시가 뜨고, Overrides 드롭다운이 활성화된다.

씬에 배치된 Enemy_Fast_Variant 인스턴스를 선택하고, Inspector에서 VariantProbe의 tint를 빨간색으로 바꾼다(이 변경은 Variant 에셋에 적용하려면 Apply가 필요하다). Overrides → Apply All을 누르면, Project의 Enemy_Fast_Variant 에셋에 오버라이드가 저장된다. 같은 인스턴스에서 EnemyMover.speed를 12로 바꾸고 Overrides 목록을 열면 ‘씬 인스턴스 오버라이드’로 잡힌다. 이 상태에서 Revert All을 누르면 speed만 되돌아가고 tint는 유지된다(Variant 에셋에 이미 저장했기 때문).

OverrideVisualizer.cs
1using UnityEngine;
2
3public class OverrideVisualizer : MonoBehaviour
4{
5    public VariantProbe probe;
6    public EnemyMover mover;
7
8    private void Reset()
9    {
10        probe = GetComponent<VariantProbe>();
11        mover = GetComponent<EnemyMover>();
12    }
13
14    private void OnGUI()
15    {
16        if (probe == null || mover == null) return;
17        GUI.Label(new Rect(10, 10, 600, 22), $"{name} speed={mover.speed} baseMoveSpeed={probe.baseMoveSpeed} tint={probe.tint}");
18    }
19}

OverrideVisualizer를 Base Prefab에 추가하고, 씬에서 Base 인스턴스와 Variant 인스턴스를 동시에 띄운다. Play 모드에서 좌상단에 speed/baseMoveSpeed/tint가 찍힌다. Base 프리팹 에셋에서 baseMoveSpeed를 8로 바꾸고 저장하면, Play 재시작 후 두 인스턴스 모두 baseMoveSpeed가 8로 찍힌다. Variant 에셋에서 tint를 파란색으로 바꾸면, Variant 인스턴스만 tint가 바뀌고 Base 인스턴스는 흰색으로 남는다.

심화 활용

한 문단 요약: Base는 ‘공통 규칙’을 담고 Variant는 ‘예외’만 담는다. 오버라이드가 늘어나는 순간부터는 데이터가 아니라 부채가 된다. Apply/Revert는 저장 위치를 바꾸는 동작이고, 저장 위치가 바뀌면 팀 작업 충돌 지점도 바뀐다.

패턴 1: 역할 기반 Base + 스펙 기반 Variant(적/무기/스킬 공통)

적을 예로 들면 Base에는 충돌 레이어, 공통 애니메이션 컨트롤러, 공통 히트박스 구조, 공통 스크립트 컴포넌트 구성을 넣는다. Variant에는 체력/이동속도/드랍테이블 같은 밸런스 파라미터만 남긴다. 이렇게 나누면 ‘구조 변경’은 Base에서만 일어나고, Variant는 값 변경만 하게 된다. 구조 변경은 오버라이드 깨짐 리스크가 크기 때문에, 발생 지점을 줄이는 게 목표다.

EnemyStats.cs
1using UnityEngine;
2
3[DisallowMultipleComponent]
4public class EnemyStats : MonoBehaviour
5{
6    [Min(1)] public int hp = 10;
7    [Min(0f)] public float moveSpeed = 5f;
8    public int exp = 1;
9
10    public void ApplyTo(EnemyMover mover, VariantProbe probe)
11    {
12        if (mover != null) mover.speed = moveSpeed;
13        if (probe != null) probe.baseMoveSpeed = moveSpeed;
14    }
15}

EnemyStats를 Base에 넣고, Base에서는 기본값만 둔다. Variant에서는 hp/moveSpeed/exp만 바꾼다. 실행하면 EnemyMover의 흔들림 폭이 Variant마다 달라진다. 여기서 중요한 점은 ‘컴포넌트 추가/삭제’를 Variant에서 하지 않는 습관이다. 컴포넌트 구조를 Variant에서 바꾸기 시작하면, Base 구조 변경 시 오버라이드 목록이 폭증한다.

패턴 2: 프리팹 구조는 Base에서 고정, 렌더링만 Variant로 교체(모델/머티리얼/LOD)

아트 파이프라인에서 흔한 요구가 ‘같은 적인데 스킨만 다르게’다. 이때 Variant에서 MeshRenderer의 머티리얼이나 SkinnedMeshRenderer의 Mesh만 바꾸면 된다. 구조(본, 히트박스, 콜라이더, 스크립트)는 Base에 고정한다. Variant는 렌더러 필드 몇 개만 오버라이드한다.

SkinSwitcher.cs
1using UnityEngine;
2
3public class SkinSwitcher : MonoBehaviour
4{
5    public Renderer target;
6    public Material skinMaterial;
7
8    private void Reset()
9    {
10        target = GetComponentInChildren<Renderer>();
11    }
12
13    private void Start()
14    {
15        if (target != null && skinMaterial != null)
16            target.sharedMaterial = skinMaterial;
17    }
18}

SkinSwitcher는 런타임 교체 예시지만, 실무에서는 대부분 에디터에서 Variant로 sharedMaterial만 바꾼다. Start에서 sharedMaterial을 쓰는 이유는 material을 쓰면 인스턴스 머티리얼이 생성되어 메모리가 늘기 때문이다. Variant로 머티리얼을 바꾸는 작업은 에셋 참조만 바뀌고, 런타임 복제가 없다.

처음에 나도 Variant를 ‘상속 프리팹’ 정도로만 생각하고, Variant 쪽에서 컴포넌트를 막 추가했다. 어느 날 Base에서 Collider를 하나 추가했더니, 일부 Variant 인스턴스에서 히트 판정이 두 번 들어갔다. 콘솔에는 에러가 없고, 디버그로 GetComponents<Collider>() 찍어보니 Variant마다 콜라이더 개수가 달랐다.

원인은 단순했다. Variant에서 이미 Collider를 오버라이드로 추가해 둔 상태였고, Base에 같은 역할의 Collider를 추가하면서 중복이 생겼다. 해결은 ‘구조 변경은 Base에서만’ 규칙을 팀 규약으로 박고, Variant에서는 값과 참조만 바꾸는 것으로 제한하는 것이었다. 이후에는 Prefab Overrides 창에서 ‘Added Component’가 보이면 바로 줄이는 쪽으로 리뷰했다.

자주 하는 실수

실수 1: Variant 대신 Duplicate로 파생 프리팹을 만든다

증상: Base 프리팹의 공통 수정(레이어, 스크립트 버그 수정, 사운드 교체)이 일부 프리팹에만 반영된다. QA에서 ‘특정 몬스터만 피격 사운드가 옛날 버전’ 같은 리포트가 들어온다.

원인: Duplicate는 베이스 참조가 없는 독립 에셋이다. Unity가 머지할 부모가 없으니, 공통 변경을 자동으로 전파할 방법이 없다. 팀원이 Apply/Revert로 해결하려 해도 구조적으로 불가능하다.

해결: Project 창에서 Base 우클릭 → Create → Prefab Variant로 파생을 만든다. 이미 Duplicate가 퍼졌다면, Variant로 다시 만들고(새 Variant 생성), Duplicate에서 바뀐 값만 수동으로 옮긴다. Overrides 창을 열어 Added/Removed를 최소화하면서 옮기는 게 포인트다.

실수 2: Apply 위치를 착각해서 씬 오버라이드를 Variant 에셋에 저장한다

증상: 씬에서 잠깐 테스트하려고 speed를 바꿨는데, 다음 날 다른 씬의 같은 Variant까지 전부 speed가 바뀌어 있다. Git diff를 보면 Variant 프리팹 에셋이 수정되어 있다.

원인: Overrides → Apply All을 눌렀기 때문이다. Apply는 ‘현재 선택된 대상의 오버라이드를 부모 에셋으로 저장’한다. 씬 인스턴스에서 Apply를 하면 Variant 에셋이 바뀐다. 팀 단위로 공유되는 값이 되어 버린다.

해결: 테스트용 변경은 Play 모드에서만 하고, 에디터 값 변경은 Overrides 창에서 항목 단위로 Apply/Revert를 선택한다. Inspector 상단의 프리팹 바(파란색/회색)에서 ‘어느 에셋이 바뀌는지’ 표시를 먼저 확인한다.

실수 3: Base에서 컴포넌트 순서를 바꾸거나 필드명을 바꾼다

증상: Variant에서 분명히 바꿔둔 값이 어느 날 기본값으로 돌아간다. Overrides 창에서 항목이 사라지거나, ‘Missing’ 비슷한 표시가 뜬다. 경우에 따라 콘솔에 The referenced script on this Behaviour is missing! 같은 메시지가 섞인다.

원인: 오버라이드는 프로퍼티 경로 기반이라서, 필드명 변경/SerializeField 제거/컴포넌트 교체가 일어나면 적용 대상이 사라진다. 특히 [FormerlySerializedAs] 없이 필드명을 바꾸면 YAML의 키가 바뀌어 복원이 실패한다.

해결: 필드명 변경이 필요하면 UnityEngine.Serialization.FormerlySerializedAs를 붙여 직렬화 키를 유지한다. 컴포넌트 구조 변경은 Base에서만 하고, 변경 후 Prefab Overrides 창에서 Variant들의 오버라이드가 유지되는지 샘플 몇 개를 열어 확인한다.

RenamedFieldSafe.cs
1using UnityEngine;
2using UnityEngine.Serialization;
3
4public class RenamedFieldSafe : MonoBehaviour
5{
6    [FormerlySerializedAs("oldSpeed")]
7    public float newSpeed = 5f;
8
9    private void Awake()
10    {
11        Debug.Log($"newSpeed={newSpeed}");
12    }
13}

이 코드를 적용한 뒤 oldSpeed라는 이름으로 저장된 프리팹/씬 데이터가 있어도 newSpeed로 복원된다. Prefab Variant에서 값이 ‘조용히 풀리는’ 문제의 상당수가 이 어트리뷰트 하나로 막힌다.

실수 4: Variant에서 Added Component가 쌓인 상태로 Base를 계속 키운다

증상: 어떤 Variant는 동작하고 어떤 Variant는 동작하지 않는다. 예를 들어 데미지 처리 스크립트가 두 번 호출되거나, 콜라이더가 겹쳐서 OnTriggerEnter가 두 번 찍힌다.

원인: Variant는 구조 변경도 허용한다. Variant에서 컴포넌트를 추가하면 ‘Added Component’ 오버라이드가 저장된다. 이후 Base에도 같은 역할의 컴포넌트를 추가하면 중복이 된다. 엔진은 중복을 자동으로 합치지 않는다.

해결: Overrides 창에서 Added/Removed 항목을 주기적으로 청소한다. 구조는 Base에서만, Variant는 값/참조만 원칙을 지킨다. 팀 규칙으로 ‘Variant에서 Add Component 금지(예외는 문서화)’를 두면 QA 비용이 줄어든다.

실수 5: material을 바꿔야 하는데 material 프로퍼티를 만져 런타임에 인스턴스가 폭증한다

증상: Play 후 메모리가 늘고, Hierarchy에서 같은 오브젝트인데도 드로우콜이 늘어난다. Profiler의 Memory에서 Material 인스턴스 수가 계속 증가한다.

원인: Renderer.material은 접근 시점에 머티리얼을 복제한다. Variant로 머티리얼을 ‘에셋 참조’로 바꾸려면 sharedMaterial을 써야 한다. material은 런타임 개별 수정이 필요할 때만 쓴다.

해결: Variant에서는 Renderer의 sharedMaterial 슬롯을 에디터에서 교체한다. 런타임에서 색만 바꾸려면 MaterialPropertyBlock을 쓴다. 프리팹 파생 문제처럼 보이지만, 사실은 렌더러 API 설계(복제 정책) 때문에 생긴다.

성능 최적화 체크리스트

  • Project Settings → Editor → Asset Serialization을 Force Text로 유지한다(프리팹 diff/merge 안정).
  • Base에는 구조(컴포넌트 구성, 자식 트리, 레이어/태그)를 넣고 Variant에는 값/참조만 남긴다.
  • Variant Overrides 창에서 Added Component/Removed Component가 보이면 이유를 문서화하거나 제거한다.
  • 씬 인스턴스에서 Apply All을 누르기 전, Inspector 상단에서 적용 대상이 Variant 에셋인지 Base 에셋인지 확인한다.
  • 필드명 변경 시 FormerlySerializedAs를 사용해 오버라이드 유실을 막는다.
  • Renderer.material 접근을 Update에서 하지 않는다(머티리얼 인스턴스 생성 + GC 압력 가능).
  • GetComponent를 Update에서 반복 호출하지 않고 Awake/Start에서 캐싱한다(네이티브 컴포넌트 배열 탐색 비용).
  • Profiler에서 EditorLoop 스파이크가 있으면 Prefab Overrides 수(오버라이드 레코드)를 의심한다.
  • Nested Prefab 구조에서 상위/하위 Prefab의 오버라이드가 섞이지 않게 책임 경계를 나눈다(상위는 배치, 하위는 기능).
  • Apply/Revert 작업은 PR 단위로 묶고, 프리팹 에셋 변경 diff를 리뷰한다(YAML에서 의도치 않은 필드 변경 탐지).
  • 씬 오버라이드는 최소화하고, 반복되는 씬 오버라이드는 Variant 에셋으로 승격한다(씬 파일 충돌 감소).
  • Play Mode Options에서 Domain Reload를 끄는 팀이면, ScriptableObject/정적 캐시가 프리팹 값과 섞이지 않게 초기화 경로를 둔다.

자주 묻는 질문

Prefab Variant와 Nested Prefab은 뭐가 다르나? 같이 쓰면 어떤 순서로 머지되나?

Nested Prefab은 프리팹 안에 다른 프리팹 인스턴스를 자식으로 포함하는 구조이고, Variant는 프리팹 에셋이 다른 프리팹 에셋을 부모로 참조하는 ‘상속형’ 구조다. 머지 관점에서는 (1) Base Prefab 로드 → (2) Variant의 오버라이드 적용 → (3) 씬 인스턴스 오버라이드 적용 순으로 최종 값이 결정된다. Nested는 각 자식 프리팹이 자기 자신의 Base/Variant/Instance 규칙으로 따로 머지된다. 그래서 상위 프리팹에서 자식 프리팹의 내부 값을 막 바꾸기 시작하면 오버라이드가 여러 레벨로 퍼지고, 나중에 Apply 대상이 헷갈린다. 다음 학습 키워드는 Prefab Stage, Overrides UI의 Apply 대상, Nested Prefab의 책임 분리(상위는 배치, 하위는 기능)다.

왜 Prefab Override가 필드명 변경이나 컴포넌트 재정렬에 약한가?

Unity 직렬화는 ‘오브젝트 그래프 + 프로퍼티 경로’로 값을 복원한다. Prefab Override는 전체 스냅샷이 아니라 ‘어떤 프로퍼티가 기본값에서 달라졌는지’만 기록하는데, 이때 프로퍼티를 찾는 키가 필드명과 경로에 의존한다. 필드명을 바꾸면 YAML 키가 달라지고, 컴포넌트 순서를 바꾸면 같은 타입이라도 다른 컴포넌트 인스턴스로 매칭될 수 있다. 네이티브 쪽에서는 SerializedProperty 경로 문자열을 기반으로 타겟을 찾는 과정이 들어가고, 매칭 실패 시 오버라이드 레코드가 적용되지 않거나 무시된다. 다음 학습 키워드는 FormerlySerializedAs, SerializeReference, 컴포넌트 GUID 매칭 방식, Prefab YAML 구조다.

Variant를 런타임에서 구분할 수 있나? 플레이 중에 ‘이 오브젝트는 Variant 출신’ 같은 걸 알 수 있나?

플레이어(빌드) 런타임에서는 Prefab Variant라는 개념이 거의 ‘에셋 제작 단계의 정보’로 소거된다. 빌드에 포함된 에셋이 로드될 때는 이미 최종 직렬화 데이터로 디시리얼라이즈 되어 네이티브 오브젝트가 만들어지고, MonoBehaviour 필드는 그 결과로 채워진다. 에디터에서는 PrefabUtility로 원본/부모/오버라이드 정보를 추적할 수 있지만, UnityEditor 네임스페이스는 빌드에 들어가지 않는다. 런타임에서 출처를 추적하려면 Addressables 라벨, 커스텀 메타(예: ScriptableObject ID), 혹은 빌드 전처리로 별도 테이블을 만들어야 한다. 다음 학습 키워드는 UnityEditor 제한, AssetBundle/Addressables 메타데이터, 런타임 로더 설계다.

Apply All을 누르면 내부적으로 어떤 일이 일어나나? 왜 가끔 Apply가 오래 걸리나?

Apply는 ‘현재 오브젝트의 오버라이드 목록’을 계산하고, 그 변경을 대상 프리팹 에셋(YAML 직렬화 데이터)에 반영한 뒤, 에셋을 다시 임포트/리로드하는 흐름으로 이어진다. 에디터에서는 SerializedObject 변경 추적과 Prefab override 비교가 일어나고, 오버라이드가 많으면 비교해야 할 프로퍼티 경로 수가 늘어난다. 또한 Apply는 단순히 값만 쓰는 게 아니라, Added/Removed 컴포넌트나 자식 오브젝트 변경 같은 구조 변경을 포함할 수 있어서, 내부적으로 오브젝트 그래프 재구성이 커진다. Apply가 오래 걸릴 때는 Overrides 수가 많거나, Nested Prefab이 깊거나, 텍스처/모델 리임포트 같은 부수 작업이 엮였을 가능성이 크다. 다음 학습 키워드는 Prefab Overrides 최소화, Apply 대상 분리, 에셋 임포트 파이프라인이다.

Variant에서 공통 스크립트 값을 바꾸면 왜 모든 파생이 바뀌나? 특정 파생만 바꾸려면?

Variant 에셋에서 바꾼 값은 그 Variant를 부모로 하는 모든 인스턴스에 공통으로 적용된다. 씬 인스턴스에서 바꾼 값은 그 씬 인스턴스에만 적용된다. 특정 파생만 바꾸려면 두 가지 선택이 있다. (1) Variant를 한 단계 더 만든다(Variant of Variant) 해서 그 단계에서 오버라이드한다. (2) 씬 인스턴스 오버라이드로 남긴다. 실무에서는 ‘그 값이 여러 씬/여러 배치에서 반복되나’가 기준이다. 반복되면 Variant로 승격하고, 일회성이면 씬 오버라이드로 둔다. 다음 학습 키워드는 Variant 계층 설계, 씬 오버라이드 최소화, 데이터 드리븐(Stats ScriptableObject)로 이동이다.

Prefab Variant를 쓰면 성능이 좋아지나? 런타임 FPS에 영향이 있나?

Prefab Variant 자체가 런타임 FPS를 직접 올리지는 않는다. 이유는 머지가 로딩 시점에 끝나고, 플레이 중에는 최종 값만 남기 때문이다. 성능 이슈는 주로 에디터에서 발생한다. Overrides가 많아지면 Inspector가 오버라이드 비교를 하느라 EditorLoop에서 스파이크가 나고, Apply/Revert가 느려진다. 런타임에서 영향을 주는 경우는 ‘Variant 때문에’가 아니라, Variant 운영이 무너지면서 중복 컴포넌트가 생기거나(Renderer/material 복제, 중복 Collider) Update 호출이 늘어나는 식으로 간접 비용이 생길 때다. 다음 학습 키워드는 Profiler에서 EditorLoop vs PlayerLoop 구분, 중복 컴포넌트 탐지, material/sharedMaterial 정책이다.

팀 작업에서 Prefab 충돌이 자주 난다. Variant를 어떻게 운영하면 충돌이 줄어드나?

충돌은 ‘같은 파일을 여러 사람이 건드릴 때’ 생긴다. Variant 운영의 핵심은 변경을 분산시키는 것이다. Base는 구조와 공통 규칙이라서 수정자가 제한되어야 하고, Variant는 밸런스/스킨 같은 역할로 쪼개 파일을 나눠야 한다. 예를 들어 Enemy_Base는 프로그래머 1~2명이 관리하고, Enemy_Skin_* Variant는 아티스트가, Enemy_Stat_* Variant는 기획자가 관리하는 식으로 책임을 나누면 같은 YAML 파일을 동시에 수정할 확률이 떨어진다. 또 씬 인스턴스 오버라이드를 줄이면 씬 파일 충돌도 줄어든다. 다음 학습 키워드는 Force Text, YAML merge 전략, 프리팹 책임 분리, Git LFS/스마트 머지 설정이다.

관련 글

28. Prefab Variant로 공통 설정 유지하며 개별 오버라이드 적용하기

28. Prefab Variant로 공통 설정 유지하며 개별 오버라이드 적용하기

Prefab Variant로 공통 프리팹 설정을 유지하면서 개별 오버라이드를 적용하는 방법을 엔진 직렬화, 오버라이드 저장 구조, Player Loop 관점에서 설명한다. 실습 포함. 154자 내외 조정됨: Prefab Variant로 공통 설정을 유지하면서 개별 오버라이드를 적용하는 방법을 엔진 직렬화와 오버라이드 저장

29. Prefab Variant로 공통 설정 유지하며 파생 프리팹 운영하기

29. Prefab Variant로 공통 설정 유지하며 파생 프리팹 운영하기

Unity Prefab Variant를 엔진 직렬화·오버라이드 관점에서 설명하고, 캐릭터/아이템 파생 프리팹을 공통 설정 유지하며 운영하는 실습과 성능 함정을 다룬다. 2022 LTS 기준 실무 패턴 포함.



























27. Prefab 인스턴스와 원본의 관계: Override, Apply, Variant까지

27. Prefab 인스턴스와 원본의 관계: Override, Apply, Variant까지

Prefab 원본과 인스턴스가 왜 연결되고 어디서 끊기는지, Override/Apply/Unpack/Variant를 엔진 직렬화·네이티브 오브젝트 관점에서 설명한다. 실습과 디버깅 포인트 포함. 2022 LTS 기준. (150자)‏‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‏‏‎