---
title: "Time.deltaTime 을 곱하는 이유"
description: "내 컴퓨터에서는 알맞던 캐릭터가 다른 컴퓨터에서는 두 배로 빨라집니다. Update 가 1초에 몇 번 불릴지 모르기 때문이고, deltaTime 은 \"한 번에 얼마\" 를 \"1초에 얼마\" 로 바꿔 줍니다."
author: "유니얼"
pubDate: 2026-11-05
category: "game-dev/math"
tags: ["Unity", "기초"]
canonical: https://blog.unialgames.com/posts/math-deltatime/
---
유니티로 캐릭터를 처음 움직여 보면 이런 일을 겪습니다.
내 컴퓨터에서는 알맞은 속도였는데, 친구 컴퓨터에서는 두 배로 빨리 달리고 휴대폰에서는 느릿느릿 걷습니다.
코드는 한 글자도 안 바꿨습니다.

원인은 **`Update` 가 1초에 몇 번 불릴지 정해져 있지 않다**는 데 있습니다.
어떤 기계는 1초에 120번 부르고, 어떤 기계는 30번 부릅니다.
"불릴 때마다 0.1 씩 가라" 고 적으면 많이 불리는 쪽이 멀리 갑니다.

> **`Time.deltaTime` 은 지난 프레임이 몇 초 걸렸는지입니다.
> 여기에 "1초에 얼마" 를 곱하면 이번 프레임에 갈 거리가 나오고,
> 몇 번 불리든 1초 동안 가는 거리가 같아집니다.**

## Update 는 1초에 몇 번 불리나

**프레임**은 화면에 그리는 그림 한 장입니다.
유니티는 그림 한 장을 그릴 때마다, 그리기 직전에 모든 스크립트의 `Update` 를 한 번씩 부릅니다.
그래서 **Update 가 불리는 횟수 = 그린 그림 장수**입니다.
1초에 그린 장수를 **fps**(frames per second)라고 부릅니다.

이 fps 는 기계마다, 순간마다 다릅니다.
모니터가 1초에 화면을 몇 번 바꾸는지를 **주사율**이라 하고 Hz 로 적습니다. 60Hz 면 1초에 60번입니다.

| 상황 | fps |
| --- | --- |
| 수직 동기화(모니터 주사율에 맞춰 그리는 설정)를 켜고 60Hz 모니터 | 60 |
| 같은 설정으로 144Hz 모니터 | 144 |
| 수직 동기화를 끄고 상한도 안 정함 | 그릴 수 있는 만큼. 수백 |
| 휴대폰에서 따로 정하지 않음 | 30 |
| 무거운 장면, 로딩 중 | 그만큼 떨어짐 |

같은 코드가 어떤 기계에서는 1초에 30번, 어떤 기계에서는 120번 돕니다.
**`Update` 안에 적은 한 줄은 "1초에 한 번" 도 "1초에 60번" 도 아닙니다. "그림 한 장마다 한 번" 입니다.**

## 한 번에 0.1 씩 가면

캐릭터를 오른쪽으로 보내려고 이렇게 썼다고 합시다.

```csharp
void Update()
{
    // 불릴 때마다 오른쪽으로 0.1
    transform.position += new Vector3(0.1f, 0f, 0f);
}
```

1초 동안 몇 번 불렸는지만 세면 1초 뒤 위치가 나옵니다.

| fps | 1초 동안 불린 횟수 | 한 번에 | 1초 뒤 위치 |
| --- | --- | --- | --- |
| 30 | 30 | 0.1 | 0.1 × 30 = **3** |
| 60 | 60 | 0.1 | 0.1 × 60 = **6** |
| 120 | 120 | 0.1 | 0.1 × 120 = **12** |

같은 코드인데 120fps 기계가 30fps 기계보다 네 배 멀리 갑니다.
아래 블록은 게임 시간 1초 동안 세 기계에서 캐릭터가 간 거리입니다. 줄 위의 눈금 하나가 `Update` 한 번입니다.

<x-dt>
<script type="application/json">
{
  "label": "같은 코드를 fps 가 다른 세 기계에서",
  "per": 0.1,
  "speed": 6
}
</script>
</x-dt>

문제는 0.1 이라는 숫자의 뜻입니다. 이 0.1 은 **"프레임 한 장 동안 갈 거리"** 입니다.
그런데 프레임 한 장의 길이가 기계마다 다릅니다. 30fps 의 한 장은 1/30초, 120fps 의 한 장은 1/120초입니다.
길이가 다른 자로 "한 칸씩" 재고 있는 셈입니다.

## deltaTime 은 지난 프레임이 걸린 시간이다

`Time.deltaTime` 은 **바로 전 프레임부터 지금 프레임까지 걸린 시간을 초로** 돌려줍니다.
60fps 로 고르게 돌고 있다면 매번 1/60 ≈ 0.0167 입니다.

여기서 쓰는 것은 학교에서 배운 식 하나입니다.

```text
거리 = 속도 × 시간
```

1초에 6 만큼 가고 싶다면, 이번 프레임 동안 갈 거리는 `6 × 이번 프레임이 걸린 시간` 입니다.
그 시간이 `Time.deltaTime` 입니다.

```csharp
public float speed = 6f;   // 1초에 6

void Update()
{
    // 1초에 6 × 이번 프레임이 걸린 초 = 이번 프레임에 갈 거리
    float step = speed * Time.deltaTime;

    transform.position += new Vector3(step, 0f, 0f);
}
```

세 기계에서 한 단계씩 계산하면 이렇게 됩니다.

| fps | deltaTime | 한 번에 (6 × deltaTime) | 1초 동안 불린 횟수 | 1초 뒤 위치 |
| --- | --- | --- | --- | --- |
| 30 | 1/30 | 0.2 | 30 | 0.2 × 30 = **6** |
| 60 | 1/60 | 0.1 | 60 | 0.1 × 60 = **6** |
| 120 | 1/120 | 0.05 | 120 | 0.05 × 120 = **6** |

**자주 불리는 기계는 조금씩, 드물게 불리는 기계는 한 번에 많이** 갑니다. 그래서 1초 뒤에는 모두 같은 자리에 있습니다.
위 블록의 「deltaTime 곱하기」 코드에서는 눈금 간격이 줄마다 달라도 점이 나란히 갑니다.

왜 항상 맞아떨어지는지는 덧셈으로 보면 바로 보입니다.
1초 동안의 deltaTime 을 전부 더하면, 몇 번으로 나눴든 **1초**입니다.
30fps 는 1/30 을 30번, 120fps 는 1/120 을 120번 더하는데 둘 다 1 입니다.

```text
간 거리 = 6 × dt₁ + 6 × dt₂ + ... + 6 × dtₙ
        = 6 × (dt₁ + dt₂ + ... + dtₙ)
        = 6 × 1초
        = 6
```

곱하기 전과 후에 달라진 것은 **`speed` 의 뜻**입니다.

| | 숫자 | 뜻 | 기계마다 |
| --- | --- | --- | --- |
| 곱하기 전 | 0.1 | 한 프레임에 0.1 | 달라짐 |
| 곱한 뒤 | 6 | 1초에 6 | 같음 |

단위로 읽어도 됩니다. 유니티에서 1 은 보통 1m 로 봅니다.
`6 (m/초) × 0.0167 (초) = 0.1 (m)` 이라 초가 지워지고 거리만 남습니다.
**deltaTime 을 곱한다는 것은 "프레임" 이라는 단위를 "초" 로 바꾸는 일**입니다.

## 프레임 길이가 들쭉날쭉해도 맞는다

실제 게임은 fps 가 고르지 않습니다.
가비지 컬렉션(쓰지 않는 메모리를 치우는 작업)이나 로딩 때문에 **한 프레임만 0.3초씩 걸리는** 일이 생깁니다.
화면이 순간 멈칫하는 그 현상입니다.

deltaTime 은 평균이 아니라 **매 프레임 그 프레임의 길이를 다시 잽니다.**
그래서 긴 프레임 뒤에는 그만큼 큰 값이 들어옵니다.
아래는 0.02초짜리 프레임 사이에 0.3초짜리 프레임 하나가 끼어 있는 기계입니다.

<x-dt>
<script type="application/json">
{
  "label": "한 프레임만 0.3초 걸린 기계",
  "mode": "dt",
  "speed": 6,
  "lanes": [
    { "fps": 60 },
    { "name": "중간에 끊김", "frames": [20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 300, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20] }
  ],
  "note": "아래 줄의 0.2초부터 0.5초까지는 Update 가 한 번도 안 불립니다. 0.5초에 불렸을 때 deltaTime 이 0.3 이라 한 번에 6 × 0.3 = 1.8 을 갑니다."
}
</script>
</x-dt>

아래 줄은 0.3초 동안 멈춰 있다가 **한 번에 1.8 을 건너뜁니다.** 대신 그 뒤로는 위 줄과 같은 자리에 있습니다.
deltaTime 을 곱하지 않았다면 멈춘 0.3초만큼 영영 뒤처졌을 것입니다.

이것이 deltaTime 의 대가이기도 합니다.

- **한 번에 멀리 건너뛸 수 있습니다.** 그 사이에 얇은 벽이 있으면 벽 반대편으로 나가 버립니다
- 그래서 유니티는 deltaTime 에 상한을 둡니다. `Time.maximumDeltaTime` 이고 기본값은 1/3초입니다.
  한 프레임이 1초 걸려도 deltaTime 은 0.333 으로 잘려 들어오고, 그 순간에는 게임 시간이 실제 시간보다 느리게 흐릅니다
- deltaTime 은 **이미 지나간** 프레임의 길이입니다. 이번 프레임이 얼마나 걸릴지는 모르므로, 끊긴 직후 한 프레임이 늘 크게 튑니다

## 어디에 곱하고 어디에 안 곱하나

기준은 하나입니다. **"매 프레임 조금씩 쌓이는 변화" 면 곱하고, 그렇지 않으면 안 곱합니다.**
표의 `rb` 는 물리 엔진으로 움직이는 오브젝트에 붙이는 `Rigidbody` 컴포넌트입니다.

| 코드 | 곱하나 | 왜 |
| --- | --- | --- |
| `transform.position` 을 직접 옮김 | 곱함 | 프레임마다 쌓이는 이동 |
| `transform.Rotate(0, 90 * Time.deltaTime, 0)` | 곱함 | 1초에 90도 |
| `timer += Time.deltaTime` | 더함 | 지난 시간을 모음 |
| `rb.linearVelocity = dir * speed` | 안 곱함 | 속도는 이미 "1초에 얼마". 물리 엔진이 시간을 곱해 옮김 |
| `FixedUpdate` 안의 `rb.AddForce(force)` | 안 곱함 | 기본 방식(`ForceMode.Force`)이 물리 한 걸음의 시간(0.02초)을 알아서 곱함 |
| `Input.GetAxis("Horizontal")` 로 키보드 이동 | 곱함 | −1 ~ 1 은 "얼마나 눌렀나" 일 뿐. 속도와 deltaTime 을 곱해야 거리 |
| `Input.GetAxis("Mouse X")` | 안 곱함 | 이미 "이번 프레임 동안 움직인 양" |
| 점프 순간 속도를 한 번 넣음 | 안 곱함 | 쌓이지 않고 한 번만 일어남 |

자주 틀리는 것은 아래 셋입니다.

- **물리 속도에 곱하기.** `rb.linearVelocity`(Unity 6 이전 이름은 `velocity`)는 "1초에 얼마" 입니다.
  여기에 deltaTime 을 곱하면 속도가 1/60 로 줄어 거의 안 움직이고, fps 에 따라 속도까지 달라집니다
- **마우스 이동량에 곱하기.** 마우스 값은 그 프레임 동안 손이 움직인 거리라서, 프레임이 길면 이미 큰 값이 들어옵니다.
  여기에 deltaTime 을 또 곱하면 시간을 두 번 곱한 것이라 fps 가 낮을수록 시점이 더 많이 돌아갑니다
- **`FixedUpdate` 에서 착각하기.** 물리 계산용 `FixedUpdate` 는 fps 와 상관없이 정해진 간격(기본 0.02초)으로 불립니다.
  그 안에서 `Time.deltaTime` 을 읽으면 자동으로 그 간격인 0.02 가 나옵니다. 값이 늘 같을 뿐, 곱하는 규칙은 같습니다.
  `AddForce` 도 여기서 부릅니다. `Update` 에서 부르면 그림을 많이 그리는 기계일수록 힘이 여러 번 쌓여 더 세게 밀립니다

## 곱해 두면 일시정지가 공짜다

`Time.deltaTime` 에는 `Time.timeScale` 이 이미 곱해져 있습니다.

| timeScale | deltaTime (60fps) | deltaTime 을 곱한 움직임 |
| --- | --- | --- |
| 1 | 0.0167 | 그대로 |
| 0.5 | 0.0083 | 절반 속도. 슬로 모션 |
| 0 | 0 | 멈춤. 일시정지 |

`Time.timeScale = 0` 한 줄로 deltaTime 을 곱한 것은 전부 멈춥니다. 이때도 `Update` 는 계속 불리고, deltaTime 이 0 이라 움직임만 0 이 됩니다.
**곱하지 않은 코드는 일시정지 중에도 계속 움직입니다.** 이동에 deltaTime 을 빠뜨린 캐릭터가 일시정지 화면 뒤에서 걸어가는 것이 그 경우입니다.

반대로 일시정지 메뉴의 애니메이션처럼 **멈추면 안 되는 것**은 `Time.unscaledDeltaTime` 을 씁니다. timeScale 을 곱하지 않은 실제 프레임 시간입니다.

## 정리

- `Update` 는 그림 한 장마다 한 번 불립니다. 1초에 몇 번인지는 기계와 순간마다 다릅니다
- "한 번에 0.1" 처럼 적으면 30fps 는 3, 120fps 는 12 를 갑니다
- `Time.deltaTime` 은 지난 프레임이 걸린 초입니다. `속도 × deltaTime` 이 이번 프레임에 갈 거리입니다
- 1초 동안의 deltaTime 을 다 더하면 1초라서, 몇 번 불리든 1초 동안 간 거리가 같습니다
- 매 프레임 쌓이는 변화에는 곱하고, 이미 "1초에 얼마" 인 물리 속도, 이미 "프레임 동안의 양" 인 마우스 값, 한 번만 일어나는 일에는 곱하지 않습니다
- 긴 프레임 뒤에는 한 번에 멀리 건너뜁니다. 상한은 `Time.maximumDeltaTime` 입니다
- deltaTime 을 곱해 두면 `Time.timeScale` 로 멈추거나 느리게 할 수 있습니다