Quick Flow
| 단계 | 호출 횟수 | 두는 일 | 보장하지 않는 것 |
|---|---|---|---|
Awake | 인스턴스 수명에서 1회 | 같은 GameObject의 컴포넌트 캐시, 내부 상태 | 다른 GameObject의 Awake/manager 준비 순서 |
OnEnable / OnDisable | 활성 상태가 바뀔 때마다 | 이벤트 구독·해제, 활성 수명 등록 | 1회 초기화, coroutine 사용 (OnEnable은 coroutine 불가) |
Start | 첫 활성화 뒤 1회 | 모든 씬 객체 Awake 뒤의 연결 | 런타임 생성 객체까지 포함한 전역 실행 순서 |
Update / FixedUpdate / LateUpdate | 반복 | 프레임·물리·후처리 역할 분리 | 서로 다른 GameObject 사이의 호출 순서 |
OnDestroy | 파괴 수명에서 | 파괴 전 최종 정리 | 비활성화 때의 정리, 한 번도 active가 아니었던 객체 |
private void Awake() => _rb = GetComponent<Rigidbody>();
private void OnEnable() => GameEvents.Paused += HandlePause;
private void Start() => _hud = FindFirstObjectByType<Hud>();
private void Update() => ReadInput();
private void FixedUpdate() => ApplyPhysics();
private void LateUpdate() => FollowTarget();
private void OnDisable() => GameEvents.Paused -= HandlePause;순서에 의존하지 않는 연결
Awake와 Start의 경계
Awake는 스크립트 인스턴스가 로드될 때 한 번 호출됩니다. 처음부터 inactive인 GameObject는 활성화될 때까지 Awake가 늦어질 수 있습니다. 그래서 Awake에는 같은 GameObject에서 확실히 얻을 수 있는 참조와 내부 상태만 둡니다.
씬에 미리 존재하던 모든 활성 스크립트의 Awake는 어느 Start보다 먼저 호출됩니다. 다른 씬 객체를 연결해야 하면 Start가 더 적절한 출발점입니다. 그러나 같은 event function이 서로 다른 GameObject에서 호출되는 정확한 순서는 기본적으로 보장되지 않습니다. Script Execution Order는 서로 다른 script type의 순서를 설정할 수 있지만, 같은 MonoBehaviour type의 여러 인스턴스 순서는 정할 수 없습니다.
서로 준비 순서가 중요한 시스템은 Find 결과를 Awake에서 믿는 대신, bootstrapper의 명시적 Initialize 호출·의존성 주입·ready event 중 하나로 계약을 만듭니다.
public sealed class GameBootstrap : MonoBehaviour
{
[SerializeField] private SaveService saveService;
[SerializeField] private InventoryService inventoryService;
private void Start()
{
saveService.Initialize();
inventoryService.Initialize(saveService.CurrentSave);
}
}활성화, reload, 파괴
OnEnable은 Behaviour.enabled와 GameObject.activeInHierarchy가 모두 true가 되는 경로에서 호출됩니다. OnDisable은 반대 전환, 파괴 직전, Editor script reload 같은 경로에서 호출될 수 있으므로 구독 해제는 여러 번 불려도 안전해야 합니다. OnDestroy는 비활성화만으로는 호출되지 않으며, 한 번도 active가 아니었던 GameObject에는 호출되지 않을 수 있습니다.
Enter Play Mode Options에서 Domain Reload를 끄면 static field와 static event가 이전 play session 값을 유지할 수 있습니다. static 구독·캐시가 있는 프로젝트는 이 옵션을 켠 상태와 끈 상태 모두에서 초기화와 해제를 검증하고, 필요하면 play 시작 시 static 상태를 명시적으로 reset합니다.
프레임 루프
Update는 렌더 프레임 기준, FixedUpdate는 물리 step 기준, LateUpdate는 해당 프레임의 모든 Update 뒤에 호출됩니다. 한 렌더 프레임에는 FixedUpdate가 0회 또는 여러 회일 수 있습니다. 물리·입력 경계는 Update vs FixedUpdate에서, coroutine 재개 위치는 코루틴과 시간 기반 흐름에서 따로 확인합니다.
자주 틀리는 부분
| 증상 | 원인 | 수정 방향 |
|---|---|---|
| 특정 씬에서만 manager가 null | 다른 객체의 Awake 순서를 가정했습니다. | Start 또는 명시적 초기화 계약으로 연결합니다. |
| 풀 재사용 뒤 이벤트가 두 번 처리됨 | OnEnable 구독을 OnDisable에서 해제하지 않았습니다. | 구독과 해제를 한 쌍으로 둡니다. |
| inactive prefab의 정리가 실행되지 않음 | OnDestroy를 보편적 cleanup으로 믿었습니다. | 활성화 여부와 destroy 경로를 검증하고 필요한 cleanup을 명시합니다. |
| play를 다시 눌렀는데 static 상태가 남음 | Domain Reload 비활성 옵션을 고려하지 않았습니다. | static reset과 editor play-mode 조합을 테스트합니다. |
| Script Execution Order를 늘려도 간헐 버그가 남음 | 같은 type 인스턴스 순서는 설정되지 않습니다. | 의존성을 직접 연결하고 준비 완료를 전달합니다. |
실행 순서는 코드의 의존성을 해결하는 만능 장치가 아닙니다. "A가 먼저여야 B가 동작한다"면 그 사실을 필드, Initialize 메서드, ready event 중 하나로 표현하세요. 그러면 additive scene, prefab 생성, test runner에서도 같은 계약을 검사할 수 있습니다.
참고 링크
4 sources