.NET의 문자열은 유니코드(UTF-16) 기반입니다. 이 글에서는 올바른 비교와 검색을 위한 정규화, 실제 글자 단위 처리, 안전한 인코딩 변환, 파일/스트림과 콘솔 인코딩 설정까지 실무 중심으로 정리합니다.
1. 문자열과 유니코드 핵심
.NET의 string은 UTF-16 코드 유닛(char 16비트)의 시퀀스입니다. 하나의 문자(그래프림)는 하나 이상의 코드포인트로, 또 각 코드포인트는 하나 또는 두 개의 UTF-16 코드 유닛(서로게이트 페어)로 표현될 수 있습니다. 즉 char 개수와 사람이 인식하는 글자 수가 다를 수 있습니다.
문화권에 따라 동등한 문자도 이진 비교로는 다를 수 있습니다. 예: "é"(U+00E9) vs "é"(e + 결합 악센트, U+0301). 이런 경우 정규화 후 Ordinal 비교가 안전합니다.
using System;
using System.Globalization;
using System.Text;
string a = "é"; // U+00E9 (단일 코드포인트)
string b = "e\u0301"; // 'e' + COMBINING ACUTE ACCENT (표기 목적)
Console.WriteLine(a == b); // False (바이너리 비교)
// 정규화 후 Ordinal 비교
var an = a.Normalize(NormalizationForm.FormC);
var bn = b.Normalize(NormalizationForm.FormC);
Console.WriteLine(string.Equals(an, bn, StringComparison.Ordinal)); // True
Console.WriteLine($"a.Length={a.Length}, b.Length={b.Length}");
// 서로게이트 예시: 😀 (U+1F600) 는 UTF-16에서 char 2개
string emoji = "😀";
Console.WriteLine($"emoji.Length={emoji.Length}"); // 2
2. 올바른 비교/검색을 위한 정규화
저장 전/키 생성 전 정규화(FormC 권장)로 표현을 표준화하십시오. 비교는 문화 민감도가 필요하면 CurrentCulture, 정확한 바이트 등가성이 필요하면 Ordinal을 사용합니다. 파일 경로, 키, 토큰 비교는 Ordinal이 안전합니다.
using System;
using System.Globalization;
string userInput = "Cafe\u0301"; // 'Café'의 분해형 입력 가정
string canonical = userInput.Normalize(NormalizationForm.FormC);
// 사전 저장/색인 전 정규화
SaveToDb(canonical);
// 검색 시에도 동일하게 정규화 후 Ordinal 비교
bool EqualsNormalized(string x, string y)
{
return string.Equals(
x.Normalize(NormalizationForm.FormC),
y.Normalize(NormalizationForm.FormC),
StringComparison.Ordinal);
}
3. 글자 수 세기와 그래프림 처리(Rune, StringInfo)
사용자에게 보이는 글자 단위 처리는 StringInfo 또는 Rune을 사용합니다. StringInfo는 텍스트 요소(그래프림) 단위 열거를, Rune은 코드포인트 단위 처리를 제공합니다.
using System;
using System.Globalization;
using System.Text;
string s = "한🇰🇷e\u0301😀"; // 한글, 국기(조합 플래그), 결합 악센트, 이모지
// 그래프림(텍스트 요소) 단위 순회
var enumerator = StringInfo.GetTextElementEnumerator(s);
int graphemeCount = 0;
while (enumerator.MoveNext())
{
graphemeCount++;
Console.WriteLine($"[{graphemeCount}] '{enumerator.GetTextElement()}'");
}
Console.WriteLine($"Text elements: {graphemeCount}");
// 코드포인트(Rune) 단위 순회
foreach (var rune in s.EnumerateRunes())
{
Console.WriteLine($"Rune U+{rune.Value:X4} '{rune}'");
}
4. 인코딩 변환 기본 패턴(UTF-8 권장)
전송/저장은 UTF-8을 우선하세요. 레거시 코드페이지로 변환 시 CodePagesEncodingProvider 등록이 필요합니다. 데이터 손실 가능성(매핑 불가 문자)을 Fallback으로 제어할 수 있습니다.
using System;
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
string text = "안녕하세요 😀";
// UTF-8 바이트로
byte[] utf8 = Encoding.UTF8.GetBytes(text);
Console.WriteLine(BitConverter.ToString(utf8));
// EUC-KR로 변환 (이모지는 매핑 불가)
var euckr = Encoding.GetEncoding(
"euc-kr",
EncoderFallback.ReplacementFallback, // 매핑 불가 문자는 '?' 등으로 대체
DecoderFallback.ReplacementFallback);
byte[] euckrBytes = Encoding.Convert(Encoding.UTF8, euckr, utf8);
Console.WriteLine(BitConverter.ToString(euckrBytes));
Console.WriteLine(euckr.GetString(euckrBytes)); // 이모지 대체 표시
// 손실 금지: 예외 fallback
var euckrStrict = Encoding.GetEncoding(
"euc-kr",
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
try
{
_ = euckrStrict.GetBytes(text); // 매핑 불가 시 EncoderFallbackException 발생
}
catch (EncoderFallbackException ex)
{
Console.WriteLine("Cannot encode: " + ex.Message);
}
5. 파일/스트림 인코딩과 BOM
UTF-8은 BOM 유무를 선택할 수 있습니다. 상호운용 대상이 BOM을 싫어하면 UTF8Encoding(false)를 사용하세요. StreamReader/Writer에 명시적으로 인코딩을 지정하면 예측 가능성이 높습니다.
using System;
using System.IO;
using System.Text;
string pathBom = "with-bom.txt";
string pathNoBom = "no-bom.txt";
string content = "파일 저장 테스트: 한글, emoji 😀";
var utf8Bom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
// 쓰기
using (var sw = new StreamWriter(pathBom, append: false, encoding: utf8Bom))
sw.Write(content);
using (var sw = new StreamWriter(pathNoBom, append: false, encoding: utf8NoBom))
sw.Write(content);
// 읽기: BOM 자동 감지 켜고, 없으면 지정 인코딩 사용
using (var sr = new StreamReader(pathNoBom, encoding: utf8NoBom, detectEncodingFromByteOrderMarks: true))
{
string read = sr.ReadToEnd();
Console.WriteLine(read);
}
6. 콘솔 입출력 깨짐 방지(Windows)
Windows 콘솔은 기본적으로 지역 코드페이지를 사용합니다. 한글/이모지 깨짐을 방지하려면 UTF-8로 변경하세요.
using System;
using System.Text;
Console.OutputEncoding = new UTF8Encoding(false);
Console.InputEncoding = Encoding.UTF8;
Console.WriteLine("콘솔 UTF-8 출력: 안녕하세요 😀");
7. 간단 BOM 감지기
외부 파일 인코딩을 모를 때 BOM을 간단히 스니핑하고, 없으면 UTF-8을 가정하거나 사용자 선택을 받는 전략이 일반적입니다.
using System;
using System.IO;
using System.Text;
static Encoding? DetectBom(string file)
{
using var fs = File.OpenRead(file);
Span bom = stackalloc byte[4];
int n = fs.Read(bom);
if (n >= 4)
{
if (bom[0] == 0xFF && bom[1] == 0xFE && bom[2] == 0x00 && bom[3] == 0x00) return new UTF32Encoding(bigEndian: false, byteOrderMark: true);
if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == 0xFE && bom[3] == 0xFF) return new UTF32Encoding(bigEndian: true, byteOrderMark: true);
}
if (n >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) return new UTF8Encoding(true);
if (n >= 2)
{
if (bom[0] == 0xFF && bom[1] == 0xFE) return new UnicodeEncoding(bigEndian: false, byteOrderMark: true);
if (bom[0] == 0xFE && bom[1] == 0xFF) return new UnicodeEncoding(bigEndian: true, byteOrderMark: true);
}
return null; // BOM 없음
}
var enc = DetectBom("no-bom.txt") ?? Encoding.UTF8; // 합리적 기본값
using var sr = new StreamReader("no-bom.txt", enc);
Console.WriteLine(enc.WebName);
Console.WriteLine(sr.ReadToEnd());
8. 실무 체크리스트
1) 비교/키/색인 전에 FormC 정규화합니다. 2) 기본 인코딩은 UTF-8을 사용하고, 상호운용 대상 요구에 맞춰 BOM 유무를 결정합니다. 3) 레거시 코드페이지는 Fallback 정책을 명확히 하고, 손실 금지 시 예외 Fallback을 사용합니다. 4) Console/Input/Output 인코딩을 명시합니다. 5) Encoding.Default 사용을 피하고, 항상 명시적으로 Encoding을 전달합니다. 6) 사용자 단위 글자 처리는 StringInfo 또는 Rune로 구현합니다.
요약: 내부는 UTF-16(string) + 정규화, 외부는 UTF-8 우선 + 명시적 Encoding으로 관리하면 대부분의 유니코드/인코딩 문제를 예방할 수 있습니다.
'C#' 카테고리의 다른 글
| C# Environment 클래스 활용해 시스템 정보 가져오기 (1) | 2026.07.14 |
|---|---|
| C# 동적 메서드 호출(DynamicMethod)으로 런타임 성능 개선 (0) | 2026.07.14 |
| C# CollectionView를 이용한 데이터 필터링 및 정렬 (0) | 2026.07.13 |
| C# Stopwatch로 함수 호출 빈도 측정하기 (0) | 2026.07.12 |
| C# 직렬화 시 참조 순환 처리 전략 (0) | 2026.07.12 |