모든 시간 슬롯을 24:00으로 만든 Blazor 클로저 버그 🕛 🕛

발행: (2026년 1월 4일 오전 04:41 GMT+9)
6 min read
원문: Dev.to

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.Rangeforeach 사용

@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만의 문제는 아니다

  • JavaScriptvar를 루프에서 사용할 때 같은 문제 발생.
  • C# – 루프 안의 모든 람다에서 발생 가능.
  • Java – 루프 변수를 캡처하는 익명 클래스.
  • Python – 정의 시점에 평가되는 기본 인자.

경험 법칙

루프 안에서 람다식이나 이벤트 핸들러를 만들 때는 변수가 아니라 값을 캡처하라.

  • 람다 내부에 즉시 로그를 추가한다.
  • 문제를 격리하기 위해 하드코딩된 값을 테스트한다.
  • 람다 전에 로컬 복사(var current = loopVar)를 만든다.
  • 람다가 포함된 루프는 “항상 의심해라”는 태도로 접근한다.

마무리 생각

이 버그를 통해 가장 단순해 보이는 코드도 가장 놀라운 동작을 할 수 있다는 것을 배웠습니다. 루프 안의 int 하나가 조심하지 않으면 모든 이벤트 핸들러가 공유하는 참조가 될 수 있습니다.

토론

  • 여러분도 클로저 캡처 버그에 잡힌 적 있나요?
  • 다른 Blazor 함정은 무엇을 겪어보셨나요?
  • 가장 기억에 남는 디버깅 이야기를 공유해주세요!
Back to Blog

관련 글

더 보기 »

C#에서 RTF를 PDF로 변환하기

개요 RTF(Rich Text Format)는 문서 편집 및 데이터 교환에 널리 사용되는 크로스‑플랫폼 형식입니다. 반면 PDF는 문서 배포에 이상적입니다.