[PART6.배열과 문자열 기본(10/14)] 보간 자리 표시자 안 줄바꿈 (C# 11) — LINQ 체이닝을 한 줄에 욱여넣지 않아도 된다
C# 10 이전엔 $"..." 중괄호 안에서 줄바꿈 불가 / C# 11부터 자유 / 컴파일 결과 IL은 한 줄과 완전히 동일 / LINQ·삼항 연산자 분해에 직접적인 가독성 향상 / 문자열 리터럴 부분의 줄바꿈은 여전히 원시 문자열 """..."""이 필요
목차
1. C# 10이 풀어 주지 못한 한 가지 — 자리 표시자 안 줄바꿈
C# 6.0이 보간 $"..."을 도입한 뒤로, 한 가지 답답한 제약이 오랫동안 남아 있었습니다 — 자리 표시자 {...} 안에서 줄바꿈 사용 불가. LINQ 체이닝이나 복잡한 삼항 표현식을 보간 안에 넣으려면 한 줄에 강제로 욱여넣어야 했습니다.
// C# 10 이전 — 강제 한 줄
return $"주문 {order.Id} — {order.Items.Count} 개, 총액 {order.Items.Sum(i => i.Price * i.Quantity):N0}원";
LINQ가 길어지면 한 줄이 200자를 넘어가고, 코드 리뷰에서 가로 스크롤이 시작됩니다. C# 11이 이 작은 제약을 풀어 줘서, 자리 표시자 안에서 자유롭게 줄바꿈을 쓸 수 있게 됐습니다.
// C# 11+ — 자리 표시자 안 줄바꿈
return $"주문 {order.Id} — {
order.Items.Count} 개, 총액 {
order.Items.Sum(i => i.Price * i.Quantity):N0}원";
이 변화는 컴파일러 단의 파서 확장이고, IL은 한 줄짜리 코드와 완전히 동일하게 생성됩니다. 즉 런타임 비용은 0 — 순수하게 가독성만 향상되는 신문법입니다.
2. 무엇이 가능해졌는가
비유 — 코드 작성 공간이 한 줄에서 여러 줄로 늘어남
C# 10까지는 보간 자리 표시자 안이 "한 줄 짜리 작업장"이었습니다. 책상 한 줄 안에 작업을 다 끝내야 해서 LINQ나 조건식을 작은 글씨로 욱여넣어야 했습니다. C# 11부터는 그 작업장 칸막이가 사라져 자리 표시자 안에서도 여러 줄로 식을 펼쳐 쓸 수 있게 됐습니다.
구체적 패턴 1 — LINQ 체이닝 분해
public static string OrderSummary(Order order)
{
return $"주문 {order.Id} — {
order.Items.Count} 개, 총액 {
order.Items.Sum(i => i.Price * i.Quantity):N0}원";
}
위 코드의 자리 표시자 세 개:
{order.Id}— 짧음{order.Items.Count}— 한 줄{order.Items.Sum(i => i.Price * i.Quantity):N0}— 길어서 다음 줄로 분리
세 번째 자리 표시자처럼 LINQ 체이닝이 길 때 다음 줄로 옮겨 적기가 가능해졌습니다. 형식 지정자(:N0)도 그대로 적용됩니다.
구체적 패턴 2 — 다단 조건식
public static string Status(int score)
{
return $"점수: {score} ({(
score >= 90 ? "최우수" :
score >= 70 ? "우수" :
score >= 50 ? "양호" : "재시험"
)})";
}
삼항 연산자가 4단계로 중첩되어 있는데, 한 줄로 적으면 가독성이 떨어집니다. C# 11에서는 자리 표시자 안에서 각 분기를 한 줄씩 적을 수 있어 의도가 한눈에 들어옵니다.
삼항 안의:은 보간 형식 지정자로 오해될 수 있음 컴파일러가score >= 90 ? "최우수" : "보통"의 가운데:을 보간 형식 지정자로 잘못 해석할 수 있다. 위 예시처럼 외곽을(...)로 한 번 더 묶으면 모호성이 사라진다. 줄바꿈과 별개로 C# 10 이전부터 적용되던 규칙.
3. IL — 줄바꿈은 컴파일러가 그대로 무시한다
줄바꿈 있는 코드와 없는 코드의 IL 비교
// 줄바꿈 있음 (C# 11+)
public static string A(Order order) =>
$"주문 {order.Id} — {
order.Items.Count} 개";
// 줄바꿈 없음 (등가)
public static string B(Order order) =>
$"주문 {order.Id} — {order.Items.Count} 개";
// A의 IL (B와 한 글자도 다르지 않다)
IL_0006: call instance void DefaultInterpolatedStringHandler::.ctor(int32, int32)
IL_000d: ldstr "주문 "
IL_0012: call instance void DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_001b: callvirt instance int32 Order::get_Id()
IL_0020: call instance void DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_0028: ldstr " — "
IL_002d: call instance void DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_0036: callvirt instance List`1<Item> Order::get_Items()
IL_003b: callvirt instance int32 List`1<Item>::get_Count()
IL_0040: call instance void DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
IL_0048: ldstr " 개"
IL_004d: call instance void DefaultInterpolatedStringHandler::AppendLiteral(string)
...
IL은 줄바꿈을 인지하지 못합니다. 컴파일러의 파서가 자리 표시자 안의 화이트스페이스(공백·탭·줄바꿈)를 모두 동일하게 처리하기 때문에, 줄바꿈 있는 코드와 없는 코드는 같은 IL로 컴파일됩니다.
이는 변환 규칙이 동일하다는 뜻이고, 줄바꿈이 런타임 성능에 어떤 영향도 주지 않는다는 보증입니다.
4. 문자열 리터럴 부분은 여전히 한 줄 — 또는 원시 문자열
무엇이 안 되는가
C# 11의 줄바꿈 허용은 자리 표시자 {...} 안에 한정됩니다. 문자열 리터럴 부분(자리 표시자 사이의 정적 텍스트)에서는 여전히 줄바꿈이 컴파일 오류입니다.
// ❌ 일반 보간 — 리터럴 부분에 줄바꿈 불가 (C# 10·11 모두)
string s = $"한국어
번역: {text}"; // 컴파일 오류
// ✅ 원시 문자열 + 보간 — 둘 다 자유
string s = $"""
한국어
번역: {text}
""";
여러 줄 텍스트가 필요하면 원시 문자열 """...""" 또는 축자 보간 $@"..."을 써야 합니다. 이는 PART 6 #9 "문자열 포맷팅 총정리"에서 다뤘습니다.
자리 표시자 줄바꿈 vs 원시 문자열의 역할 분담
| 줄바꿈 위치 | 어떤 표기를 쓰는가 |
|---|---|
자리 표시자 {...} 안 (C# 11+) |
일반 보간 $"..."도 OK |
| 문자열 리터럴 부분 | 원시 보간 $"""...""" 또는 축자 보간 $@"..." |
| 둘 다 자유롭게 | 원시 보간 $"""...""" (가장 강력) |
LINQ 체이닝만 길어졌다면 일반 보간 + 자리 표시자 줄바꿈으로 충분합니다. 텍스트 전체가 여러 줄(예: JSON 템플릿)이라면 원시 보간을 쓰는 게 더 자연스럽습니다.
5. 실전 적용 — 가독성이 살아나는 코드
Before/After: Unity UI 라벨 빌드
// ❌ Before (C# 10 이전 또는 한 줄 강제)
healthText.text = $"HP: {currentHp}/{maxHp} ({(float)currentHp / maxHp * 100:N1}%) [{string.Join(",", buffs.Where(b => b.IsActive).Select(b => b.Name))}]";
// ✅ After (C# 11+ 자리 표시자 줄바꿈)
healthText.text = $"HP: {currentHp}/{maxHp} ({
(float)currentHp / maxHp * 100:N1}%) [{
string.Join(",", buffs.Where(b => b.IsActive).Select(b => b.Name))
}]";
각 자리 표시자가 어떤 식인지 한눈에 들어옵니다. 코드 리뷰 시점에서 변수명·연산·LINQ 체이닝을 분리해서 따로따로 검토할 수 있게 됩니다.
Before/After: 로그 메시지
// ❌ Before
Debug.Log($"Player {player.Name} (Level {player.Level}) defeated {enemies.Count(e => e.IsDead)} enemies in {Time.time - startTime:F2}s");
// ✅ After
Debug.Log($"Player {player.Name} (Level {player.Level}) defeated {
enemies.Count(e => e.IsDead)} enemies in {
Time.time - startTime:F2}s");
긴 LINQ나 계산식이 들어간 자리만 다음 줄로 옮기면 됩니다 — 짧은 변수는 그대로 한 줄에 둡니다.
안 써도 되는 경우
자리 표시자가 모두 짧은 변수라면 그냥 한 줄로 쓰는 게 더 보기 좋습니다. 줄바꿈은 자리 표시자 식이 길어졌을 때만 쓰면 충분합니다.
// ❌ 짧은데 굳이 줄바꿈
$"{x}+{y}={
x + y}"
// ✅ 한 줄로
$"{x}+{y}={x + y}"
6. 함정과 주의사항
함정 1 — C# 10 이하 빌드 환경에서는 컴파일 오류
C# 11이 아직 일반화되기 전이라 일부 프로젝트는 C# 10 또는 그 이전을 쓰고 있을 수 있습니다. <LangVersion> 설정을 확인하세요.
<!-- .csproj — C# 11 이상 명시 -->
<PropertyGroup>
<LangVersion>11</LangVersion>
</PropertyGroup>
Unity는 2022.3 LTS부터 C# 9, 2023.x부터 C# 10, 6000.x부터 C# 11+를 점진적으로 지원합니다. 사용 중인 Unity 버전이 C# 11을 지원하는지 확인이 필요합니다.
함정 2 — 들여쓰기는 정렬되지 않는다
원시 문자열은 종료 """의 위치에 따라 들여쓰기를 자동으로 빼주지만, 일반 보간의 자리 표시자 줄바꿈은 그런 처리를 하지 않습니다. 자리 표시자 안의 줄바꿈은 단지 코드 가독성용이고, 결과 문자열 자체에는 어떤 줄바꿈도 추가되지 않습니다.
string s = $"start {
1 + 2
} end";
// 결과: "start 3 end" — 줄바꿈 없음, 추가 공백 없음
함정 3 — 식 안에서 따옴표 충돌
자리 표시자 안에서 문자열 리터럴을 쓸 때 따옴표가 충돌할 수 있습니다.
// ❌ 컴파일 오류 — 안쪽 "active"가 보간 종료로 해석
$"Status: {(IsOn ? "active" : "inactive")}"
위 코드는 사실 C# 8.0부터 정상 동작합니다 — 보간 안의 따옴표 처리가 개선되었기 때문. 다만 옛 컴파일러에서는 문제가 됐습니다. 현재 환경에선 문제 없습니다.
함정 4 — 줄바꿈이 만든 가독성을 다른 사람이 다시 한 줄로 정리할 수 있음
코드 리뷰 도구나 자동 포매터에 따라서는 자리 표시자 안 줄바꿈을 다시 한 줄로 정리해 버립니다. 팀 컨벤션에 자리 표시자 줄바꿈 정책을 명시하면 일관성이 유지됩니다.
7. C# 버전별 변화
| 버전 | 변화 | 비고 |
|---|---|---|
| 6.0 | 보간 $"..." 도입 / 자리 표시자 안 줄바꿈 불가 |
한 줄 강제 |
| 8.0 | 자리 표시자 안 따옴표 충돌 개선 | |
| 10.0 | DefaultInterpolatedStringHandler 도입 |
박싱 없음 (PART 6 #9) |
| 11.0 | 자리 표시자 안 줄바꿈 허용 | 가독성 개선 |
| 11.0 | 원시 문자열 """...""" (PART 6 #9) |
리터럴 부분도 여러 줄 |
C# 11의 두 신문법 — 자리 표시자 줄바꿈과 원시 문자열 — 이 짝을 이뤄, 보간 문자열의 가독성 한계를 사실상 모두 풀어 줬습니다.
8. 정리
- [ ] C# 11부터 보간
$"..."자리 표시자{...}안에서 줄바꿈 자유 — 긴 LINQ·삼항·계산식을 여러 줄로 나눠 가독성 향상. - [ ] 컴파일 결과 IL은 한 줄과 완전히 동일 — 런타임 비용 0. 순수 가독성 신문법.
- [ ] 문자열 리터럴 부분(자리 표시자 사이 정적 텍스트)은 여전히 한 줄 — 여러 줄 텍스트는 원시 보간
$"""..."""또는 축자 보간$@"...". - [ ] 자리 표시자 안 삼항 연산자는 외곽을
(...)로 묶어:이 형식 지정자로 오해되지 않게. - [ ] 짧은 자리 표시자에 굳이 줄바꿈 쓰지 않기 — 자리 표시자 식이 길어졌을 때만 분해.
- [ ]
<LangVersion>11이상 또는 C# 11 지원 컴파일러 필요. Unity 6000.x 시리즈부터 안정적. - [ ] 자리 표시자 줄바꿈 + 원시 보간 + 박싱 없는 핸들러(C# 10) 세 가지가 모이면 보간 문자열로 거의 모든 형식 표현이 가능.
- [ ] 같은 자리에서 매 프레임 호출되는 코드라면 보간이라도 alloc 1회는 발생 — 변경 감지 + 캐시 SB는 PART 6 #8.