---
title: "문자는 숫자로 저장된다"
description: "'A' 가 왜 하필 65인지, 대문자와 소문자가 왜 32 차이인지. 아스키코드에서 유니코드까지 봅니다."
author: "유니얼"
pubDate: 2026-09-08
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-char-and-ascii/
---
앞에서 이런 걸 봤습니다.

```csharp
int  n = 65;
char c = (char)65;

Console.WriteLine(n);   // 65
Console.WriteLine(c);   // A
```

같은 비트를 `int` 로 읽으면 65고 `char` 로 읽으면 `A` 입니다.

그런데 **왜 하필 65일까요.** 누가 정한 걸까요.

## 컴퓨터는 글자를 모른다

지금까지 본 대로 메모리에는 0과 1밖에 없습니다.
그러니 글자를 저장하려면 방법은 하나뿐입니다.

> **글자마다 번호를 붙여두고, 그 번호를 저장합니다.**

`A` 를 저장하는 게 아니라 `A` 의 번호인 65를 저장합니다.
화면에 그릴 때 다시 번호를 보고 `A` 모양을 찾아 그립니다.

문제는 **누가 그 번호를 정하느냐**입니다.
회사마다 다르게 정하면 다른 컴퓨터에서 글이 깨집니다.
그래서 표준이 필요했고, 그게 **아스키코드**입니다.

## 아스키코드 (ASCII)

1960년대에 만들어진 표입니다. **7비트로 128개의 글자에 번호를 붙였습니다.**
영어 알파벳과 숫자, 기호, 그리고 몇 가지 제어 신호가 전부입니다.

| 번호 | 무엇 | 예시 |
| --- | --- | --- |
| 0 ~ 31 | 제어 문자 (화면에 안 보임) | 줄바꿈, 탭 |
| 32 | 공백 | ` ` |
| 48 ~ 57 | 숫자 문자 | `'0'` ~ `'9'` |
| 65 ~ 90 | 대문자 | `'A'` ~ `'Z'` |
| 97 ~ 122 | 소문자 | `'a'` ~ `'z'` |

**`'A'` 가 65인 것은 그냥 그렇게 정한 것**입니다. 계산으로 나오는 수가 아닙니다.

직접 확인해 보세요. 비트를 눌러 65를 만들면 `A` 가 뜹니다.
97을 만들면 `a` 가 뜹니다.

<x-bits>
<script type="application/json">
{
  "label": "비트를 눌러 문자 만들기",
  "bits": 8,
  "value": 65,
  "showChar": true,
  "note": "65에서 97로, 97에서 65로 바꿔보세요. 비트 하나만 바뀝니다. 자릿값이 32인 그 칸입니다."
}
</script>
</x-bits>

### 이 번호에는 규칙이 있습니다

아무렇게나 붙인 번호가 아닙니다. **순서대로 붙였고, 구간을 나눠 붙였습니다.**
그래서 계산이 됩니다.

**첫째, 같은 종류는 이어져 있습니다.**

`'A'` 부터 `'Z'` 까지가 65~90으로 끊김 없이 이어집니다.
그래서 범위 검사가 그냥 부등호로 됩니다.

```csharp
if (c >= 'a' && c <= 'z')
{
    Console.WriteLine("소문자입니다");
}
```

**둘째, 숫자 문자는 48부터입니다.**

`'7'` 이라는 **문자**와 `7` 이라는 **숫자**는 다릅니다. `'7'` 의 번호는 55입니다.
그런데 `'0'` 이 48이니 빼주면 진짜 숫자가 나옵니다.

```csharp
char c = '7';

Console.WriteLine(c - '0');   // 7   (55 - 48)
Console.WriteLine((int)c);    // 55  (문자의 번호)
```

**셋째, 대문자와 소문자는 정확히 32 차이입니다.**

`'A'` 는 65, `'a'` 는 97입니다. 이게 우연이 아닙니다.

<figure class="diagram">
<svg viewBox="0 0 680 196" role="img" aria-label="대문자 A와 소문자 a의 비트가 자릿값 32인 한 칸만 다른 그림">
<text x="110" y="71" text-anchor="end" fill="var(--text)" font-size="14" class="bit">'A' = 65</text>
<text x="110" y="141" text-anchor="end" fill="var(--text)" font-size="14" class="bit">'a' = 97</text>
<g fill="var(--bg)" stroke="var(--border)">
<rect x="130" y="40" width="52" height="48" rx="5"></rect>
<rect x="188" y="40" width="52" height="48" rx="5"></rect>
<rect x="304" y="40" width="52" height="48" rx="5"></rect>
<rect x="362" y="40" width="52" height="48" rx="5"></rect>
<rect x="420" y="40" width="52" height="48" rx="5"></rect>
<rect x="478" y="40" width="52" height="48" rx="5"></rect>
<rect x="536" y="40" width="52" height="48" rx="5"></rect>
<rect x="130" y="110" width="52" height="48" rx="5"></rect>
<rect x="188" y="110" width="52" height="48" rx="5"></rect>
<rect x="304" y="110" width="52" height="48" rx="5"></rect>
<rect x="362" y="110" width="52" height="48" rx="5"></rect>
<rect x="420" y="110" width="52" height="48" rx="5"></rect>
<rect x="478" y="110" width="52" height="48" rx="5"></rect>
<rect x="536" y="110" width="52" height="48" rx="5"></rect>
</g>
<rect x="246" y="40" width="52" height="48" rx="5" fill="none" stroke="var(--warn)" stroke-width="2"></rect>
<rect x="246" y="110" width="52" height="48" rx="5" fill="none" stroke="var(--warn)" stroke-width="2"></rect>
<g text-anchor="middle" font-size="16" class="bit" fill="var(--text-muted)">
<text x="156" y="71">0</text><text x="214" y="71">1</text>
<text x="272" y="71" fill="var(--warn)">0</text>
<text x="330" y="71">0</text><text x="388" y="71">0</text><text x="446" y="71">0</text>
<text x="504" y="71">0</text><text x="562" y="71">1</text>
<text x="156" y="141">0</text><text x="214" y="141">1</text>
<text x="272" y="141" fill="var(--warn)">1</text>
<text x="330" y="141">0</text><text x="388" y="141">0</text><text x="446" y="141">0</text>
<text x="504" y="141">0</text><text x="562" y="141">1</text>
</g>
<g text-anchor="middle" font-size="11" class="bit" fill="var(--text-muted)">
<text x="156" y="180">128</text><text x="214" y="180">64</text>
<text x="272" y="180" fill="var(--warn)">32</text>
<text x="330" y="180">16</text><text x="388" y="180">8</text><text x="446" y="180">4</text>
<text x="504" y="180">2</text><text x="562" y="180">1</text>
</g>
</svg>
<figcaption>대문자와 소문자는 비트 하나만 다릅니다. 그 칸의 자릿값이 32입니다.</figcaption>
</figure>

**비트 하나를 켜고 끄는 것으로 대소문자가 바뀝니다.**
표를 만들 때 일부러 이렇게 배치한 것입니다.

```csharp
char upper = 'A';
char lower = (char)(upper + 32);   // 'a'
```

> 실제 코드에서는 이렇게 쓰지 마세요. `char.ToLower(c)` 를 쓰면 됩니다.
> 32를 더하는 방식은 **아스키 범위의 영어에서만** 맞고 한글이나 다른 언어에서는 깨집니다.
> 여기서는 번호가 어떻게 배치되어 있는지를 보려고 든 예입니다.

## 128자로는 모자랐다

아스키코드는 영어를 위한 표입니다. **한글도 한자도 들어갈 자리가 없습니다.**

그래서 나라마다 자기 방식을 만들었고, 같은 번호가 나라마다 다른 글자를 가리키게 됐습니다.
한글 문서를 다른 컴퓨터에서 열면 깨지던 시절의 이야기입니다.

이걸 해결하려고 만든 게 **유니코드(Unicode)** 입니다.
**세상의 모든 글자에 번호를 하나씩 붙이자**는 계획입니다.

- 아스키의 128자는 **번호를 그대로 물려받았습니다.** `'A'` 는 유니코드에서도 65입니다
- 한글 `'가'` 는 44032번입니다
- 이모지도 번호가 있습니다

여기서 용어가 둘로 갈립니다. 헷갈리기 쉬운 자리입니다.

| | 무엇 |
| --- | --- |
| **유니코드** | 글자마다 번호를 정해둔 **표** |
| **UTF-8 · UTF-16** | 그 번호를 **바이트로 적는 방법** |

번호가 44032라고 해서 메모리에 그 숫자를 어떻게 넣을지가 저절로 정해지지는 않습니다.
그걸 정하는 게 UTF-8 이나 UTF-16 입니다.

**C# 의 `char` 는 UTF-16 한 조각이고 2바이트입니다.**
앞에서 `char` 가 2바이트였던 이유가 이것입니다. 아스키만 담을 거면 1바이트로 충분한데,
한글과 한자까지 담아야 해서 두 배를 씁니다.

```csharp
Console.WriteLine((int)'A');   // 65
Console.WriteLine((int)'가');   // 44032
```

## `char` 는 숫자처럼 계산된다

번호가 붙어 있으니 `char` 는 계산이 됩니다.

```csharp
char c = 'A';

Console.WriteLine(c + 1);           // 66     int 로 계산됩니다
Console.WriteLine((char)(c + 1));   // B      다시 char 로 읽으면
```

`c + 1` 의 결과가 `char` 가 아니라 `int` 인 게 눈에 걸릴 겁니다.
앞에서 본 대로 연산자는 값을 만드는데, `char` 끼리 더하면 넘칠 수 있으니
C# 은 결과를 `int` 로 넓혀서 줍니다. `char` 로 되돌리려면 직접 캐스팅해야 합니다.

이 성질로 알파벳을 쭉 찍어볼 수 있습니다.

```csharp
for (char c = 'A'; c <= 'Z'; c++)
{
    Console.Write(c);   // ABCDEFGHIJKLMNOPQRSTUVWXYZ
}
```

## `'A'` 와 `"A"` 는 다르다

앞에서 잠깐 봤던 이야기를 여기서 마무리합니다.

| | 따옴표 | 담는 것 | 크기 |
| --- | --- | --- | --- |
| `char` | 작은따옴표 `'A'` | 문자 **하나** | 2바이트 고정 |
| `string` | 큰따옴표 `"A"` | 글자들 | 글자 수에 따라 다름 |

```csharp
char c = 'A';       // 됩니다
char c = "A";       // 컴파일 에러
char c = 'AB';      // 컴파일 에러. 하나만 들어갑니다

string s = "A";     // 됩니다
string s = "";      // 빈 글도 됩니다
```

`char` 는 한 칸이고 `string` 은 묶음입니다. **빈 `char` 는 없지만 빈 `string` 은 있습니다.**

## 화면에 안 보이는 문자들

아스키 표의 0~31번은 **모양이 없는 문자**입니다. 줄을 바꾸거나 탭으로 띄우라는 신호입니다.

키보드로 칠 수 없으니 역슬래시로 적습니다. 이걸 **이스케이프 문자**라고 합니다.

| 적는 법 | 뜻 | 번호 |
| --- | --- | --- |
| `\n` | 줄바꿈 | 10 |
| `\t` | 탭 | 9 |
| `\\` | 역슬래시 문자 그 자체 | 92 |
| `\"` | 큰따옴표 | 34 |
| `\'` | 작은따옴표 | 39 |

```csharp
Console.WriteLine("첫 줄\n둘째 줄");
Console.WriteLine("이름\t점수");
Console.WriteLine("그가 \"안녕\" 이라고 했다");
```

역슬래시가 특별한 뜻을 가지므로, **역슬래시 자체를 쓰려면 두 번** 써야 합니다.
파일 경로에서 이게 성가십니다.

```csharp
string path = "C:\\Users\\Documents";     // 두 번씩
string path = @"C:\Users\Documents";      // @ 를 붙이면 그대로
```

`@` 를 앞에 붙이면 역슬래시를 특별하게 보지 않습니다. 경로를 쓸 때 편합니다.

## 정리

- 컴퓨터는 글자를 모른다. 글자마다 번호를 붙여두고 그 번호를 저장한다.
- 아스키코드는 그 번호를 정한 표다. 7비트로 128자를 담았다.
- `'A'` 는 65, `'a'` 는 97, `'0'` 은 48이다. 같은 종류는 번호가 이어져 있다.
- 대문자와 소문자는 비트 하나 차이다. 그 자릿값이 32다.
- 128자로 모자라서 유니코드가 나왔다. 아스키 번호는 그대로 물려받았다.
- 유니코드는 번호를 정한 표고, UTF-16 은 그 번호를 바이트로 적는 방법이다.
- `char` 는 UTF-16 한 조각이라 2바이트다.
- `'A'` 는 `char`, `"A"` 는 `string` 이다. 따옴표가 다르다.