---
title: "DOTween 은 getter 와 setter 두 람다로 어떤 값이든 트윈한다"
description: "코루틴이나 Update 보간은 움직임마다 시작값, 경과 시간, 이징, 적용을 새로 짭니다. DOTween 은 그 보간을 한 번만 짜 두고, 바꿀 값을 getter 와 setter 두 람다로 받아 어떤 값이든 같은 엔진으로 돌립니다."
author: "유니얼"
pubDate: 2026-09-25
updatedDate: 2026-09-25
category: "game-dev/unity/libraries"
tags: ["Unity", "라이브러리", "트윈"]
canonical: https://blog.unialgames.com/posts/unity-dotween-why/
---
```csharp
IEnumerator MoveUpCo(Vector3 to, float duration)
{
    Vector3 from = transform.position;
    for (float t = 0f; t < duration; t += Time.deltaTime)
    {
        float p = t / duration;
        transform.position = Vector3.LerpUnclamped(from, to, 1f - (1f - p) * (1f - p));
        yield return null;
    }
}
```

움직임 하나를 코루틴이나 `Update` 보간으로 만들면 시작값 기억, 경과 시간, 진행률, 이징, 적용이 매번 따라옵니다. 반복이나 되감기, 지연이 붙으면 더 늘어납니다. `Animator` 는 이 코드를 없애 주지만 도착점이 클립에 박혀, 실행 중에 정해지는 목표에는 쓰기 어렵습니다.

DOTween 은 **이 보간을 값의 종류와 상관없이 한 번만 짜 두고, 바꿀 값을 getter 와 setter 두 람다로 받습니다.** 사용자는 무엇을, 어디까지, 몇 초 동안만 말합니다. 기준 판본은 DOTween 1.3.030(GitHub 커밋 `4846808`)입니다.

## 움직임마다 짜던 보간을 한 번만 짠다

같은 "1초 동안 위로 3 만큼, 끝에서 부드럽게" 를 네 방식으로 적으면 이렇습니다.

<x-compare>
<script type="application/json">
{
  "label": "1초 동안 위로 3 만큼, 끝에서 부드럽게 멈추기",
  "tabs": [
    {
      "label": "코루틴",
      "code": "IEnumerator MoveUpCo(Vector3 to, float duration)\n{\n    Vector3 from = transform.position;\n    float t = 0f;\n    while (t < duration)\n    {\n        t += Time.deltaTime;\n        float p = Mathf.Clamp01(t / duration);\n        p = 1f - (1f - p) * (1f - p);   // 이징을 손으로\n        transform.position = Vector3.LerpUnclamped(from, to, p);\n        yield return null;\n    }\n}\n\nStartCoroutine(MoveUpCo(transform.position + Vector3.up * 3f, 1f));",
      "stats": [
        [
          "이징",
          "식을 직접 쓴다"
        ],
        [
          "멈추기 · 끝으로 보내기",
          "StopCoroutine 뿐, 끝값으로 보내려면 따로 짠다"
        ],
        [
          "도는 곳",
          "이 MonoBehaviour"
        ],
        [
          "반복 · 되감기 · 지연",
          "전부 직접"
        ]
      ],
      "tone": "bad"
    },
    {
      "label": "Update 보간",
      "code": "Vector3 from, to;\nfloat elapsed = -1f, duration = 1f;\n\npublic void MoveUp()\n{\n    from = transform.position;\n    to = from + Vector3.up * 3f;\n    elapsed = 0f;\n}\n\nvoid Update()\n{\n    if (elapsed < 0f) return;\n    elapsed += Time.deltaTime;\n    float p = Mathf.Clamp01(elapsed / duration);\n    transform.position = Vector3.LerpUnclamped(from, to, 1f - (1f - p) * (1f - p));\n    if (p >= 1f) elapsed = -1f;\n}",
      "stats": [
        [
          "이징",
          "식을 직접 쓴다"
        ],
        [
          "상태",
          "움직임마다 필드 네다섯 개"
        ],
        [
          "도는 곳",
          "이 MonoBehaviour 의 Update"
        ],
        [
          "반복 · 되감기 · 지연",
          "전부 직접"
        ]
      ],
      "tone": "bad"
    },
    {
      "label": "Animator",
      "text": "애니메이션 클립에 position 키를 찍고, Animator Controller 에 상태를 만들어 트리거로 넘긴다. 에디터에서 모양을 보며 다듬기에는 좋지만 키 값은 클립 에셋에 박혀 있다. 도착점이 실행 중에 정해지는 값(클릭한 자리, 다른 오브젝트의 위치)이면 클립만으로는 표현하기 어렵다.",
      "stats": [
        [
          "이징",
          "커브 편집기"
        ],
        [
          "도착점",
          "클립에 고정"
        ],
        [
          "도는 곳",
          "Animator 컴포넌트"
        ],
        [
          "코드에서 만들기",
          "어렵다"
        ]
      ]
    },
    {
      "label": "DOTween",
      "code": "transform.DOMove(transform.position + Vector3.up * 3f, 1f)\n         .SetEase(Ease.OutQuad);",
      "stats": [
        [
          "이징",
          "이름으로 고른다"
        ],
        [
          "멈추기 · 끝으로 보내기",
          "Kill · Complete · Pause · Rewind"
        ],
        [
          "도는 곳",
          "[DOTween] 오브젝트 하나가 전부"
        ],
        [
          "반복 · 되감기 · 지연",
          "SetLoops · SetDelay"
        ]
      ],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

코루틴과 `Update` 보간은 모양이 같고, 요구가 붙을 때마다 분기가 늘어납니다. DOTween 쪽은 끝값을 코드에서 계산해 넘기고, 이징과 반복을 한 줄씩 붙이며, 이 컴포넌트에는 `Update` 가 없습니다. 보간 코드는 라이브러리 안에 한 벌만 있습니다.

## 바꿀 값은 두 람다로 받는다

"어떤 필드든 트윈한다" 를 리플렉션으로 하면 느리고, DOTween 이 나온 2014년의 .NET 3.5 수준 Mono 와 iOS AOT 에서는 위험했습니다. 그래서 DOTween 은 값을 **읽는 법과 쓰는 법**을 델리게이트로 받습니다. `transform.DOMove` 는 그 두 람다를 대신 써 주는 확장 메서드일 뿐입니다.

```csharp
// DOTween/ShortcutExtensions.cs
public static TweenerCore<Vector3, Vector3, VectorOptions> DOMove(this Transform target, Vector3 endValue, float duration, bool snapping = false)
{
    TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.position, x => target.position = x, endValue, duration);
    t.SetOptions(snapping).SetTarget(target);
    return t;
}
```

라이브러리는 값의 이름도 주인도 모른 채 시간만 계산해 setter 를 부릅니다. 단축 메서드가 없는 값은 같은 모양으로 직접 넘깁니다.

```csharp
int score;
DOTween.To(() => score, x => score = x, 1000, 2f);   // 0 에서 1000 까지 2초
```

값은 제네릭 필드 `T2 startValue, endValue, changeValue` 에 들어가서 갱신 중에 박싱이 없습니다. 대신 트윈 하나를 만들 때마다 두 람다가 힙에 올라갑니다. 이 설계는 **어떤 값이든 같은 엔진으로 돌리는 대신 만들 때 할당을 치르는** 쪽을 골랐습니다.

## 첫 트윈이 구동기를 세운다

2014년에는 한 프레임의 단계에 끼어드는 PlayerLoop API 가 공개되지 않았습니다(2018.1 에 공개). DOTween 은 `MonoBehaviour` 하나를 만들어 그 `Update` 에서 모든 트윈을 돌리고, 그 오브젝트는 **첫 트윈을 만드는 순간** 생깁니다.

<x-wiring>
<script type="application/json">
{
  "label": "첫 DOMove 한 번이 구동기를 세우고 값이 흐르기까지",
  "width": 760,
  "height": 330,
  "nodes": [
    {
      "id": "code",
      "title": "transform.DOMove",
      "sub": "첫 호출",
      "x": 0,
      "y": 8,
      "w": 180,
      "h": 54
    },
    {
      "id": "dt",
      "title": "DOTween",
      "sub": "InitCheck · AutoInit",
      "x": 290,
      "y": 8,
      "w": 180,
      "h": 54
    },
    {
      "id": "set",
      "title": "DOTweenSettings",
      "sub": "Resources 폴더의 에셋",
      "x": 580,
      "y": 8,
      "w": 170,
      "h": 54
    },
    {
      "id": "mgr",
      "title": "TweenManager",
      "sub": "활성 트윈 배열",
      "x": 0,
      "y": 150,
      "w": 180,
      "h": 54
    },
    {
      "id": "comp",
      "title": "[DOTween]",
      "sub": "DOTweenComponent",
      "x": 290,
      "y": 150,
      "w": 180,
      "h": 54
    },
    {
      "id": "mod",
      "title": "DOTweenModuleUtils",
      "sub": "Modules 폴더의 .cs",
      "x": 580,
      "y": 150,
      "w": 170,
      "h": 54
    },
    {
      "id": "tr",
      "title": "transform.position",
      "sub": "setter 가 쓰는 자리",
      "x": 0,
      "y": 272,
      "w": 180,
      "h": 54,
      "aside": true
    }
  ],
  "edges": [
    {
      "from": "code",
      "to": "dt",
      "label": "DOTween.To",
      "bend": 0
    },
    {
      "from": "dt",
      "to": "set",
      "label": "Resources.Load",
      "bend": 0
    },
    {
      "from": "dt",
      "to": "comp",
      "label": "Create",
      "bend": 0
    },
    {
      "from": "comp",
      "to": "mod",
      "label": "리플렉션으로 Init",
      "bend": 0
    },
    {
      "from": "dt",
      "to": "mgr",
      "label": "GetTweener",
      "bend": 0
    },
    {
      "from": "comp",
      "to": "mgr",
      "label": "Update 마다",
      "bend": 0
    },
    {
      "from": "mgr",
      "to": "tr",
      "label": "setter(x)",
      "bend": 0
    }
  ],
  "steps": [
    {
      "edge": 0,
      "why": "DOMove 는 두 람다를 만들어 DOTween.To 에 넘깁니다. To 의 첫 줄이 InitCheck 입니다."
    },
    {
      "edge": 1,
      "why": "아직 초기화 전이고 플레이 중이면 AutoInit 이 Resources 에서 DOTweenSettings 에셋을 읽습니다. 에셋이 없으면 코드의 기본값으로 갑니다."
    },
    {
      "edge": 2,
      "why": "[DOTween] 이라는 GameObject 를 새로 만들고 DontDestroyOnLoad 를 겁니다. 씬이 바뀌어도 남습니다."
    },
    {
      "edge": 3,
      "why": "AddComponent 가 부른 Awake 에서, 코어 DLL 이 컴파일 시점에 알 수 없는 모듈 코드를 이름으로 찾아 Init 을 부릅니다."
    },
    {
      "edge": 4,
      "why": "그다음에야 트윈 객체를 하나 얻어 활성 배열 끝에 넣습니다. 이 순간에는 아직 값이 바뀌지 않습니다."
    },
    {
      "edge": 5,
      "why": "이후 매 프레임 DOTweenComponent.Update 가 TweenManager.Update 를 부릅니다. LateUpdate, FixedUpdate 도 각자 맡은 트윈을 돌립니다."
    },
    {
      "edge": 6,
      "why": "매니저가 시간을 계산해 트윈마다 setter 를 부릅니다. DOTween 은 position 이라는 이름을 모릅니다. 받은 람다를 부를 뿐입니다."
    }
  ],
  "note": "사용자가 아무것도 설치하지 않아도 첫 호출이 구동기를 세웁니다. 이 오브젝트 하나가 모든 트윈을 돌립니다."
}
</script>
</x-wiring>

`DOTween.Init()` 을 직접 부르면 이 과정을 원하는 때에 미리 하고, 인자로 준 값이 설정 에셋보다 우선합니다. 부르지 않으면 첫 트윈이 대신 부릅니다. 사용자 컴포넌트에는 트윈을 돌리는 코드가 하나도 남지 않습니다.

## 코어는 DLL, 유니티 모듈 코드는 .cs

설치 폴더에서 코어는 `DOTween.dll` 이고, 그 옆 `Modules` 폴더의 `DOTweenModule*.cs` 는 소스 그대로입니다. 코어는 `Transform`, `Vector3`, `Color` 처럼 버전마다 거의 안 바뀐 타입만 써서 한 DLL 을 모든 유니티 버전에 줄 수 있습니다. `Rigidbody`, `Image`, `AudioMixer` 는 프로젝트에서 뺄 수 있고 API 가 바뀌므로, 사용자 프로젝트에서 컴파일되는 `.cs` 로 빼고 define 으로 감쌌습니다.

| 종류 | define | 기본 |
| --- | --- | --- |
| 유니티 내장 시스템 | `DOTWEEN_NOAUDIO`, `DOTWEEN_NOPHYSICS`, `DOTWEEN_NOPHYSICS2D`, `DOTWEEN_NOSPRITES`, `DOTWEEN_NOUI` | define 이 없으니 켜짐 |
| 외부 에셋이나 선택 기능 | `DOTWEEN_UITOOLKIT`, `DOTWEEN_TEXTMESHPRO`, `DOTWEEN_TK2D` 등 | 꺼짐 |

내장 시스템은 거의 모든 프로젝트에 있으니 "끄는 define" 으로 아무 설정 없이 켜 두고, 없는 프로젝트에서 켜면 컴파일이 깨지는 외부 에셋은 "켜는 define" 으로 둡니다. 코어는 모듈을 컴파일 시점에 알 수 없으므로 위 그림의 `Awake` 처럼 `DOTweenModuleUtils` 를 이름으로 찾아 초기화합니다.

## 주의할 점

### 1. 모듈 define 을 한 플랫폼에서만 손으로 지우지 않는다

**안 지키면 생기는 문제.** Utility Panel 은 모듈 define 을 유효한 모든 BuildTargetGroup 에 같이 넣고, 모든 그룹에 있어야 "켜짐" 으로 봅니다. Player Settings 에서 한 플랫폼의 define 만 지우면 패널은 꺼졌다고 보여 주는데 다른 플랫폼에서는 그 모듈이 여전히 컴파일됩니다. 플랫폼마다 쓸 수 있는 확장 메서드가 달라져 한쪽에서만 빌드가 깨집니다.

**이렇게 쓴다.** 모듈은 패널에서만 켜고 끕니다. 설정 에셋이 원본이라, 패널에서 바꾸면 에셋과 모든 플랫폼의 define 이 함께 맞춰집니다.

### 2. DOTweenSettings 에셋을 Resources 밖으로 옮기지 않는다

**안 지키면 생기는 문제.** 런타임은 첫 트윈에서 `Resources.Load("DOTweenSettings")` 로 설정을 읽습니다. 에셋을 `Resources` 가 아닌 곳으로 옮기면 찾지 못하고 코드의 기본값으로 돕니다. 패널에서 바꿔 둔 재활용, SafeMode, 로그 수준 같은 기본값이 조용히 빠집니다.

**이렇게 쓴다.** 에셋은 `Resources/DOTweenSettings` 그대로 둡니다. 코드로 확실히 정하고 싶은 값은 `DOTween.Init(...)` 인자로 넘깁니다. 인자가 에셋보다 우선합니다.

### 3. 기본 이징을 InOutQuad 로 알고 SetEase 를 빼지 않는다

**안 지키면 생기는 문제.** `DOTween.defaultEaseType` 의 XML 주석은 `InOutQuad` 라고 적지만 실제 초기값과 설정 에셋의 기본값은 `OutQuad` 입니다. 주석을 믿고 이징을 비워 두면 천천히 출발할 것이라 기대한 트윈이 빠르게 출발합니다.

**이렇게 쓴다.** 곡선 모양이 중요한 트윈에는 `SetEase` 를 명시합니다. 프로젝트 전체의 기본을 바꾸려면 설정 에셋이나 `DOTween.defaultEaseType` 을 게임 시작 때 정합니다.

## 정리

DOTween 은 **보간을 한 번만 짜 두고, 바꿀 값을 getter 와 setter 두 람다로 받아** 어떤 값이든 트윈합니다.

- 코루틴이나 `Update` 보간에서 움직임마다 짜던 코드를 "무엇을, 어디까지, 몇 초" 한 줄로 바꿉니다. 값은 제네릭 필드에 있어 갱신 중 박싱이 없고, 대신 만들 때 람다를 할당합니다.
- 첫 트윈이 `DontDestroyOnLoad` 오브젝트 `[DOTween]` 을 만들고, 그 `MonoBehaviour` 가 모든 트윈을 돌립니다.
- 코어는 버전과 무관한 DLL, 유니티 하위 시스템에 기대는 코드는 define 으로 켜고 끄는 모듈 `.cs` 입니다.