Unity Async Resource Loading으로 씬 전환 스터터링 해결하기
Unity 씬 전환에서 발생하는 프레임 드롭과 입력 멈춤은 메인 스레드의 동기 로딩이 원인인 경우가 많습니다. Addressables와 LoadSceneAsync를 이용해 리소스 로딩, 활성화, 해제를 분리하는 실전 구조와 프로파일링 기준을 정리합니다.

핵심 요약
씬 전환 스터터링은 텍스처·메시·셰이더·씬 데이터를 한 프레임에 동기 로딩하면서 메인 스레드가 오래 점유될 때 발생한다. Unity에서는 SceneManager.LoadSceneAsync 또는 Addressables의 LoadSceneAsync로 로딩을 분리하고 로딩 완료 뒤 allowSceneActivation 또는 activateOnLoad로 활성화 시점을 제어하면 플레이 중 프레임 드롭을 줄일 수 있다.
핵심은 비동기 API를 호출하는 것만으로 끝내지 않고 씬 활성화와 대형 UI 생성, 셰이더 워밍업, 기존 리소스 해제를 같은 프레임에 몰지 않는 것이다.
씬 전환 스터터링은 왜 발생하는가?
Unity의 씬 전환 비용은 씬 파일을 읽는 시간만 뜻하지 않는다. 게임 오브젝트 역직렬화, Awake·OnEnable 실행, 텍스처와 메시 업로드, 셰이더 변형 준비, UI 레이아웃 계산, 가비지 컬렉션까지 전환 구간에 겹칠 수 있다.
특히 SceneManager.LoadScene은 동기 로딩이다. 호출 프레임에서 로딩과 활성화 작업이 완료될 때까지 게임 루프가 진행되지 않을 수 있으므로 화면이 멈추거나 프레임 시간이 급격히 늘어난다.
| 작업 | 스터터링이 생기는 대표 원인 | 분리 방법 |
|---|---|---|
| 씬 데이터 읽기 | 동기 LoadScene 호출 | LoadSceneAsync 사용 |
| 씬 활성화 | 다수의 Awake, OnEnable, 첫 프레임 초기화 | 활성화 시점 제어, 초기화 분산 |
| UI 표시 | 대형 Canvas 재빌드와 레이아웃 계산 | 로딩 화면을 사전 유지, UI 지연 생성 |
| 셰이더 준비 | 처음 사용하는 Shader Variant 컴파일·로드 | Shader Variant Collection 워밍업 |
| 메모리 정리 | 전환 중 Resources.UnloadUnusedAssets와 GC 실행 | 안전한 로딩 화면 구간에서 실행 |

Async Resource Loading은 어떻게 씬 전환을 부드럽게 만드는가?
AsyncOperation은 로딩 작업을 여러 프레임에 걸쳐 진행하게 한다. 로딩 진행률은 0.9까지 올라간 뒤 씬 활성화를 기다리는 방식으로 동작한다. 이 구간에서 페이드 아웃, 로딩 UI, 네트워크 동기화 같은 전환 준비를 처리할 수 있다.
flowchart LR
A[플레이 중 씬] --> B[페이드 아웃 시작]
B --> C[비동기 씬 로딩]
C --> D{로딩 준비 완료?}
D -- 아니오 --> C
D -- 예 --> E[기존 씬 정리]
E --> F[새 씬 활성화]
F --> G[페이드 인]
다만 비동기 로딩은 모든 비용을 없애지 않는다. 새 씬을 활성화하는 순간에는 게임 오브젝트 생성과 라이프사이클 콜백이 집중될 수 있다. 따라서 활성화 직후의 초기화량도 관리해야 한다.
Unity LoadSceneAsync로 씬 전환을 구현하는 방법
1. 로딩 화면을 먼저 표시한다
로딩 UI는 전환할 씬 안에 두기보다 부트스트랩 씬 또는 DontDestroyOnLoad 오브젝트에 유지하는 편이 안정적이다. 새 씬이 활성화되기 전에도 즉시 표시할 수 있기 때문이다.
2. 씬을 비동기로 로드하고 활성화를 보류한다
다음 예제는 단일 씬 전환에서 사용할 수 있는 최소 구조다. allowSceneActivation = false로 두면 진행률이 0.9에 도달한 뒤 활성화만 대기한다.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public sealed class SceneTransition : MonoBehaviour
{
[SerializeField] private CanvasGroup loadingCanvas;
public IEnumerator LoadAsync(string sceneName)
{
yield return Fade(1f);
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;
while (operation.progress < 0.9f)
{
// Unity의 0.0~0.9 진행률을 UI용 0~1 값으로 변환한다.
float progress = Mathf.Clamp01(operation.progress / 0.9f);
Debug.Log($"Loading: {progress:P0}");
yield return null;
}
operation.allowSceneActivation = true;
yield return operation;
yield return Fade(0f);
}
private IEnumerator Fade(float targetAlpha)
{
loadingCanvas.alpha = targetAlpha;
yield return null;
}
}
3. 활성화 이후의 첫 프레임 비용을 줄인다
LoadSceneAsync가 끝난 직후 무거운 초기화를 한 곳에 모으면 스터터링이 다시 생긴다. NPC 수십 개 생성, Addressables 프리팹 인스턴스화, 대형 목록 UI 바인딩은 코루틴으로 나누거나 플레이어 입력이 가능한 시점 이후로 미룬다.
예를 들어 적 생성은 한 프레임에 전부 처리하지 말고 일정 개수씩 나눈다.
private IEnumerator SpawnEnemies(GameObject prefab, int count)
{
const int batchSize = 5;
for (int i = 0; i < count; i++)
{
Instantiate(prefab);
if ((i + 1) % batchSize == 0)
yield return null;
}
}
Addressables는 언제 사용하는 것이 좋은가?
LoadSceneAsync는 Build Settings에 포함된 씬을 전환하는 간단한 경우에 충분하다. 반면 씬과 함께 내려받을 콘텐츠, 메모리 수명, 의존성 번들을 관리해야 한다면 Unity Addressables가 더 적합하다.
| 상황 | 권장 방식 |
|---|---|
| 소규모 게임, 빌드에 포함된 고정 씬 | SceneManager.LoadSceneAsync |
| DLC, 원격 카탈로그, 다운로드 콘텐츠 | Addressables |
| 씬별 에셋의 명시적 로드·해제 필요 | Addressables |
| 애드티브 씬을 여러 개 유지 | Addressables 또는 LoadSceneMode.Additive |
Addressables에서는 activateOnLoad: false로 활성화를 지연할 수 있다. 로드 핸들은 해제 시 필요하므로 보관해야 한다.
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
public sealed class AddressableSceneLoader : MonoBehaviour
{
private AsyncOperationHandle<SceneInstance> sceneHandle;
public IEnumerator LoadScene(string key)
{
sceneHandle = Addressables.LoadSceneAsync(key, activateOnLoad: false);
yield return sceneHandle;
AsyncOperationHandle<SceneInstance> activation = sceneHandle.Result.ActivateAsync();
yield return activation;
}
public IEnumerator UnloadScene()
{
if (!sceneHandle.IsValid())
yield break;
yield return Addressables.UnloadSceneAsync(sceneHandle);
}
}
Addressables를 사용하면서 Resources.Load와 수동 번들 로딩을 같은 자산에 섞으면 참조 수명 추적이 어려워진다. 하나의 콘텐츠 경로에서는 로딩 책임을 한 방식으로 통일하는 편이 좋다.
씬 전환 최적화에서 확인할 프로파일링 지표
추측으로 최적화하지 말고 Unity Profiler의 CPU Usage와 Timeline에서 전환 구간을 측정한다. 아래 항목이 길게 보이면 대응 위치를 찾기 쉽다.
Loading.ReadObject,Loading.AwakeFromLoad: 에셋 역직렬화와 로드 후 초기화 비용GC.Collect: 대량 할당 뒤 가비지 컬렉션 발생 여부Canvas.SendWillRenderCanvases: UI 리빌드 비용BehaviourUpdate,PlayerLoop: 활성화 직후 스크립트 초기화 집중 여부- Rendering 구간의 셰이더 관련 스파이크: 첫 렌더 시 셰이더 변형 준비 여부
프레임 예산도 같이 본다. 목표가 60 FPS라면 한 프레임 예산은 다음과 같다.
전환 중 일시적으로 로딩 화면을 보여도 활성화 프레임이 수백 ms까지 늘어나면 사용자는 멈춤으로 인식한다. 최적화 목표는 평균 FPS보다 이 최악 프레임 시간을 낮추는 데 있다.
자주 묻는 질문 (FAQ)
LoadSceneAsync를 쓰면 스터터링이 완전히 사라지나요?
아니다. 파일 읽기와 일부 로드는 분산되지만 씬 활성화, Awake, UI 생성, 셰이더 준비 비용은 여전히 스파이크를 만들 수 있다. 활성화 직후 초기화를 나누고 Profiler로 병목을 확인해야 한다.
operation.progress가 0.9에서 멈춘 것은 버그인가요?
아니다. allowSceneActivation이 false이면 Unity는 로딩 준비가 된 뒤 진행률을 0.9에 유지하고 활성화를 기다린다. true로 변경하면 다음 단계로 진행된다.
Resources.UnloadUnusedAssets는 언제 호출해야 하나요?
필요할 때만 로딩 화면이 이미 표시된 구간에서 호출한다. 매 전환마다 습관적으로 실행하면 정리 자체가 프레임 스파이크를 만들 수 있다.
정리
씬 전환 스터터링의 첫 해결책은 동기 LoadScene을 비동기 로딩으로 바꾸는 것이다. 그 다음에는 씬 활성화 시점, 첫 프레임 초기화, UI 리빌드, 셰이더 준비, 메모리 정리를 분리해 최악 프레임 시간을 측정하고 줄인다.
작은 프로젝트는 SceneManager.LoadSceneAsync로 시작해도 충분하다. 콘텐츠 다운로드와 에셋 수명 관리가 필요해질 때만 Addressables로 확장하면 된다.



