
시작하기 전에 #
오늘 소개해 드릴 예제들은 2025년 11월 11일에 공식 출시된 .NET 10과 C# 14 환경이 필요해요. 아직 설치 전이라면 공식 홈페이지에서 내려받을 수 있어요.
준비가 되셨다면, 이제 본격적으로 들어가 볼게요!
왜 C# 14 확장 블록이 필요할까요? #
기존 방식도 충분히 훌륭했지만, 몇 가지 가려운 부분이 있었어요.
- 메서드만 가능: 프로퍼티나 연산자는 확장할 수 없었어요.
- 반복되는 코드: 같은 타입을 확장할 때마다 매번
this TypeName parameter를 써줘야 했죠. - 정적 확장 불가: 인스턴스가 아닌 타입 자체에 정적 멤버를 추가할 수 없었어요.
예를 들어, 리스트가 비었는지 확인할 때 myList.IsEmpty()처럼 괄호를 붙이는 게 가끔은 어색하게 느껴질 때가 있더라고요. 그냥 myList.IsEmpty처럼 프로퍼티로 쓰고 싶다는 생각, 다들 한 번쯤 해보셨을 거예요.
C# 14 확장 블록(Extension Blocks) 핵심 개념과 문법 #
C# 14의 확장 블록은 이 모든 고민을 해결해 줘요. 이제 메서드뿐만 아니라 프로퍼티, 연산자, 정적 멤버까지 한 번에 정의할 수 있답니다.
예제로 보는 변화 #
문자열이 비었는지 확인하는 코드를 새 문법으로 바꿔볼게요.
// 새로운 방식: 확장 블록 사용
public static class EnumerableExtensions
{
extension(IEnumerable source)
{
// 이제 메서드가 아니라 '프로퍼티'로 정의할 수 있어요!
public bool IsEmpty => !source.Any();
}
}문법이 꽤 직관적이죠? extension 키워드 뒤에 확장할 타입과 이름을 적고, 중괄호 안에 원하는 멤버를 넣으면 끝이에요. 호출할 때도 numbers.IsEmpty처럼 자연스럽게 프로퍼티로 접근할 수 있어 훨씬 보기 좋아요.

실전 예제: 문자열 유틸리티 및 프로퍼티 확장 #
실제로 유용하게 쓸 수 있는 문자열 유틸리티 예제를 만들어봤어요.
public static class StringExtensions
{
extension(string str)
{
public bool IsEmpty => string.IsNullOrEmpty(str);
public bool IsValidEmail => str.Contains("@") && str.Contains(".");
public string Truncate(int maxLength)
{
if (str.Length <= maxLength) return str;
return str.Substring(0, maxLength) + "...";
}
}
}이제 email.IsEmpty나 email.IsValidEmail처럼 쓸 수 있어요. 괄호가 사라지니까 코드가 읽기 훨씬 편해지더라고요.
정적 확장 (Static Extensions) #
이게 정말 대박이에요. 인스턴스가 아니라 타입 자체에 멤버를 추가할 수 있거든요. 파라미터 이름을 생략하면 정적 확장이 됩니다.
public static class ListExtensions
{
extension(List)
{
// 타입 자체에서 호출하는 정적 프로퍼티
public static List Empty => new List();
}
}
// 사용 예시
var myNewList = List.Empty;기존에는 불가능했던 방식이라, 팩토리 메서드나 유틸리티 함수를 만들 때 정말 유용할 것 같아요.
확장 블록의 3가지 제약 사항과 한계 #
물론 만능은 아니에요. 몇 가지 기억해 둘 포인트가 있어요.
필드 추가 불가 #
확장 블록은 기존 타입에 ‘기능’을 붙이는 것이지, 새로운 ‘데이터 공간’을 만드는 게 아니에요. 그래서 상태를 저장해야 하는 필드나 자동 구현 프로퍼티는 쓸 수 없답니다.
extension(User user)
{
// ❌ 오류: 확장 블록 내부에서는 필드를 선언할 수 없어요.
private int _accessCount;
// ❌ 오류: 필드가 필요한 자동 구현 프로퍼티도 안 돼요.
public string Nickname { get; set; }
// ✅ 계산된 프로퍼티(로직만 있는 경우)는 가능해요!
public string FullName => $"{user.FirstName} {user.LastName}";
}기존 멤버 우선 #
만약 확장하려는 타입에 이미 같은 이름의 메서드나 프로퍼티가 있다면 어떻게 될까요? C#은 언제나 원본 타입의 멤버를 우선해서 호출해요. 확장이 원본을 덮어쓰거나 가로챌 수는 없다는 점을 꼭 기억해야 해요.
extension(string str)
{
// ⚠️ 원본 string에 이미 Length 프로퍼티가 있죠?
// 이 코드는 에러는 안 나지만, str.Length를 호출하면 항상 원본 값이 나옵니다.
public int Length => 999;
}
string name = "Gemini";
Console.WriteLine(name.Length); // 결과는 999가 아니라 '6'이 나와요!제네릭 제약 #
확장 블록에서 제네릭(T)을 쓸 때는, 그 타입 파라미터가 반드시 확장 대상이 되는 타입(Receiver)에 포함되어 있어야 해요.
// ✅ T가 List<T> 안에 포함되어 있으므로 가능해요.
extension(List<T> list)
{
public void PrintAll() => list.ForEach(Console.WriteLine);
}
// ❌ 오류: T2는 확장 대상인 List<T1> 어디에도 속해있지 않아요.
extension(List<T1> list)
{
public void DoSomething<T2>(T2 extra) { /* ... */ }
}요약하자면 이래요! #
“확장 블록은 타입에 새로운 시각(View)을 제공하는 것이지, 타입의 설계도 자체를 수정하는 것은 아니에요.”
이 세 가지만 기억해도 확장 블록을 쓰면서 겪을 시행착오를 훨씬 줄일 수 있을 거예요.
마무리하며 #
C# 14의 확장 블록은 우리가 코드를 더 ‘C#답게’ 표현할 수 있도록 도와주는 아주 반가운 변화예요. 기존 방식과 섞어서 쓸 수도 있으니, 당장 모든 코드를 바꿀 필요도 없죠.
새로운 .NET 10 환경에서 작은 유틸리티부터 하나씩 적용해 보시는 건 어떨까요? 코드가 한결 가벼워지는 걸 느끼실 수 있을 거예요.
최근 글