---
title: "\"123\" 은 숫자 123 이 아니다"
description: "입력으로 받은 것은 언제나 문자열입니다. Parse 는 바꿀 수 없으면 프로그램을 멈추고, TryParse 는 못 바꿨다고 알려주기만 합니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-parse-tryparse/
---
나이를 입력받아 한 살을 더해 보겠습니다.

```csharp
Console.Write("나이: ");
string age = Console.ReadLine();     // 20 을 입력했습니다

Console.WriteLine(age + 1);          // 201
```

`21` 이 아니라 `201` 입니다. 오타도 버그도 아니고, 코드가 시킨 대로 한 것입니다.

## `"123"` 안에 들어 있는 것

앞에서 글자마다 번호가 붙어 있다고 했습니다. 문자 `'1'` 의 번호는 49 입니다.
`'0'` 이 48이니 그다음입니다.

그러니까 문자열 `"123"` 이 메모리에 들고 있는 것은 이렇습니다.

<figure class="diagram">
<svg viewBox="0 0 680 240" role="img" aria-label="문자열 123 은 문자 세 개의 번호 49 50 51 을 담고 있고 정수 123 은 2진수 1111011 을 담고 있다는 비교 그림">
<text x="30" y="30" font-size="14" fill="var(--text-muted)">string "123"</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="30" y="44" width="60" height="48" rx="6"></rect>
<rect x="94" y="44" width="60" height="48" rx="6"></rect>
<rect x="158" y="44" width="60" height="48" rx="6"></rect>
</g>
<g font-family="var(--font-mono)" font-size="18" text-anchor="middle" fill="var(--text)">
<text x="60" y="75">'1'</text>
<text x="124" y="75">'2'</text>
<text x="188" y="75">'3'</text>
</g>
<g font-family="var(--font-mono)" font-size="13" text-anchor="middle" fill="var(--accent)">
<text x="60" y="112">49</text>
<text x="124" y="112">50</text>
<text x="188" y="112">51</text>
</g>
<text x="30" y="140" font-size="13" fill="var(--text-muted)">칸 세 개, char 하나가 2바이트니 6바이트</text>
<text x="30" y="184" font-size="14" fill="var(--text-muted)">int 123</text>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="30" y="196" width="188" height="30" rx="6"></rect>
</g>
<text x="124" y="217" font-family="var(--font-mono)" font-size="14" text-anchor="middle" fill="var(--accent)">0000 0000 ... 0111 1011</text>
<line x1="300" y1="30" x2="300" y2="230" stroke="var(--border)" stroke-dasharray="4 4"></line>
<g font-size="14" fill="var(--text-muted)">
<text x="330" y="70">글자 세 개가 나란히 놓인 것입니다.</text>
<text x="330" y="94">계산에 쓸 수 있는 수가 아닙니다.</text>
<text x="330" y="180">숫자 하나가 2진수로 들어 있습니다.</text>
<text x="330" y="204">더하기 곱하기가 되는 것은 이쪽뿐입니다.</text>
</g>
</svg>
<figcaption>같은 123 처럼 보여도 메모리에 들어 있는 비트가 아예 다릅니다.</figcaption>
</figure>

앞에서 이렇게 정리했습니다. **메모리에는 비트만 있고, 그걸 무엇으로 읽을지는 타입이 정한다.**
`"123"` 과 `123` 은 같은 것을 다르게 읽는 것도 아니고, 아예 다른 비트가 들어 있는 것입니다.

## `+` 는 양쪽을 보고 할 일을 정한다

앞에서 연산자를 다룰 때 `+` 가 두 가지 일을 한다고 했습니다.
어느 쪽인지는 **양옆에 무엇이 있느냐**로 정해집니다.

```csharp
Console.WriteLine(1 + 2);       // 3     양쪽이 숫자면 덧셈
Console.WriteLine("1" + "2");   // 12    양쪽이 문자열이면 이어붙이기
Console.WriteLine("20" + 1);    // 201   한쪽이 문자열이면 이어붙이기
```

세 번째가 처음 예제의 정체입니다. **한쪽이 문자열이면 다른 쪽도 문자열로 바꿔서 이어붙입니다.**
`1` 이 `"1"` 이 되고, `"20"` 뒤에 붙어 `"201"` 이 됩니다.

에러가 안 나기 때문에 더 골치입니다. 앞에서 본 좁은 칸으로 가는 변환은 컴파일러가
막아 세우기라도 하는데, 이건 조용히 되고 결과만 이상합니다.

## 입력은 언제나 문자열이다

`Console.ReadLine()` 은 무엇을 입력하든 `string` 을 돌려줍니다.
숫자를 입력했다고 숫자가 되지 않습니다. 파일에서 읽거나 통신으로 받은 것도 마찬가지입니다.

계산하려면 **바꿔야 합니다.** 바꾸려는 타입 이름 뒤에 `Parse` 를 붙입니다.

```csharp
int n = int.Parse("123");
double d = double.Parse("3.14");
bool flag = bool.Parse("true");
```

## `Parse` 는 못 바꾸면 터진다

문제는 바꿀 수 없는 값이 들어왔을 때입니다.

```csharp
int.Parse("abc");            // FormatException
int.Parse("");               // FormatException
int.Parse("3.5");            // FormatException    int 자리에 소수점
int.Parse("99999999999");    // OverflowException  int 범위를 넘음
```

마지막 것은 앞에서 본 정수의 범위와 이어집니다. `int` 가 담을 수 있는 범위 밖이라
**조용히 한 바퀴 도는 게 아니라 예외로 알려줍니다.** 캐스팅과 다른 점입니다.
`(int)` 는 잘라내고 넘어가지만 `Parse` 는 멈춥니다.

예외가 나면 프로그램이 그 자리에서 죽습니다. 코드를 짠 사람 입장에서는 확실해서 좋지만,
**사용자가 오타 한 번 냈다고 프로그램이 종료되면 안 됩니다.**

## `TryParse` 는 알려주기만 한다

같은 일을 하되 실패했을 때 행동이 다릅니다. **터지는 대신 `false` 를 돌려줍니다.**

<x-flow>
<script type="application/json">
{
  "label": "TryParse 가 성공할 때와 실패할 때",
  "lines": [
    "string input = Console.ReadLine();",
    "if (int.TryParse(input, out int n))",
    "{",
    "    Console.WriteLine(n * 2);",
    "}",
    "else",
    "{",
    "    Console.WriteLine(\"숫자만 입력해주세요.\");",
    "}"
  ],
  "cases": [
    {
      "label": "21 을 입력",
      "state": "아직 입력 전",
      "path": [
        { "line": 0, "why": "입력은 언제나 문자열로 들어옵니다.", "state": "input = \"21\"" },
        { "line": 1, "why": "숫자로 바꿀 수 있으므로 n 에 담고 true 를 돌려줍니다.", "state": "input = \"21\", n = 21", "verdict": "true" },
        { "line": 3, "why": "이제 n 은 진짜 숫자라 곱셈이 됩니다.", "state": "input = \"21\", n = 21", "out": "42" }
      ],
      "skip": [5, 6, 7, 8]
    },
    {
      "label": "스물하나 를 입력",
      "state": "아직 입력 전",
      "path": [
        { "line": 0, "why": "이것도 그냥 문자열입니다.", "state": "input = \"스물하나\"" },
        { "line": 1, "why": "숫자로 못 바꿉니다. n 에 0 을 넣고 false 를 돌려줍니다.", "state": "input = \"스물하나\", n = 0", "verdict": "false" },
        { "line": 7, "why": "프로그램은 죽지 않고 여기로 옵니다.", "state": "input = \"스물하나\", n = 0", "out": "숫자만 입력해주세요." }
      ],
      "skip": [2, 3, 4]
    }
  ],
  "note": "실패하면 n 에 0 이 들어갑니다. 그래서 n 이 0 인지로 성공 여부를 판단하면 안 됩니다. 사용자가 진짜로 0 을 입력했을 때와 구분되지 않습니다."
}
</script>
</x-flow>

`out int n` 이 낯설 텐데, **"결과를 담을 변수를 여기 줄 테니 채워 달라"** 는 뜻입니다.
`TryParse` 가 돌려주는 값은 성공했는지 여부(`bool`)라서, 바꾼 숫자를 받을 자리가 따로 필요합니다.
`out` 이 정확히 어떻게 동작하는지는 메서드를 다룰 때 봅니다.

주의할 게 하나 있습니다. **실패하면 `n` 에 `0` 이 들어갑니다.**
그래서 `n == 0` 으로 실패를 판단하면 안 됩니다. 사용자가 진짜 `0` 을 입력한 경우와 같아집니다.
판단은 항상 돌려받은 `bool` 로 합니다.

## 될 때까지 다시 묻기

`TryParse` 가 진가를 발휘하는 자리는 앞에서 본 반복문과 만날 때입니다.

```csharp
int n;

while (!int.TryParse(Console.ReadLine(), out n))
{
    Console.WriteLine("숫자만 입력해주세요.");
}

Console.WriteLine($"{n} 의 두 배는 {n * 2}");
```

**바꾸는 데 실패하는 동안 계속 다시 묻습니다.** 성공하면 조건이 `false` 가 되어 빠져나오고,
그 시점에 `n` 에는 제대로 된 숫자가 들어 있습니다.
`Parse` 로는 이렇게 못 씁니다. 첫 오타에서 이미 프로그램이 없습니다.

정리하면 기준은 이렇습니다.

> **바깥에서 들어온 값은 `TryParse`, 코드 안에서 만든 확실한 값은 `Parse`.**

사용자 입력, 파일, 통신으로 받은 것은 전부 바깥에서 온 값입니다.

## 반대 방향은 그냥 된다

숫자를 문자열로 바꾸는 것은 실패할 일이 없어서 `TryToString` 같은 게 없습니다.

```csharp
int n = 1234567;

string a = n.ToString();          // "1234567"
string b = $"{n}";                // "1234567"   앞에서 본 보간
string c = n.ToString("N0");      // "1,234,567" 형식 지정자도 그대로 쓸 수 있습니다
```

앞에서 본 형식 지정자를 `ToString` 에 그대로 넘길 수 있습니다.
보간의 콜론 뒤에 적던 것과 같은 문자열입니다.

## 정리

- 입력으로 받은 것은 언제나 문자열이다. 숫자를 입력해도 숫자가 되지 않는다.
- `"123"` 은 문자 세 개의 번호를 담고 있고, `123` 은 2진수 하나를 담고 있다. 다른 비트다.
- `+` 는 한쪽이 문자열이면 이어붙인다. `"20" + 1` 은 `"201"` 이다.
- 바꾸려는 타입 이름 뒤에 `Parse` 를 붙인다. `int.Parse`, `double.Parse`.
- `Parse` 는 못 바꾸면 예외를 던진다. 범위를 넘겨도 예외다. 캐스팅처럼 잘라내지 않는다.
- `TryParse` 는 터지는 대신 `false` 를 돌려주고, 결과는 `out` 변수에 담아 준다.
- 실패하면 `out` 변수에 `0` 이 들어간다. 성공 여부는 반드시 돌려받은 `bool` 로 판단한다.
- 바깥에서 들어온 값은 `TryParse`, 코드 안에서 만든 확실한 값은 `Parse`.