모든 시간 슬롯을 24:00으로 만든 Blazor 클로저 버그 🕛 🕛
Source: Dev.to
신비한 버그
Blazor로 회의실 예약 시스템을 만들던 중 기이한 버그를 마주했습니다. 타임라인 컴포넌트는 오전 6시부터 오후 11시까지의 시간 슬롯을 표시했지만, 어떤 슬롯을 클릭해도 콘솔에 24:00이 표시되었습니다.
// Console output – ALWAYS showed 24!
Time slot selected: 24:0
Start: 06-01-2026 00:00:00, End: 06-01-2026 01:00:00
시각적으로는 타임라인이 정상적으로 보였지만, 모든 버튼이 깨져 있었습니다. 어떻게 오전 6시부터 오후 11시까지의 모든 버튼이 시간으로 “24”를 보내게 된 걸까요?
첫 번째 가정
- 타임존 문제? –
DateTime.Kind, UTC 변환 확인 DateTime파싱 버그? – 광범위한 검증 추가- 브라우저
datetime-local버그? – 여러 브라우저에서 테스트
3시간 동안 디버깅한 뒤, 로그를 더 추가했습니다:
private async Task OnTimeSlotClick(int hour, int minute)
{
Console.WriteLine($"[DEBUG] Clicked hour: {hour}");
// Always showed 24, regardless of which button!
}
버튼을 생성하던 루프는 겉보기엔 완벽했습니다:
@for (int hour = 6; hour < 24; hour++)
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
버튼은 6:00, 7:00, 8:00… 로 표시됐지만, 클릭하면 모두 OnTimeSlotClick(24, 0)이 호출되었습니다!
클로저 캡처 함정
C#에서 람다식은 값이 아니라 변수를 캡처합니다. 버튼이 클릭될 때쯤이면 루프가 이미 종료되어 hour == 24가 됩니다(루프는 hour가 24가 될 때 종료됩니다).
// When loop finishes: hour = 24
// ALL click handlers now reference hour = 24!
해결 방법
1. Enumerable.Range와 foreach 사용
@foreach (var hour in Enumerable.Range(6, 18))
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
foreach의 각 반복은 새로운 변수를 생성하므로 공유 캡처를 피할 수 있습니다.
2. for 루프 내부에 로컬 복사 만들기 (가장 일반적인 해결법)
@for (int hour = 6; hour < 24; hour++)
{
var currentHour = hour;
<button @onclick="() => OnTimeSlotClick(currentHour, 0)">
@currentHour:00
</button>
}
3. 매개변수를 받는 메서드 사용
@for (int hour = 6; hour < 24; hour++)
{
<button @onclick="() => HandleHourClick(hour)">
@hour:00
</button>
}
@code {
private void HandleHourClick(int hour) // ✅ New parameter each call
{
OnTimeSlotClick(hour, 0);
}
}
4. @key를 사용해 강제 재렌더링
@for (int hour = 6; hour < 24; hour++)
{
<button @key="hour" @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
Blazor에서 이 버그가 더 흔히 발생하는 이유
- 이벤트 핸들러는 나중에 실행되는 람다식입니다.
- 컴포넌트 라이프사이클이 렌더링 후까지 실행을 지연시킵니다.
- 상태 변화가 재렌더링을 트리거하면 캡처된 변수가 바뀔 수 있습니다.
- 비동기 특성 때문에 람다가 루프가 끝난 뒤에 실행되는 경우가 많습니다.
이것은 Blazor만의 문제는 아니다
- JavaScript –
var를 루프에서 사용할 때 같은 문제 발생. - C# – 루프 안의 모든 람다에서 발생 가능.
- Java – 루프 변수를 캡처하는 익명 클래스.
- Python – 정의 시점에 평가되는 기본 인자.
경험 법칙
루프 안에서 람다식이나 이벤트 핸들러를 만들 때는 변수가 아니라 값을 캡처하라.
- 람다 내부에 즉시 로그를 추가한다.
- 문제를 격리하기 위해 하드코딩된 값을 테스트한다.
- 람다 전에 로컬 복사(
var current = loopVar)를 만든다. - 람다가 포함된 루프는 “항상 의심해라”는 태도로 접근한다.
마무리 생각
이 버그를 통해 가장 단순해 보이는 코드도 가장 놀라운 동작을 할 수 있다는 것을 배웠습니다. 루프 안의 int 하나가 조심하지 않으면 모든 이벤트 핸들러가 공유하는 참조가 될 수 있습니다.
토론
- 여러분도 클로저 캡처 버그에 잡힌 적 있나요?
- 다른 Blazor 함정은 무엇을 겪어보셨나요?
- 가장 기억에 남는 디버깅 이야기를 공유해주세요!