콘텐츠로 이동

Unity 2022.3에서 Unity 6으로 올리기 전 사전 검토

Unity 2022.3.33f1을 쓰는 모바일 게임에 Unity 6을 도입하기 전에, 왜 올려야 하는지, 무엇이 문제가 될 수 있는지, 작업 비용이 얼마나 드는지를 검토했다. 실제 프로젝트를 리서치 용도로 Unity 6에 먼저 올려 보고 에디터 경고·에러를 분류했다.

가장 큰 이유는 Android 16KB 메모리 페이지 크기 대응이다. 최신 Android 기기와 스토어가 16KB 페이지 크기 지원을 요구하고, Unity 6은 이 환경을 공식 지원한다.

Unity 6이 16KB 대응의 필수 조건은 아니다. 다만 Unity 버전을 올릴 때마다 항상 전수 QA를 해 왔으므로, 어차피 치를 QA 비용을 16KB 대응 시점에 맞춰 한 번에 쓰는 편이 낫다고 판단했다.

둘 다 필수는 아니다.

  • obsolete된 Unity API를 새 API로 교체했다.
  • PSB와 2D 스프라이트 시스템이 바뀌어 .meta 변경을 반영했다.

임포트 후 에디터 경고 (수정 필수 아님)

섹션 제목: “임포트 후 에디터 경고 (수정 필수 아님)”
경고 원인 / 조치
Main Object Name 'NormalHit' does not match filename 'NormalHIt' .anim 파일 이름과 내부 메인 오브젝트 이름이 다르다. 파일명 오타도 같은 경고를 낸다.
The legacy Animation Clip "Hit" cannot be used in the State "Hit". Legacy AnimationClips are not allowed in Animator Controllers. Animator에서 legacy 타입 클립을 쓰고 있다.
Failed to convert 3012674763 to a signed 32 bit int in file '...prefab' 포맷 업그레이드 과정에서 내부 직렬화 검사가 더 엄격해졌다.
Creating missing Symbol component for SymbolStateDisplay in Symbol_Blank. 업그레이드에 따른 메타데이터 변경으로 참조가 깨졌다.
Sprite ... matches more than one built-in atlases. 기존부터 있던 중복 아틀라싱 경고다.
com.unity.textmeshpro is deprecated TextMeshPro 기능이 com.unity.ugui로 들어갔다. 패키지 정리가 필요하다.
Identifier uniqueness violation: 'Name:fan002, Type:Mesh' 같은 이름·타입의 오브젝트가 한 FBX에서 여러 개 나온다. FBX 수정이 필요하다.
'.' is not allowed in State name Animator 내부 State 이름을 고친다.
A polygon of Mesh '...' is self-intersecting and has been discarded. 모델링 툴에서 self-intersecting/non-manifold 폴리곤을 정리한 뒤 재임포트한다.
Source Texture ... of Sprite ... is using compressed format. 아래 에러 항목과 같은 이중 압축 문제다.

전체 리임포트 후 에디터 에러 (수정 필요)

섹션 제목: “전체 리임포트 후 에디터 에러 (수정 필요)”
에러 원인 / 조치
transform.localRotation assign attempt ... Input rotation is { NaN, NaN, NaN, NaN } 기존 오류. 참조 없는 미사용 리소스다.
ImportFBX Errors: Time range (447401) for translation curve(s) ... larger than maximum allowed (100000) 기존 오류. 참조 없는 미사용 리소스다.
Nested Prefab problem. Missing Nested Prefab Asset: '... (Missing Prefab with guid: ...)' 중첩 프리팹 GUID 문제다. 아래 툴로 찾는다.
Problem detected while importing the Prefab file: '...prefab' GUID 문제다.
Importer(PrefabImporter) generated inconsistent result for asset(...) 프리팹을 하나씩 열어 missing이나 에러를 고쳐야 한다.
Shader error in 'TextMeshPro/Mobile/Distance Field_CF_MASK': Couldn't open include file 'TMPro_Properties.cginc' 셰이더 컴파일 문제다.
Material 'NoRender Material' with Shader 'Spine/Special/HiddenPass' doesn't have a texture property '_MainTex' 셰이더 컴파일 문제다.
Broken text PPtr in file(...controller). Local file identifier (...) doesn't exist! Animator와 클립 사이의 참조가 깨졌다.
BFImporter: not found '...png' 비트맵 폰트 임포트 실패다.
Source Texture ... is using compressed format. To ensure no loss in source pixel details when packing to SpriteAtlas, please use uncompressed format in TextureImporter. 이중 압축 문제다. 원본 텍스처의 압축을 해제한다.

프로젝트 전체 프리팹에서 Missing Nested Prefab을 찾고, 선택적으로 해당 오브젝트를 지운 뒤 프리팹을 다시 저장하는 에디터 창이다.

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
public class PrefabMissingFinder : EditorWindow
{
private Vector2 scroll;
private List<string> brokenPrefabs = new List<string>();
private bool autoFix = false;
[MenuItem("Tools/Prefab/Missing Nested Prefab Finder")]
public static void ShowWindow()
{
GetWindow<PrefabMissingFinder>("Missing Nested Prefab Finder");
}
private void OnGUI()
{
GUILayout.Label("🔎 프로젝트 전체에서 'Missing Nested Prefab' 자동 탐색", EditorStyles.boldLabel);
if (GUILayout.Button("검색 시작"))
{
FindBrokenPrefabs();
}
if (brokenPrefabs.Count > 0)
{
GUILayout.Space(10);
autoFix = EditorGUILayout.Toggle("자동 삭제(고급)", autoFix);
scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(300));
foreach (var path in brokenPrefabs)
{
GUILayout.Label(path, EditorStyles.wordWrappedLabel);
}
EditorGUILayout.EndScrollView();
if (autoFix && GUILayout.Button("Missing Nested Prefab 오브젝트 자동 삭제"))
{
FixBrokenPrefabs();
}
}
else
{
GUILayout.Space(10);
GUILayout.Label("🔍 아직 검색 결과가 없습니다.");
}
}
private void FindBrokenPrefabs()
{
brokenPrefabs.Clear();
string[] prefabGUIDs = AssetDatabase.FindAssets("t:Prefab");
foreach (string guid in prefabGUIDs)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null) continue;
// 미싱 프리팹(Disconnected, Missing) 찾기
var allTransforms = prefab.GetComponentsInChildren<Transform>(true);
foreach (var tr in allTransforms)
{
// Missing Prefab일 경우 gameObject.name이 'Missing Prefab'이거나, 프리팹 연결 상태가 Disconnected/Missing
var stage = PrefabUtility.GetPrefabInstanceStatus(tr.gameObject);
var assetType = PrefabUtility.GetPrefabAssetType(tr.gameObject);
if (tr.name.Contains("Missing Prefab") ||
(stage == PrefabInstanceStatus.MissingAssetReference) ||
(assetType == PrefabAssetType.MissingAsset))
{
if (!brokenPrefabs.Contains(path))
brokenPrefabs.Add(path);
break;
}
}
}
if (brokenPrefabs.Count > 0)
Debug.Log($"[PrefabMissingFinder] 발견: {brokenPrefabs.Count}개 프리팹에 Missing Nested Prefab 있음. \n\n{string.Join("\n", brokenPrefabs)}");
else
Debug.Log("[PrefabMissingFinder] 모든 프리팹의 Nested Prefab 연결이 정상입니다.");
}
// 자동 삭제(옵션) - Missing Prefab만 찾아 삭제 후 Prefab 재저장
private void FixBrokenPrefabs()
{
foreach (var path in brokenPrefabs)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null) continue;
bool changed = false;
var allTransforms = prefab.GetComponentsInChildren<Transform>(true);
var targets = new List<GameObject>();
foreach (var tr in allTransforms)
{
if (tr.name.Contains("Missing Prefab") ||
PrefabUtility.GetPrefabInstanceStatus(tr.gameObject) == PrefabInstanceStatus.MissingAssetReference ||
PrefabUtility.GetPrefabAssetType(tr.gameObject) == PrefabAssetType.MissingAsset)
{
targets.Add(tr.gameObject);
}
}
if (targets.Count > 0)
{
foreach (var go in targets)
{
GameObject.DestroyImmediate(go, true);
}
changed = true;
}
if (changed)
{
EditorUtility.SetDirty(prefab);
PrefabUtility.SavePrefabAsset(prefab);
Debug.Log($"[PrefabMissingFinder] '{path}'에서 Missing Nested Prefab 자동 삭제 완료");
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[PrefabMissingFinder] 자동 삭제 작업 완료");
brokenPrefabs.Clear();
}
}

업그레이드를 진행한다면 필요한 작업은 다음과 같다.

  • 프로젝트 전수 QA
  • 전체 에셋 리임포트와 발생한 에러 제거
  • Android, iOS, PC 빌드 확인
  • Addressables 개편에 따른 변경점 검토와 수락
  • Package Manager 의존성과 플랫폼·광고 SDK의 Unity 6 지원 확인
  • 정리 차원의 에셋·스크립트·경고 제거 (선택)

필수 작업만 하면 개발 5일(SDK 연동 2일 별도), 경고와 불필요·중복 리소스 정리까지 하면 7일 정도로 산정했다. 생성형 AI로 이 시간을 줄이는 것도 목표로 잡았다.

기존 코드와 에셋은 Unity 6과 호환되지 않는 부분이 일부뿐이었고, Android, iOS, PC 빌드에서 런타임 문제도 발견하지 못했다. 그래서 업그레이드 시점은 Android 16KB 지원이 강제되는 기한, 그리고 의존하는 외부 SDK들이 Unity 6 대응 버전을 배포한 시점에 맞추는 것을 제안했다.