---
title: "유니티에서 프레임이 가끔 튀는 이유"
description: "평균은 멀쩡한데 몇 초에 한 번 걸립니다. 대개 GC 인데, 그 GC 가 지금 유니티에서 갈아 끼워지는 중입니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/unity/runtime"
tags: ["Unity", "최적화", "GC"]
canonical: https://blog.unialgames.com/posts/unity-frame-spike/
---
프로파일러를 켜면 대부분의 프레임이 16밀리초 안에 끝납니다. 그런데 몇 초에 한 번 30밀리초짜리가 섞입니다.
평균만 보면 문제가 없어 보이는데 화면에서는 확실히 걸립니다.

그 튀는 프레임을 열어 보면 `GC.Collect` 같은 이름이 붙어 있는 경우가 많습니다.

여기까지는 알려진 이야기입니다. 그다음이 문제입니다.
검색해서 나오는 글마다 답이 다릅니다. 세대가 어쩌고 하는 글도 있고, 세대 같은 건 없다는 글도 있습니다.

둘 다 맞습니다. **유니티에는 GC 가 하나가 아니기 때문입니다.**

> **내 프로젝트가 어느 백엔드로 도는지부터 확인해야 합니다. 그걸 모르면 남의 답을 가져다 쓰게 됩니다.**

## 백엔드가 셋이다

C# 코드는 그대로인데 그걸 실행하는 런타임이 여럿입니다. 그리고 런타임마다 GC 가 다릅니다.

| 백엔드 | 무엇 | GC |
| --- | --- | --- |
| Mono | 지금까지의 기본. 에디터와 데스크톱 | Boehm. 보수적·비압축·비세대 |
| IL2CPP | C# 을 C++ 로 바꿔 빌드. 모바일·콘솔은 대개 이쪽 | Boehm. 보수적·비압축·비세대 |
| CoreCLR | 새로 들어오는 것. 요즘 .NET 이 쓰는 그 런타임 | 정밀·압축·세대 |

지금 어느 것으로 도는지는 프로젝트 설정에서 확인합니다.
`Edit > Project Settings > Player > Configuration > Scripting Backend` 입니다.

여기서 갈립니다. **세대 이야기가 나오는 글은 CoreCLR 이나 일반 .NET 을 보고 쓴 것이고,
세대가 없다는 글은 Boehm 을 보고 쓴 것입니다.**

## 지금 벌어지는 일

유니티가 Mono 를 걷어내고 CoreCLR 로 갈아타는 중입니다.
6.7 에서 실험적으로 들어왔고, **6.8 에서 Mono 가 아예 없어집니다.**

그런데 여기에 반드시 알아야 할 단서가 하나 붙습니다.

> **IL2CPP 는 이 변화의 영향을 받지 않습니다. 계속 Boehm 을 씁니다.**

유니티 문서에 그대로 적혀 있는 내용입니다. IL2CPP 의 Boehm GC 동작에는 변경이 없다고요.

그러니까 이렇게 됩니다.

- 데스크톱으로 빌드하고 CoreCLR 을 쓴다 → 세대 GC 가 온다
- **모바일로 빌드한다 → IL2CPP 라서 계속 Boehm 이다.** 세대 GC 는 안 온다

모바일 게임을 만들고 있다면 "곧 좋아진다" 는 이야기가 내 이야기가 아닐 수 있습니다.
이 시리즈를 쓰는 이유의 절반이 이것입니다.

## 어느 쪽이든 안 변하는 것

다행히 바닥에 깔린 두 가지는 런타임이 달라도 같습니다.

**하나. GC 를 부르는 것은 시간이 아니라 할당입니다.**
`new` 를 했는데 자리가 없을 때 그 자리에서 돕니다. 한가할 때 알아서 도는 것이 아닙니다.
그래서 프레임 한가운데서 돌고, 그 프레임이 튑니다.

**둘. 뿌리에서 손이 닿는 것만 살아남습니다.**
지금 실행 중인 메서드의 지역 변수, static 필드, 그런 것들이 뿌리입니다.
거기서 참조를 따라가 닿은 것은 살고 나머지는 지워집니다.

그러니 대응도 어느 쪽이든 같습니다. **할당을 줄이면 됩니다.**
GC 를 빠르게 만드는 것은 우리 손을 떠난 일이지만, GC 를 덜 부르는 것은 우리가 할 수 있습니다.

## 힙은 늘어나기만 한다

한 가지 더 알아 둘 것이 있습니다. 이건 유니티 특유의 성질입니다.

관리 힙에 자리가 모자라면 힙이 확장됩니다. 그런데 **한 번 늘어난 힙은 거의 안 줄어듭니다.**
안이 텅 비어도 유니티는 그 크기를 그냥 들고 있습니다.
확장할 때는 직전에 늘린 만큼의 두 배씩 늘립니다.

그래서 이런 일이 생깁니다.

로딩 화면에서 한 번 크게 할당했다고 해 봅시다. 힙이 그만큼 커집니다.
로딩이 끝나고 그 객체들이 전부 죽어도 **힙 크기는 그대로**입니다.
게임 내내 안 쓰는 메모리를 들고 있게 됩니다.

즉 힙 크기를 정하는 것은 평균 사용량이 아니라 **가장 나빴던 한순간**입니다.
피크를 한 번 찍으면 그 값이 남습니다.

## 정리

- 유니티에는 GC 가 하나가 아닙니다. **내 프로젝트가 어느 백엔드로 도는지부터 확인해야** 남의 답을 가져다 쓰지 않습니다
- Mono 와 IL2CPP 는 Boehm 입니다. 보수적·비압축·비세대입니다
- CoreCLR 은 정밀·압축·세대이고, 데스크톱과 iOS 에 실험적으로 들어와 있습니다
- 관리 힙은 한 번 늘면 거의 안 줄어듭니다. 힙 크기를 정하는 것은 평균이 아니라 **가장 나빴던 한순간**입니다
- 그래서 로딩 화면의 피크 한 번이 게임 내내 따라다닙니다