---
title: "VContainer 는 모노비헤이비어를 꺼 둔 채로 주입하고 켠다"
description: "프리팹을 복제한 뒤 주입하면 Awake 에서 주입 필드가 널입니다. VContainer 는 새로 만드는 컴포넌트를 꺼진 채 복제해 주입한 뒤 켜고, 씬에 있는 것은 스코프의 Awake 안에서 빌드 직후 찾아 채웁니다."
author: "유니얼"
pubDate: 2026-09-24
updatedDate: 2026-09-30
category: "game-dev/unity/libraries"
tags: ["Unity", "DI", "라이브러리"]
canonical: https://blog.unialgames.com/posts/unity-vcontainer-component/
---
```csharp
public class Enemy : MonoBehaviour
{
    [Inject] IHealthService health;
    void Awake() => health.Init(100);
}
```

이 적 프리팹을 두 가지로 복제했습니다(VContainer 1.19.0, 에디터 PlayMode 테스트).

| 복제 방법 | `Awake` 안에서 `health == null` |
| --- | --- |
| `Object.Instantiate(prefab)` 다음에 주입 | True. `Init` 에서 널 참조 |
| `resolver.Instantiate(prefab)` | False. `Init(100)` 성공 |

모노비헤이비어는 `new` 를 못 하니 이미 만들어진 뒤에 `[Inject]` 멤버로만 받습니다. 그래서 VContainer 는 **새로 만드는 것은 꺼진 채 복제해 주입한 뒤 켜서 `Awake` 를 주입 뒤로 미루고, 씬에 있는 것은 스코프의 `Awake` 안에서 빌드 직후 찾아 채웁니다.**

## 모노비헤이비어는 `[Inject]` 가 붙은 멤버로 받는다

생성자를 못 쓰니 받을 자리를 `[Inject]` 로 표시합니다. 자리는 셋입니다.

```csharp
public class Enemy : MonoBehaviour
{
    [Inject] IHealthService health;                 // 필드

    [Inject] public ScoreService Score { get; set; } // 속성

    IAudioService audio;

    [Inject]                                         // 메서드. 이름은 아무거나 된다
    public void Construct(IAudioService audio)
    {
        this.audio = audio;
    }
}
```

`Construct` 는 VContainer 의 API 가 아니라 흔히 쓰는 이름일 뿐입니다. 표시는 `[Inject]` 이고, 메서드 이름이 무엇이든 불립니다.

주입은 **"이미 만들어진 객체에 `[Inject]` 자리를 채워 넣는 일"** 입니다. 인젝터는 그 타입의 `[Inject]` 멤버 목록을 한 번 읽어 두고, 객체가 오면 필드, 속성, 메서드 순으로 채웁니다.

```csharp
// ReflectionInjector.Inject
InjectFields(instance, resolver, parameters);      // [Inject] 필드마다 타입으로 Resolve 해서 대입
InjectProperties(instance, resolver, parameters);  // [Inject] 속성도 같은 방식
InjectMethods(instance, resolver, parameters);     // [Inject] 메서드의 인자마다 Resolve 한 뒤 메서드를 부른다
```

그래서 `Construct(IAudioService audio)` 는 컨테이너가 `IAudioService` 를 꺼내 인자로 넣어 부르는 메서드입니다. 필드보다 메서드로 받으면 무엇에 의존하는지 한 자리에 보이고, 테스트에서 직접 불러 가짜를 넣을 수도 있습니다. 아래에서 말하는 "주입" 은 모두 이 채워 넣기를 언제 하느냐의 문제입니다.

## 새로 만드는 것은 꺼진 채 복제하고 주입한 뒤 켠다

유니티는 켜진 오브젝트를 복제하면 `Instantiate` 호출 안에서 바로 `Awake` 를 부르고, 꺼진 오브젝트는 켤 때 부릅니다. VContainer 는 이 규칙을 씁니다.

<x-flow>
<script type="application/json">
{
  "label": "Enemy 프리팹을 복제할 때 Awake 가 도는 자리",
  "cases": [
    {
      "label": "Object.Instantiate",
      "state": "prefab 활성 · health = null",
      "lines": ["var enemy = Object.Instantiate(prefab);", "container.InjectGameObject(enemy.gameObject);"],
      "path": [
        {"line": 0, "why": "켜진 원본을 복제하니 복제본도 켜진 채로 생깁니다. 유니티가 이 호출 안에서 바로 Awake 를 부릅니다.", "out": "Awake: health == null, Init 에서 널 참조"},
        {"line": 1, "why": "주입은 이제야 들어옵니다. Awake 는 이미 지나갔습니다.", "state": "health = 채워짐 (늦음)", "out": "주입"}
      ],
      "note": "실측: Awake 안에서 health == null 이 True 였습니다."
    },
    {
      "label": "container.Instantiate",
      "state": "prefab 활성 · health = null",
      "lines": ["var wasActive = prefab.gameObject.activeSelf;", "prefab.gameObject.SetActive(false);", "var instance = Object.Instantiate(prefab, position, rotation, scope.transform);", "instance.transform.SetParent(null);", "scope.Container.InjectGameObject(instance.gameObject);", "prefab.gameObject.SetActive(wasActive);        // finally", "instance.gameObject.SetActive(wasActive);      // finally"],
      "path": [
        {"line": 0, "why": "원본이 원래 켜져 있었는지 적어 둡니다.", "state": "wasActive = true"},
        {"line": 1, "why": "원본을 잠깐 끕니다.", "state": "prefab 꺼짐"},
        {"line": 2, "why": "꺼진 원본을 복제하니 복제본도 꺼진 채로 생깁니다. Awake 가 돌지 않습니다.", "state": "instance 꺼짐 · Awake 아직"},
        {"line": 3, "why": "부모만 떼어 냅니다. 씬 소속은 스코프의 씬으로 남습니다."},
        {"line": 4, "why": "꺼진 오브젝트에도 주입은 됩니다. 필드에 값을 넣는 일이라 활성 여부와 상관없습니다.", "state": "health = 채워짐", "out": "주입"},
        {"line": 5, "why": "원본을 되돌립니다. 남의 것이라 빌린 그대로 돌려놓습니다.", "state": "prefab 켜짐"},
        {"line": 6, "why": "복제본을 켭니다. 유니티가 여기서 Awake 를 부릅니다.", "out": "Awake: health.Init(100) 성공"}
      ],
      "note": "실측: 꺼진 채 복제한 직후 Awake 는 돌지 않았고, SetActive(true) 에서 돌았습니다."
    }
  ]
}
</script>
</x-flow>

꺼진 오브젝트에 주입한 뒤 켜도 멀쩡했습니다. **꺼 두는 목적은 주입이 아니라 `Awake` 를 미루는 것**입니다. 원본을 되돌리는 두 줄은 `finally` 라 주입 중 예외가 나도 원본 프리팹이 꺼진 채 남지 않고, 되돌리는 값이 `true` 가 아니라 `wasActive` 라서 원래 꺼져 있던 프리팹은 **주입은 끝났지만 꺼진 채로** 돌아옵니다.

`RegisterComponentInNewPrefab` 과 `RegisterComponentOnNewGameObject` 의 프로바이더도 같은 순서를 씁니다. 새 게임오브젝트는 `AddComponent` 전에 끕니다. 실행 순서 `-5000` 은 씬에 놓인 오브젝트 사이의 이야기라 런타임 복제에는 통하지 않고, 이 순서만이 `Awake` 를 뒤로 미룹니다.

## 씬에 놓인 것은 스코프의 Awake 안에서 채워진다

`RegisterComponent` 와 `RegisterComponentInHierarchy` 는 등록하면서 **자기를 `Resolve` 하는 빌드 콜백을 답니다.** 아무도 생성자로 받지 않아도 빌드 직후 주입이 일어나고, 그 빌드는 스코프의 `Awake` 안입니다.

<x-flow>
<script type="application/json">
{
  "label": "스코프의 Awake 안에서 콜백이 불리는 순서 (줄인 코드)",
  "lines": [
    "protected virtual void Awake()                 // 실행 순서 -5000",
    "{",
    "    builder.RegisterBuildCallback(SetContainer);",
    "    Configure(builder);                        // 컴포넌트 등록이 콜백을 단다",
    "    EnsureDispatcherRegistered(builder);       // 디스패치 콜백이 맨 뒤에 붙는다",
    "    builder.Build();                           // 컨테이너를 만들고 콜백을 단 순서대로 부른다",
    "        SetContainer(container);              // Container 대입, autoInjectGameObjects 주입",
    "        container.Resolve<HudView>();         // RegisterComponent 의 콜백",
    "        container.Resolve<PlayerView>();      // RegisterComponentInHierarchy 의 콜백",
    "        dispatcher.Dispatch();                // IInitializable.Initialize()",
    "}"
  ],
  "cases": [
    {
      "label": "컴포넌트 둘을 등록한 스코프",
      "state": "Configure: RegisterComponent(hudView), RegisterComponentInHierarchy<PlayerView>()",
      "path": [
        {"line": 0, "why": "유니티가 스코프의 Awake 를 부릅니다. 실행 순서를 안 준 스크립트보다 먼저입니다."},
        {"line": 2, "why": "빌드 첫 줄에서 SetContainer 를 콜백으로 답니다. 첫 번째 콜백입니다.", "state": "콜백 [SetContainer]"},
        {"line": 3, "why": "Configure 가 돌면서 컴포넌트 등록마다 강제 Resolve 콜백이 하나씩 붙습니다.", "state": "콜백 [SetContainer, HudView, PlayerView]"},
        {"line": 4, "why": "InstallTo 의 마지막 줄이 디스패처 콜백을 붙입니다.", "state": "콜백 [SetContainer, HudView, PlayerView, Dispatch]"},
        {"line": 5, "why": "컨테이너가 만들어지고 콜백이 단 순서대로 불립니다."},
        {"line": 6, "why": "Container 프로퍼티가 채워지고 autoInjectGameObjects 목록이 주입됩니다.", "out": "1. 자동 주입 목록"},
        {"line": 7, "why": "들고 있던 참조를 꺼내면서 주입합니다.", "out": "2. HudView 주입"},
        {"line": 8, "why": "씬을 뒤져 찾고 주입합니다.", "out": "3. PlayerView 주입"},
        {"line": 9, "why": "진입점의 Initialize 는 그다음입니다. 씬 컴포넌트는 이미 채워져 있습니다.", "out": "4. Initialize"}
      ],
      "note": "씬에 놓인 컴포넌트의 주입은 스코프의 Awake 가 끝나기 전에 전부 끝납니다."
    }
  ]
}
</script>
</x-flow>

그래서 같은 씬에서 함께 로드되고 실행 순서가 `-5000` 보다 뒤인 컴포넌트는 **주입이 자기 `Awake` 보다 먼저**입니다. 이 조건은 쉽게 깨집니다. 실행 순서 0 과 `-6000` 인 컴포넌트를 둘 다 `RegisterComponentInHierarchy` 로 등록하고, 불릴 때마다 로그를 찍었습니다.

<x-wiring>
<script type="application/json">
{
  "label": "실행 순서 -6000 과 0 인 컴포넌트가 주입받는 때",
  "width": 620,
  "height": 806,
  "nodes": [
    {
      "id": "e1",
      "title": "ProbeEarly.Awake",
      "sub": "실행 순서 -6000, 아직 비어 있음",
      "x": 150,
      "y": 8,
      "w": 320,
      "h": 54,
      "aside": true
    },
    {
      "id": "s1",
      "title": "스코프 Awake",
      "sub": "실행 순서 -5000",
      "x": 150,
      "y": 100,
      "w": 320,
      "h": 54
    },
    {
      "id": "s2",
      "title": "Configure",
      "sub": "RegisterComponentInHierarchy 두 건",
      "x": 150,
      "y": 192,
      "w": 320,
      "h": 54
    },
    {
      "id": "i1",
      "title": "Probe 주입",
      "sub": "실행 순서 0, Awake 전",
      "x": 150,
      "y": 284,
      "w": 320,
      "h": 54
    },
    {
      "id": "i2",
      "title": "ProbeEarly 주입",
      "sub": "Awake 는 이미 지나감",
      "x": 150,
      "y": 376,
      "w": 320,
      "h": 54
    },
    {
      "id": "s3",
      "title": "스코프 Awake 끝",
      "sub": "컨테이너 있음",
      "x": 150,
      "y": 468,
      "w": 320,
      "h": 54
    },
    {
      "id": "p1",
      "title": "Probe.Awake",
      "sub": "실행 순서 0, 채워져 있음",
      "x": 150,
      "y": 560,
      "w": 320,
      "h": 54,
      "aside": true
    },
    {
      "id": "e2",
      "title": "ProbeEarly.Start",
      "sub": "채워져 있음",
      "x": 150,
      "y": 652,
      "w": 320,
      "h": 54,
      "aside": true
    },
    {
      "id": "p2",
      "title": "Probe.Start",
      "sub": "채워져 있음",
      "x": 150,
      "y": 744,
      "w": 320,
      "h": 54,
      "aside": true
    }
  ],
  "edges": [
    {
      "from": "e1",
      "to": "s1",
      "bend": 0
    },
    {
      "from": "s1",
      "to": "s2",
      "bend": 0
    },
    {
      "from": "s2",
      "to": "i1",
      "bend": 0
    },
    {
      "from": "i1",
      "to": "i2",
      "bend": 0
    },
    {
      "from": "i2",
      "to": "s3",
      "bend": 0
    },
    {
      "from": "s3",
      "to": "p1",
      "bend": 0
    },
    {
      "from": "p1",
      "to": "e2",
      "bend": 0,
      "label": "첫 프레임"
    },
    {
      "from": "e2",
      "to": "p2",
      "bend": 0
    }
  ],
  "steps": [
    {
      "node": "e1",
      "why": "실행 순서가 -6000 이라 스코프(-5000)보다 먼저 Awake 가 돕니다. 컨테이너가 아직 없어 [Inject] 필드가 비어 있습니다."
    },
    {
      "edge": 0,
      "why": "그다음 스코프의 Awake 가 돕니다."
    },
    {
      "edge": 1,
      "why": "Configure 에서 두 컴포넌트를 RegisterComponentInHierarchy 로 등록합니다."
    },
    {
      "edge": 2,
      "why": "빌드가 끝나며 등록한 컴포넌트에 주입합니다. 실행 순서 0 인 Probe 는 아직 Awake 전입니다."
    },
    {
      "edge": 3,
      "why": "ProbeEarly 도 주입받습니다. 하지만 이 컴포넌트의 Awake 는 이미 빈손으로 지나갔습니다."
    },
    {
      "edge": 4,
      "why": "스코프의 Awake 가 끝납니다. 컨테이너가 섰습니다."
    },
    {
      "edge": 5,
      "why": "실행 순서 0 인 Probe 의 Awake 가 이제야 돕니다. 주입이 먼저 끝나 있어 필드가 채워져 있습니다."
    },
    {
      "edge": 6,
      "why": "첫 프레임의 Start 에서는 ProbeEarly 도 채워져 있습니다."
    },
    {
      "edge": 7,
      "why": "Probe 도 마찬가지입니다."
    }
  ],
  "note": "진한 상자가 스코프가 하는 일, 흐린 상자가 컴포넌트의 Awake 와 Start 입니다. 스코프보다 먼저 도는 -6000 쪽만 Awake 에서 비어 있습니다."
}
</script>
</x-wiring>

`-6000` 쪽은 `Awake` 때 널이었고 `Start` 때는 둘 다 채워져 있었습니다. 실행 순서 말고도 스코프가 부모를 기다리며 대기열에 들어가거나 Auto Run 을 끄고 직접 `Build()` 하면 주입이 `Awake` 뒤로 밀립니다.

같은 참조라도 `RegisterInstance(hudView)` 로 넣으면 인젝터를 만들지 않아 `[Inject]` 멤버가 끝까지 비어 있습니다.

## 씬 탐색은 스코프의 씬에서 처음 찾은 하나를 가져온다

`RegisterComponentInHierarchy` 는 `ApplicationOrigin`, 즉 빌드를 시작한 스코프의 씬만 뒤집니다. 루트 오브젝트를 하이어라키 순서로, 루트 안은 깊이 우선으로, 꺼진 것까지 봅니다.

<x-flow>
<script type="application/json">
{
  "label": "RegisterComponentInHierarchy<PlayerHud>() 가 씬을 뒤지는 순서 (줄인 코드)",
  "lines": [
    "scene.GetRootGameObjects(gameObjectBuffer);",
    "foreach (var gameObject in gameObjectBuffer)",
    "{",
    "    component = gameObject.GetComponentInChildren(componentType, true);",
    "    if (component != null) break;",
    "}",
    "if (component == null)",
    "    throw new VContainerException(componentType, $\"{componentType} is not in this scene ...\");",
    "if (component is MonoBehaviour monoBehaviour)",
    "    var injector = InjectorCache.GetOrBuild(monoBehaviour.GetType());",
    "    injector.Inject(monoBehaviour, resolver, customParameters);"
  ],
  "cases": [
    {
      "label": "둘이 있을 때",
      "state": "루트 [UI, World] · UI 밑에 꺼진 PlayerHud, World 밑에 켜진 PlayerHud",
      "path": [
        {"line": 0, "why": "씬의 루트 오브젝트를 하이어라키 순서대로 버퍼에 담습니다."},
        {"line": 1, "why": "첫 루트 UI 부터 봅니다.", "state": "보는 중: UI"},
        {"line": 3, "why": "꺼진 PlayerHud 도 찾습니다. 두 번째 인자가 true 입니다."},
        {"line": 4, "why": "찾았으니 멈춥니다. World 는 열어 보지도 않습니다.", "verdict": "true"},
        {"line": 6, "verdict": "false"},
        {"line": 8, "why": "모노비헤이비어라 주입 대상입니다.", "verdict": "true"},
        {"line": 9, "why": "등록한 타입이 아니라 실제 런타임 타입으로 인젝터를 고릅니다. 파생 클래스여도 맞게 들어갑니다."},
        {"line": 10, "out": "꺼진 PlayerHud 에 주입"}
      ],
      "skip": [7],
      "note": "켜진 쪽을 원했다면 UnderTransform 으로 범위를 좁혀야 합니다."
    },
    {
      "label": "하나도 없을 때",
      "state": "루트 [UI, World] · 어디에도 PlayerHud 가 없다",
      "path": [
        {"line": 0},
        {"line": 1, "state": "보는 중: UI"},
        {"line": 3},
        {"line": 4, "verdict": "false"},
        {"line": 1, "why": "다음 루트로 넘어갑니다.", "state": "보는 중: World"},
        {"line": 3},
        {"line": 4, "verdict": "false"},
        {"line": 6, "why": "끝까지 못 찾았습니다.", "verdict": "true"},
        {"line": 7, "why": "조용히 넘어가지 않고 예외를 던집니다.", "out": "VContainerException: ... is not in this scene"}
      ],
      "skip": [8, 9, 10],
      "note": "씬 전체를 훑고 나서야 실패를 압니다."
    }
  ]
}
</script>
</x-flow>

순수 `ContainerBuilder` 에서는 `ApplicationOrigin` 이 널이라 `VContainerException` 이 아니라 `NullReferenceException` 이 납니다. 찾을 컴포넌트를 마지막 루트의 마지막 자식에 두고, 전체 탐색과 `.UnderTransform(root)` 로 좁힌 탐색이 스코프 빌드에 더한 시간입니다.

| 씬 오브젝트 수 | 전체 (Mono) | 전체 (IL2CPP) | 좁힘 (두 런타임) |
| --- | --- | --- | --- |
| 약 1,000 | 0.058 ms | 0.056 ms | 0.01 ms 미만 |
| 약 10,000 | 1.04 ms | 0.43 ms | 0.01 ms 미만 |
| 약 30,000 | 3.33 ms | 2.12 ms | 0.01 ms 미만 |

전체 탐색은 씬 크기에 따라 늘고, 등록이 여럿이면 등록마다 처음부터 다시 훑습니다.

## 등록으로 만드는 프리팹은 등록한 컴포넌트 하나만 주입한다

프리팹을 복제해 주입하는 길은 셋입니다. **어디까지 채우느냐**와 **`Awake` 전에 채우느냐**가 다릅니다.

| 방법 | 등록한 컴포넌트 | 같은 오브젝트의 다른 컴포넌트 · 자식 | `Awake` 전에 주입 |
| --- | --- | --- | --- |
| `RegisterComponentInNewPrefab` | 채워짐 | 비어 있음 | 예 |
| `resolver.Instantiate` | 채워짐 | 채워짐 | 예 |
| `Object.Instantiate` 뒤 `InjectGameObject` | 채워짐 | 채워짐 | 아니오 |

`RegisterComponentInNewPrefab` 은 꺼 둔 채 복제하고 주입한 뒤 켜지만, 복제 뒤 부르는 것이 `injector.Inject(component, ...)` 한 줄이라 등록한 타입의 컴포넌트 하나만 채웁니다. `Object.Instantiate` 는 켜진 채 복제되어 `Awake` 가 먼저 돌고 주입은 그 뒤입니다.

여러 컴포넌트를 `Awake` 전에 채우려면 `resolver.Instantiate` 를 씁니다. 등록으로 받고 싶으면 팩토리로 감쌉니다.

```csharp
builder.RegisterFactory<Enemy>(
    resolver => () => resolver.Instantiate(enemyPrefab), Lifetime.Scoped);

// 쓰는 쪽
public EnemySpawner(Func<Enemy> createEnemy) => this.createEnemy = createEnemy;
```

루트와 자식에 `[Inject]` 메서드가 있는 프리팹으로 확인했고, 프리팹 에셋이든 실행 중 만든 템플릿이든, 켜져 있든 꺼져 있든 결과가 같았습니다. 트리 전체 주입은 노드 수에 비례해 노드당 Mono 약 5 µs, IL2CPP 약 2.4 µs, 약 40 B 가 들었습니다.

## 주의할 점

### 1. `Object.Instantiate` 대신 `resolver.Instantiate` 를 쓴다

**안 지키면 생기는 문제.** 켜진 프리팹을 유니티 API 로 복제하면 복제 호출 안에서 `Awake` 가 돌고, 그 뒤의 `InjectGameObject` 는 늦습니다. `Awake` 에서 주입 필드를 쓰면 널 참조입니다.

**이렇게 쓴다.** 프리팹을 만드는 클래스가 `IObjectResolver` 를 받아 `resolver.Instantiate(prefab)` 로 복제합니다. `IObjectResolver` 를 받는 것은 이런 생성 자리로만 한정합니다.

### 2. 씬에 놓인 컴포넌트는 `Awake` 에서 주입 필드를 쓰지 않는다

**안 지키면 생기는 문제.** 스코프가 먼저 깨어나는 동안에는 문제가 안 보이다가, 더 앞선 실행 순서, 부모 대기, Auto Run 끄기 중 하나로 `Awake` 가 주입보다 앞으로 옵니다. `-6000` 을 준 컴포넌트는 `Awake` 때 널이었습니다.

**이렇게 쓴다.** `[Inject]` 메서드는 받은 것을 대입만 하고, 그걸로 하는 초기화는 `Start()` 로 옮깁니다.

### 3. `RegisterComponentInHierarchy` 는 스코프와 같은 씬을 대상으로 쓴다

**안 지키면 생기는 문제.** 애디티브로 연 다른 씬의 컴포넌트는 못 찾고 예외가 납니다. 같은 타입이 둘이면 꺼진 쪽이라도 먼저 걸린 것이 뽑히고, 3만 개 씬에서는 등록 하나에 빌드가 2~3 ms 더 듭니다.

**이렇게 쓴다.** `LifetimeScope` 의 `Configure` 안에서만 쓰고, 찾을 컴포넌트는 스코프와 같은 씬에 둡니다. 위치가 정해져 있으면 `.UnderTransform(root)` 로 좁힙니다.

### 4. 프리팹 자식까지 채우려면 `RegisterComponentInNewPrefab` 을 쓰지 않는다

**안 지키면 생기는 문제.** 등록한 컴포넌트 하나만 주입되므로 프리팹 자식이나 같은 오브젝트의 다른 컴포넌트는 `[Inject]` 가 널로 남습니다. 원래 꺼진 프리팹을 넘기면 복제본도 꺼진 채 돌아와 `Awake` 와 `Start` 가 켜질 때까지 돌지 않습니다.

**이렇게 쓴다.** 트리 전체가 주입받아야 하면 `resolver.Instantiate` 를 씁니다. 등록에 넘기는 프리팹은 켜 둔 채 저장하고, 일부러 꺼 둔 것이면 받은 쪽에서 `SetActive(true)` 를 부릅니다.

## 정리

VContainer 는 **모노비헤이비어를 꺼 둔 채 주입하고 켜서 `Awake` 를 주입 뒤로 미룹니다.**

- 새로 만드는 경로는 모두 꺼진 채 복제하고, 주입하고, 원래 상태대로 켭니다.
- 씬에 놓인 컴포넌트는 스코프 `Awake` 안의 빌드 콜백에서 채워지므로, 스코프보다 늦게 깨어날 때만 `Awake` 전에 주입됩니다.
- `RegisterComponentInHierarchy` 는 스코프의 씬에서 처음 찾은 하나, `RegisterComponentInNewPrefab` 은 등록한 컴포넌트 하나만 채웁니다.