---
title: "빌드 다섯 시간과 셰이더 0.5GB 는 같은 원인이다"
description: "셰이더 변형은 키워드 축의 곱으로 늘어납니다. 너무 많으면 빌드가 안 끝나고, 너무 적으면 화면이 조용히 깨집니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/unity/rendering"
tags: ["Unity", "최적화", "렌더링"]
canonical: https://blog.unialgames.com/posts/unity-shader-variant/
---
셰이더 파일은 하나인데 빌드 결과물에는 여러 개가 들어갑니다.
키워드가 켜진 조합마다 별도의 컴파일 결과가 필요하기 때문입니다.

```hlsl
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _ADDITIONAL_LIGHTS
#pragma multi_compile _ _SHADOWS_SOFT
```

축이 셋이고 각각 켬과 끔이면 **2 × 2 × 2 = 8개** 입니다.
축이 열 개면 1,024개입니다. 그리고 축은 쉽게 열 개가 됩니다.

**곱셈이라는 게 요점입니다.** 키워드 하나를 추가하는 것은 변형을 하나 늘리는 게 아니라
지금 있는 전부를 두 배로 만드는 일입니다.

## 두 선언이 다르다

| | `multi_compile` | `shader_feature` |
| --- | --- | --- |
| 빌드에 넣는 것 | 선언된 조합을 **전부** | 머티리얼에서 **실제로 켜진 것만** |
| 런타임 키워드 토글 | 가능합니다 | 없는 조합이면 폴백합니다 |
| 주 용도 | 파이프라인이 런타임에 전환하는 기능 | 아티스트가 인스펙터에서 켜고 끄는 기능 |
| 변형 폭증 기여 | 큽니다 | 상대적으로 작습니다 |

`shader_feature` 가 안전해 보이지만 **함정이 있습니다.**
미사용분을 지운다는 건 **빌드 시점에 머티리얼을 스캔해서 판단한다**는 뜻입니다.

런타임에 `EnableKeyword` 로 켜려던 키워드를 `shader_feature` 로 선언해 두고
어떤 머티리얼도 그걸 켜 두지 않았다면, 그 변형은 빌드에 **아예 없습니다.**
그러면 폴백이 일어나고 화면이 이상해집니다. 에디터에서는 멀쩡합니다.

## 문제가 네 방향이다

| 상태 | 증상 | 대책 |
| --- | --- | --- |
| 너무 많다 | 클린 빌드 **5시간 이상**, 셰이더 하나에 런타임 메모리 **0.5GB** | 스트리핑 |
| 너무 적다 | 필요한 변형이 없어 다른 변형으로 **조용히 폴백**. 에디터와 다르게 보임 | Strict Matching 으로 검출 |
| 적정한데 미웜업 | 처음 그리는 순간 프레임 스파이크 | 웜업 |
| 파악 자체가 불가능 | 대형 프로젝트에서 흔합니다 | 다이나믹 로딩 |

"너무 적다" 가 있다는 게 중요합니다.
**스트리핑은 많이 지울수록 좋은 게 아닙니다.** 지우면 화면이 깨집니다.

## Strict Shader Variant Matching

Project Settings 에 있는 옵션입니다.
켜면 폴백이 발생한 시점에 **에러 로그를 뱉고 그 오브젝트를 핑크색으로 칠합니다.**

- **개발과 QA 빌드에서는 켭니다.** 조용한 폴백이 눈에 보이는 사고가 됩니다
- **출시 빌드에서는 끕니다.** 사용자 화면에 핑크가 나오면 곤란합니다

자동화된 플레이 테스트에 켜 두면 "이 씬에서 이 변형이 없다" 를 로그로 잡아 줍니다.
사람이 모든 씬을 돌아 볼 필요가 없어집니다.

## 스트리핑

빌드에 무엇을 넣을지 고르는 일이고, 도구가 둘인데 **역할이 완전히 다릅니다.**

| 도구 | 누가 쓰나 | 단위 |
| --- | --- | --- |
| `ShaderKeywordFilter` | 렌더 파이프라인을 직접 만드는 사람 | 키워드 축을 통째로 제거 |
| `IPreprocessShaders` | **애플리케이션 개발자, 즉 우리** | 변형 목록에서 개별 제거 |

우리 쪽은 `IPreprocessShaders` 입니다.
빌드 콜백에서 `IList<ShaderCompilerData>` 를 받아서 안 쓸 것을 지웁니다.

```csharp
class StripVariants : IPreprocessShaders
{
    public int callbackOrder => 0;

    public void OnProcessShader(Shader shader, ShaderSnippetData snippet,
                                IList<ShaderCompilerData> data)
    {
        for (int i = data.Count - 1; i >= 0; i--)
        {
            // 2D 게임인데 3D 그림자 변형이 들어와 있다면 뺍니다
            if (data[i].shaderKeywordSet.IsEnabled(SoftShadows))
                data.RemoveAt(i);
        }
    }
}
```

**뒤에서부터 지우는 것**만 조심하면 됩니다. 앞에서부터 지우면 인덱스가 밀립니다.

지우기 전에 무엇이 실제로 쓰이는지부터 알아야 합니다.
URP 에셋에 이미 있는 스트리핑 옵션을 먼저 확인하고,
그다음 빌드 로그의 변형 수를 보고, 그다음 코드로 갑니다.
**아무 근거 없이 지우면 위의 "너무 적다" 로 갑니다.**

## 실제 효과

| | 전 | 후 |
| --- | --- | --- |
| 셰이더 하나만 쓰는 프로젝트의 셰이더 메모리 | 수십에서 수백 MB | **10MB 이하** |
| 새 빌드 머신에서의 클린 빌드 | **5시간 이상** | **1시간 미만** |

> **두 번째 빌드가 빠른 건 착시입니다.**
> 셰이더 컴파일 결과는 `Library/` 아래에 캐시됩니다.
> 같은 머신에서 두 번째부터는 극적으로 빨라집니다.
> 진짜 빌드 시간은 **새 CI 머신, 클린 클론, `Library` 삭제 후** 의 시간입니다.
> 스트리핑의 가치를 재려면 거기서 재야 합니다.

## 웜업과 다이나믹 로딩

변형 수가 적정해도 **처음 그리는 순간** 프레임이 튑니다.
GPU 프로그램을 그때 만들기 때문입니다.
프로파일러에 `Shader.CreateGPUProgram` 이나 `Shader.CreateGPUPipeline` 로 잡힙니다.

이걸 로딩 화면에서 미리 처리하는 것이 웜업이고,
셰이더 데이터를 압축된 채로 들고 있다가 필요한 부분만 푸는 것이 다이나믹 로딩입니다.

둘 다 그래픽스 API 에 따라 방법이 갈리고 (DX11 계열과 DX12/Metal/Vulkan 계열),
청크 크기와 캐시 개수를 따로 튜닝해야 합니다. 변형 수를 잡는 것과는 다른 작업입니다.

## 정리

- 변형은 키워드 축의 **곱**으로 늘어납니다. 축 하나를 더하면 개수가 두 배가 됩니다
- `multi_compile` 은 선언한 조합을 전부 넣고, `shader_feature` 는 머티리얼이 켜 둔 것만 넣습니다
- **`shader_feature` 가 안전한 것은 아닙니다.** 런타임에 `EnableKeyword` 로 켜려던 변형을
  어떤 머티리얼도 안 켜 뒀으면 빌드에 없고, 조용히 폴백이 일어나 화면이 깨집니다
- 방향이 넷입니다. 너무 많음(빌드 시간과 셰이더 용량) · 너무 적음(조용한 폴백) ·
  적정한데 미웜업(첫 렌더 스파이크) · 파악 불가(다이나믹 로딩)
- Strict Shader Variant Matching 은 폴백을 **핑크색으로** 칠해 줍니다.
  개발과 QA 빌드에서 켜고 출시 빌드에서 끕니다
- 스트리핑은 `IPreprocessShaders` 쪽이 우리 몫입니다.
  `ShaderKeywordFilter` 는 파이프라인을 만드는 쪽 도구입니다
- **두 번째 빌드가 빠른 건 착시입니다.** `Library/` 가 지워진 상태에서 재야 합니다