Unity 이벤트 함수 실행 순서
Unity에서 MonoBehaviour를 사용하다 보면 Awake, Start, Update, LateUpdate, OnDestroy 같은 이벤트 함수를 자연스럽게 사용하게 된다.
문제는 이 함수들이 단순히 코드에 작성한 순서대로 실행되는 것이 아니라는 점이다.
Unity는 게임 오브젝트의 생성, 활성화, 물리 업데이트, 프레임 업데이트, 렌더링, 파괴 등의 생명주기에 맞춰 특정 이벤트 함수를 호출한다.
따라서 이벤트 함수의 실행 순서를 이해하지 못하면 초기화 순서 문제나 NullReferenceException 같은 버그가 발생하기 쉽다.
Unity의 이벤트 함수는 크게 다음과 같은 생명주기를 가진다.
Awake -> OnEnable -> Start -> FixedUpdate -> Update -> LateUpdate -> Rendering -> OnDisable -> OnDestroy

다만 이것은 전체 Player Loop를 단순화한 개념적인 흐름이다. 실제 Unity의 실행 과정에는 물리, 애니메이션, 렌더링, 코루틴 등 더 많은 단계가 존재한다.
Awake
Awake는 객체가 생성될 때 초기화 목적으로 사용하는 함수다.
private void Awake()
{
// 초기화
}
일반적으로 자기 자신이 가지고 있는 데이터를 초기화하거나 컴포넌트 참조를 가져오는 용도로 사용한다.
private Rigidbody _rigidbody;
private void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
}
Awake는 동일한 오브젝트의 Start보다 먼저 호출된다.
다만 중요한 점이 있다.
다른 GameObject의 Awake가 정확히 어떤 순서로 실행되는지는 일반적인 코드에서 가정하면 안 된다.
Unity 공식 문서에서도 동일한 이벤트 함수에 대해 서로 다른 GameObject 간 호출 순서를 의존하지 않는 것을 권장한다. 필요한 경우 Script Execution Order 등을 사용해 명시적으로 순서를 제어할 수 있다.
OnEnable
OnEnable은 GameObject와 MonoBehaviour가 활성화될 때 호출된다.
private void OnEnable()
{
// 활성화될 때 수행할 작업
}
Awake와 중요한 차이가 있다.
Awake는 객체의 초기 생명주기에서 한 번 호출되는 반면, OnEnable은 비활성화되었다가 다시 활성화될 때마다 호출될 수 있다.
GameObject Active -> OnEnable
GameObject Inactive -> Active -> OnEnable
Script Disable -> Enable -> OnEnable
따라서 이벤트 등록이나 활성화 시점마다 다시 수행해야 하는 작업에 적합하다.
예를 들어 이벤트를 등록하는 코드라면 다음과 같이 사용할 수 있다.
private void OnEnable()
{
EventManager.OnChanged += HandleChanged;
}
그리고 비활성화될 때 해제한다.
private void OnDisable()
{
EventManager.OnChanged -= HandleChanged;
}
Start
Start는 스크립트가 활성화되어 있을 때 첫 프레임 업데이트 전에 호출된다.
private void Start()
{
// 초기화
}
보통 Awake에서는 자기 자신을 초기화하고, Start에서는 다른 시스템이나 객체와 연결하는 식으로 역할을 분리할 수 있다.
예를 들어 다음과 같은 구조다.
Awake -> 자신의 상태 및 컴포넌트 초기화
Start -> 다른 객체 및 시스템과 연결
여기서 흔히 발생하는 오해가 있다.
“모든 GameObject의 Awake가 끝난 다음 모든 Start가 실행되니까 다른 객체의 Start를 참조해도 된다.”
이렇게 단순하게 생각하면 안 된다.
Unity는 이벤트 함수의 전체적인 실행 순서는 정의하지만, 동일한 종류의 이벤트 함수가 서로 다른 GameObject에서 호출되는 상대적인 순서를 일반적으로 보장하지 않는다.
따라서 다음과 같은 구조는 위험할 수 있다.
// Player
private void Start()
{
_manager.Initialize();
}
// Manager
private void Start()
{
Initialize();
}
Player.Start()가 실행되는 시점에 Manager.Start()가 이미 실행됐다고 가정해서는 안 된다.
이런 의존성이 있다면 초기화 순서를 명시적으로 설계하는 것이 맞다.
FixedUpdate
FixedUpdate는 물리 업데이트를 위한 이벤트 함수다.
private void FixedUpdate()
{
// 물리 기반 로직
}
Update와 달리 프레임 수에 직접 종속되는 방식으로 호출되지 않는다.
게임의 렌더링 프레임과 물리 계산 주기는 서로 다른 개념이기 때문이다.
Rendering Frame Rate != Physics Update Rate
따라서 물리 기반 이동이나 Rigidbody를 이용한 처리는 일반적으로 FixedUpdate에서 수행한다.
private void FixedUpdate()
{
_rigidbody.AddForce(Vector3.forward * _force);
}
반대로 입력 처리나 일반적인 게임 로직은 보통 Update에서 처리한다.
Physics
FixedUpdate 이후에는 물리 시스템의 계산이 이어진다.
이 과정에서 충돌이나 트리거와 관련된 이벤트가 발생할 수 있다.
대표적으로 다음과 같은 함수들이 있다.
FixedUpdate -> Physics Simulation -> Collision / Trigger Events
private void OnCollisionEnter(Collision collision)
{
}
private void OnTriggerEnter(Collider other)
{
}
따라서 물리 이벤트는 일반적인 Update 흐름과 동일하게 생각하면 안 된다.
Update
Update는 가장 많이 사용하는 이벤트 함수다.
private void Update()
{
// 매 프레임 실행
}
프레임마다 실행되는 게임 로직을 작성한다.
대표적으로 다음과 같은 작업이 들어간다.
Input -> Update -> Game Logic -> State Change
예를 들어 플레이어 입력을 읽고 캐릭터의 상태를 변경하는 작업이 이에 해당한다.
private void Update()
{
float horizontal = Input.GetAxis("Horizontal");
transform.position += Vector3.right * horizontal * Time.deltaTime;
}
단, 모든 로직을 Update에 넣는 것이 좋은 것은 아니다.
물리 기반 동작은 FixedUpdate, 업데이트 이후의 후처리는 LateUpdate 등 각각의 목적에 맞는 이벤트를 사용하는 것이 좋다.
LateUpdate
LateUpdate는 Update가 끝난 이후 호출된다.
private void LateUpdate()
{
// Update 이후 처리
}
대표적인 사용 사례가 카메라다.
예를 들어 플레이어가 Update에서 이동하고 카메라가 플레이어를 따라간다면 다음과 같이 구성할 수 있다.
Player Update -> Player Movement -> Camera LateUpdate
이렇게 하면 플레이어의 이동이 먼저 끝난 뒤 카메라가 최종 위치를 기준으로 따라갈 수 있다.
특히 3인칭 카메라처럼 다른 객체의 이동 결과를 기준으로 자신의 위치를 계산해야 하는 시스템에서 유용하다.
Coroutine과 yield
Unity의 코루틴은 일반적인 함수 호출과 다른 실행 흐름을 가진다.
예를 들어 다음과 같은 코드가 있다.
private IEnumerator Process()
{
yield return null;
// 다음 프레임 이후 실행
}
또는 물리 업데이트를 기다릴 수도 있다.
yield return new WaitForFixedUpdate();
대표적인 yield 타입은 다음과 같다.
yield return null -> 다음 프레임
yield return WaitForFixedUpdate -> 다음 물리 업데이트 이후
yield return WaitForSeconds -> 지정된 시간 이후
yield return WaitForEndOfFrame -> 프레임의 주요 업데이트 이후
코루틴은 단순히 “별도의 스레드에서 실행되는 함수”가 아니다.
Unity의 메인 스레드에서 실행되며 yield를 통해 실행을 중단하고 이후 특정 시점에 다시 이어가는 방식이다.
Rendering
LateUpdate 이후에는 렌더링과 관련된 여러 단계가 진행된다.
전체 렌더링 과정은 현재 사용하는 렌더 파이프라인에 따라 달라질 수 있다.
따라서 단순히
LateUpdate -> Rendering
정도로 이해하는 것이 좋다.
특정 렌더링 콜백의 정확한 실행 시점까지 필요하다면 사용 중인 Built-in Render Pipeline, URP, HDRP 등의 문서를 별도로 확인해야 한다.
WaitForEndOfFrame
WaitForEndOfFrame은 프레임의 주요 업데이트 작업이 끝난 뒤 특정 작업을 수행하고 싶을 때 사용할 수 있다.
private IEnumerator Capture()
{
yield return new WaitForEndOfFrame();
// 프레임 종료 시점의 처리
}
스크린 캡처처럼 한 프레임의 렌더링 결과를 기준으로 작업해야 하는 경우 등에 활용할 수 있다.
OnDisable
OnDisable은 객체나 컴포넌트가 비활성화될 때 호출된다.
private void OnDisable()
{
// 비활성화 처리
}
OnEnable과 반대되는 개념으로 생각하면 된다.
OnEnable -> 활성화 시 처리
OnDisable -> 비활성화 시 처리
이벤트 등록과 해제를 함께 구성할 때 특히 유용하다.
OnEnable -> Subscribe
OnDisable -> Unsubscribe
OnDestroy
OnDestroy는 객체가 파괴될 때 호출된다.
private void OnDestroy()
{
// 정리 작업
}
예를 들어 객체가 사용하던 이벤트나 외부 리소스와의 연결을 정리하는 데 사용할 수 있다.
다만 OnDestroy를 모든 정리 작업의 만능 함수처럼 사용하는 것은 좋지 않다.
객체가 비활성화될 때 정리해야 하는 것이라면 OnDisable, 객체가 파괴되는 시점에만 필요한 정리라면 OnDestroy처럼 생명주기의 목적에 맞춰 사용해야 한다.
이벤트 함수의 핵심 흐름
게임 플레이 중 자주 사용하는 흐름만 단순화하면 다음과 같이 볼 수 있다.
Awake -> OnEnable -> Start -> FixedUpdate -> Physics -> Update -> LateUpdate -> Rendering
객체의 상태가 변경되거나 파괴되면 다음과 같은 생명주기로 이어진다.
OnEnable -> OnDisable -> OnEnable -> OnDisable -> OnDestroy
중요한 것은 이 흐름을 단순한 암기 대상으로 보는 것이 아니다.
각 이벤트 함수가 어떤 시점의 시스템에 개입하기 위한 것인지 이해하는 것이 핵심이다.
초기화 -> Awake / Start
활성화 -> OnEnable
물리 -> FixedUpdate
일반 게임 로직 -> Update
후처리 -> LateUpdate
비활성화 -> OnDisable
파괴 -> OnDestroy
Awake와 Start를 구분하는 이유
실무에서 가장 중요한 부분은 Awake와 Start의 구분이다.
예를 들어 다음과 같은 시스템이 있다고 하자.
GameManager
Player
UIManager
Inventory
각 시스템이 서로를 참조하기 시작하면 초기화 순서 문제가 발생하기 쉽다.
GameManager.Start -> Player.Start -> UIManager.Start -> Inventory.Start
이런 순서를 코드에서 당연히 보장된다고 생각하면 안 된다.
초기화 의존성이 존재한다면 명시적인 초기화 구조를 설계해야 한다.
예를 들어 다음과 같이 구분할 수 있다.
Awake -> 객체 내부 초기화
Start -> 다른 객체와 연결
Initialize -> 시스템 전체 초기화
Run -> 실제 게임 로직
더 큰 프로젝트에서는 아예 MonoBehaviour의 생명주기에 모든 초기화 책임을 몰아넣지 않고 별도의 Bootstrap이나 초기화 시스템을 두는 방식도 사용할 수 있다.
Script Execution Order
Unity는 스크립트 간 실행 순서를 직접 설정할 수 있는 기능도 제공한다.
Project Settings의 Script Execution Order를 이용하면 특정 MonoBehaviour 클래스가 다른 클래스보다 먼저 실행되도록 설정할 수 있다. 또한 [DefaultExecutionOrder] attribute를 사용하는 방법도 있다.
예를 들어 다음과 같은 의존성이 있다고 하자.
GameManager -> Player -> UIManager
이 순서가 반드시 보장되어야 한다면 실행 순서를 명시할 수 있다.
하지만 이것 역시 모든 초기화 문제를 해결하는 만능 수단은 아니다.
실행 순서를 강제로 맞추기 시작하면 시스템 간 결합도가 높아질 수 있기 때문이다.
가능하다면
숨겨진 실행 순서 의존성 -> 명시적인 초기화 구조
로 바꾸는 것이 더 좋은 설계다.
마무리
Unity의 이벤트 함수는 단순한 콜백 목록이 아니다.
게임 오브젝트의 생명주기와 Unity의 Player Loop에 맞춰 특정 시점에 호출되는 시스템이다.
핵심만 정리하면 다음과 같다.
Awake -> 자기 자신 초기화
OnEnable -> 활성화 시 처리
Start -> 첫 프레임 이전 초기화
FixedUpdate -> 물리 업데이트
Update -> 일반 게임 로직
LateUpdate -> Update 이후 처리
Rendering -> 렌더링 과정
OnDisable -> 비활성화 처리
OnDestroy -> 객체 파괴 처리
그리고 가장 중요한 원칙은 하나다.
Unity가 보장하지 않는 실행 순서를 코드가 당연히 보장한다고 가정하지 않는다.
특히 Awake, Start, Update에서 다른 객체의 동일한 이벤트 함수가 이미 실행되었다고 가정하는 순간 초기화 순서 문제가 발생하기 쉽다.
이벤트 함수의 순서를 외우는 것보다 객체 간 의존성을 어떻게 설계할 것인가가 더 중요하다.
Unity의 생명주기를 이해한다는 것은 결국 Unity가 언제 내 코드를 호출하는지를 이해하는 것이다.