「응답 없음」의 정체 ── Windows가 앱을 「멈췄다」고 판단하는 구조와, 멈추지 않는 설계

· 업데이트: · · Windows, Windows 개발, 장애 조사, 멀티스레드, WinForms, WPF, Win32 API, UI 설계

수정 이력(1건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22176675)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「「응답 없음」의 정체 ── Windows가 앱을 「멈췄다」고 판단하는 구조와, 멈추지 않는 설계」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-app-not-responding-hang-mechanism/

DOI(등록된 아카이브)
10.5281/zenodo.22176675
DOI(마지막 등록 버전)
10.5281/zenodo.22176676

「처리 중에 앱이 하얘지며 (응답 없음)이 뜬다」「가끔 멈춘다는 문의가 오는데, 개발 PC에서는 재현되지 않는다」── Windows 업무 앱에서 가장 흔한 불만 중 하나가 이 「응답 없음」입니다. 그리고 의외로 잘 알려지지 않은 점은, 「응답 없음」 표시를 내는 쪽이 멈춘 앱 자신이 아니라 OS라는 사실입니다.

Windows는 앱이 「멈췄다」는 것을 어떻게 알까요. 하얗게 흐려진 그 창은 무엇일까요. 이 글에서는 Windows에서 업무 앱을 만드는 개발자와, 멈추는 앱 문의를 받는 IT 담당자를 대상으로, 「응답 없음」 판단의 구조를 메시지 루프의 기초부터 풀어 설명하고, 멈추는 전형적인 원인, 멈추지 않는 설계, 멈춘 순간의 조사 절차까지 1차 정보를 바탕으로 정리합니다.

1. 먼저 결론

  • 「응답 없음」은 OS의 판단입니다. 입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 메시지 꺼내기(PeekMessage)를 하지 않은 창(과 그 창을 소유한 GUI 스레드)을 OS는 응답 없음으로 봅니다. 판단은 프로세스 단위가 아닙니다.1
  • 하얘진 창은 「고스트 창」입니다. OS가 원래 창을 숨기고, 같은 위치·크기·외관의 가짜로 교체합니다. 할 수 있는 것은 이동·최소화·닫기뿐이며, 내용은 동작하지 않습니다. 디버거가 연결된 동안에는 고스트 창이 만들어지지 않습니다.2
  • 멈추는 원인은 거의 하나로 귀결됩니다. 메시지 루프를 돌려야 할 UI 스레드가 무거운 처리나 대기에 막혀 있는 것입니다. 동기 I/O·네트워크 호출·락 대기·스레드 간 SendMessage가 전형적입니다.3
  • 대책의 원칙은 「UI 스레드에서 기다리지 말고, 계산하지 않는다」입니다. 무거운 처리는 워커 스레드(C#이라면 async/await + Task.Run)로 옮기고, UI 스레드는 그리기·진행·취소 수락에 전념하게 합니다.4
  • DoEvents나 메시지 펌프를 손으로 돌리는 방식은 재진입 버그의 온상입니다. 응답 없음 표시는 사라지지만, 처리 도중에 임의의 이벤트가 끼어드는 구조가 됩니다. 회피가 아니라 분리가 정석입니다.
  • 조사는 「멈춰 있는 순간」의 상태 확보가 최우선입니다. 덤프를 떠서 UI 스레드의 스택을 보면, 무엇을 기다리다 멈췄는지는 거의 특정할 수 있습니다.

2. 전제: Windows 앱은 메시지로 동작한다

「응답 없음」을 이해하려면, 먼저 Windows GUI 앱이 이벤트 구동이라는 점을 잡아 두어야 합니다. GUI 앱은 입력을 스스로 가져오지 않고, OS가 전달하는 메시지(마우스, 키보드, 다시 그리기 요청, 타이머 등)를 받아 동작합니다.3

창을 만든 각 스레드는 메시지 큐를 가지며, 다음과 같은 메시지 루프를 돌립니다.

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // 윈도우 프로시저가 호출된다
}

GetMessage로 큐에서 메시지를 꺼내고, DispatchMessage가 그 창의 윈도우 프로시저(메시지 처리 함수)를 호출합니다. 버튼 클릭 처리도, 다시 그리기도, WinForms나 WPF의 이벤트 핸들러도, 따져 보면 모두 이 루프의 한 바퀴 안에서 실행됩니다.5

메시지 루프의 기본 구조OS가 마우스나 키보드 등의 입력을 스레드의 메시지 큐에 넣고, UI 스레드의 루프가 GetMessage로 꺼내 DispatchMessage로 윈도우 프로시저를 호출하며, 처리가 끝나면 루프 처음으로 돌아간다OS(입력·다시 그리기 요청·타이머)스레드의 메시지 큐GetMessage로 꺼낸다DispatchMessage윈도우 프로시저에서 처리

그림 1: GUI 앱의 핵심은 메시지 루프이며, 이벤트 처리는 모두 이 루프의 한 바퀴로 실행된다.

이 구조에서 중요한 결론이 하나 있습니다. 윈도우 프로시저(이벤트 핸들러) 안에서 시간이 걸리는 처리를 하면, 그동안 루프는 다음 메시지를 꺼낼 수 없다는 점입니다. 클릭에도 다시 그리기 요청에도 반응하지 못하게 됩니다. 이것이 「멈춤」의 정체입니다.

메시지가 전달되는 경로가 두 가지라는 점도 잡아 둡니다. PostMessage는 큐에 넣고 바로 돌아오며, 루프가 순서대로 꺼내 처리합니다. 반면 SendMessage는 윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출 쪽으로 돌아오지 않습니다.36 이 차이는 4장의 deadlock 이야기에 그대로 이어집니다.

메시지의 두 가지 전달 경로PostMessage는 큐에 넣고 바로 돌아오며, 메시지 루프가 순서대로 꺼내 처리한다. SendMessage는 윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출 쪽으로 돌아오지 않는다PostMessage큐에 넣는다(바로 돌아온다)루프가 순서대로 꺼내 처리SendMessage프로시저를 직접 호출처리가 끝날 때까지 돌아오지 않는다

그림 2: 같은 「메시지를 보낸다」라도, 큐를 거치는 Post와 완료를 기다리는 Send는 성질이 전혀 다르다.

3. 「응답 없음」 판단의 구조 ── 5초 규칙과 고스트 창

그렇다면 OS는 「이 앱이 멈췄다」는 것을 어떻게 알까요. 판단 기준은 공식 문서에 나와 있습니다. 입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 PeekMessage(메시지 꺼내기)를 호출하지 않은 창을 OS는 응답 없음으로 봅니다.1 즉 OS는 「메시지 루프가 제대로 돌고 있는지」를 맥박처럼 보고, 5초 동안 맥박이 없으면 응답 없음으로 판단합니다(이 5초라는 값은 앞으로 바뀔 수 있다고 명시되어 있습니다). 판단 단위는 창과 그 창을 소유한 GUI 스레드이며, UI 스레드가 여러 개인 앱에서는 하나가 멈춰도 다른 스레드의 창은 살아 있을 수 있습니다. 덤프에서 봐야 할 것은 멈춘 창의 소유 스레드입니다.

판단된 최상위 창에 일어나는 일도 문서화되어 있습니다. OS는 원래 창을 숨기고, 같은 Z-order·위치·크기·외관을 가진 「고스트 창(ghost window)」으로 교체합니다. 사용자가 할 수 있는 것은 이동·크기 변경·(강제) 닫기뿐입니다. 안의 앱은 실제로 응답하지 않으므로 그 밖의 조작은 할 수 없습니다.2

응답 없음 판단과 고스트 창 교체 흐름UI 스레드가 무거운 처리에 막혀 메시지 꺼내기가 5초 멈추면, OS가 응답 없음으로 판단해 원래 창을 숨기고 같은 모습의 고스트 창으로 교체하며, 사용자에게는 이동·최소화·닫기만 제공한다아니요예UI 스레드가 무거운 처리에 막힌다메시지 꺼내기가 멈춘다5초가 지났는가?고스트 창으로 교체제목에 (응답 없음) 표시하얗게 흐려짐·이동과 닫기만 가능

그림 3: 「응답 없음」 표시도 흰 화면도, 멈춘 앱이 아니라 OS가 교체한 고스트 창의 것이다.

제목 표시줄에 붙는 「(응답 없음)」 문자열도, Aero 테마에서 하얗게 흐려지는 모습도, 모두 이 고스트 창의 것입니다. 여기서 실무적으로 두 가지 결론이 나옵니다.

  • 「응답 없음」이 표시된 시점에, 그 창의 소유 스레드는 적어도 5초 동안 메시지를 처리하지 않은 것입니다. 「표시가 너무 빨리 뜬다」가 아니라, UI 스레드가 분명히 막혀 있습니다.
  • 디버거가 연결되어 있으면 고스트 창은 만들어지지 않습니다.2 「디버그 실행에서는 응답 없음이 되지 않는데, 릴리스에서는 나온다」처럼 보이는 경우, 멈춤 자체는 같고 표시만 다른 일이 있습니다.

참고로, 프로세스 단위로 이 교체를 끄는 DisableProcessWindowsGhosting API도 있습니다.7 키오스크 단말기처럼 「OS가 임의로 창을 조작 가능한 것처럼 보이게 하지 않았으면 하는」 특수한 장면을 위한 것이며, 호출하면 「응답 없음」 표시는 나오지 않지만 멈춰 있다는 사실은 바뀌지 않습니다. 일반 앱이 응답 없음 대책으로 쓸 것은 아니라고 이해하세요.

4. 왜 멈추는가 ── UI 스레드를 막는 전형적인 패턴

원인을 따지면 「UI 스레드가 메시지 루프로 돌아오지 않는다」는 한 점이지만, 실무에서 만나는 모습은 몇 가지 전형으로 나눌 수 있습니다.

UI 스레드를 막는 전형적 원인의 분류동기 I/O와 네트워크 호출, 락 대기, 스레드 간 SendMessage, COM의 STA 관련이라는 4계통의 전형적 원인은, 모두 UI 스레드가 메시지 루프로 돌아오지 못한다는 같은 한 점으로 귀결된다동기 I/O·네트워크 호출UI 스레드가 루프로 돌아오지 못한다락 대기스레드 간 SendMessageCOM의 STA 관련응답 없음 판단으로

그림 4: 겉보기 증상은 같아도, 막고 있는 원인은 4계통으로 나눌 수 있고 대책이 각각 다르다.

동기 I/O와 네트워크 호출. 가장 많습니다. 버튼 클릭 핸들러 안에서 큰 파일 읽기/쓰기, 데이터베이스 쿼리, Web API 호출, 네트워크 드라이브 위 파일 접근을 동기로 하는 패턴입니다. 개발 PC에서는 소수점 몇 초로 끝나 눈치채지 못하고, 운영 환경의 네트워크 지연이나 파일 서버 이상으로 수십 초를 기다리게 되어 「가끔 멈춘다」는 문의가 됩니다. 네트워크 드라이브는 연결이 끊겼을 때 타임아웃이 길어, 증상을 극적으로 악화시킵니다.

락 대기. UI 스레드가 워커 스레드와 공유하는 데이터의 락을 잡으려다, 그 락을 오래 쥐고 있는 워커를 기다려 버리는 패턴입니다. 락의 규율은 멀티스레드 실무 시리즈에서 자세히 다루었습니다.

스레드 간 SendMessage. SendMessage는 대상 창의 프로시저가 처리를 끝낼 때까지 돌아오지 않습니다.6 다른 스레드의 창으로 보낸 경우, 그 스레드가 메시지를 처리할 수 있는 상태가 될 때까지 송신 쪽이 기다리게 됩니다. 여기서 대상 스레드도 무언가를 기다리고 있으면, 서로 기다리는 메시지 deadlock이 성립합니다.3 특히 HWND_BROADCAST로의 송신은, 응답하지 않는 창이 하나라도 있으면 말려듭니다. 기다릴 수 없는 장면에서는 SendMessageTimeout이나, 응답을 기다리지 않는 PostMessage를 검토합니다.8

스레드 간 SendMessage에 의한 deadlockUI 스레드가 워커 스레드의 결과를 기다리다 블록된 상태에서, 워커 스레드가 UI 스레드의 창으로 SendMessage를 보내면, 서로 상대의 완료를 기다려 deadlock이 된다워커 스레드UI 스레드워커 스레드UI 스레드메시지를 처리할 수 없다(대기 중)SendMessage에서 돌아오지 못한다서로 기다려 deadlock워커의 완료를 대기(블록)SendMessage(처리가 끝날 때까지 돌아오지 않는다)

그림 5: 「UI 스레드가 워커를 기다리고, 워커가 SendMessage로 UI 스레드를 기다린다」는 전형적인 deadlock이다.

COM apartment 관련. STA 객체로의 호출은 창 메시지로 전달되므로, UI 스레드(STA)가 막히면 다른 스레드에서의 COM 호출도 함께 블록됩니다. 이 구조는 COM STA/MTA 기사에서 설명합니다.

「잠깐이니까」의 누적. 한 번에 50ms인 동기 처리라도, 루프에서 100번 호출하면 5초입니다. 응답 없음의 임계값은 5초이지만, 체감의 「묵직함」은 100ms 정도부터 시작됩니다. 설계의 기준은 「UI 스레드를 막아도 되는 것은 밀리초 단위까지」입니다.

5. 멈추지 않는 설계 ── 무거운 처리를 UI 스레드에서 빼낸다

대책의 원칙은 하나, 시간이 걸리는 일을 UI 스레드에서 빼내는 것입니다. C#(WinForms/WPF)라면 async/await가 가장 짧은 정석입니다.

private async void RunButton_Click(object sender, EventArgs e)
{
    runButton.Enabled = false;
    try
    {
        // CPU 부하가 높은 처리·동기 API만 있는 경우는 Task.Run으로 워커에
        var result = await Task.Run(() => HeavyCalculation(input));

        // I/O는 네이티브로 async인 API를 사용한다(스레드도 소비하지 않는다)
        var data = await httpClient.GetStringAsync(url);

        // await 뒤에는 UI 스레드로 돌아와 있으므로, 컨트롤을 직접 건드릴 수 있다
        resultLabel.Text = result + data.Length.ToString();
    }
    catch (Exception ex)
    {
        // async void 핸들러에서 예외를 흘리면 앱이 죽는다. 여기서 받는다
        MessageBox.Show($"처리에 실패했습니다: {ex.Message}");
    }
    finally
    {
        runButton.Enabled = true;
    }
}

포인트는 세 가지입니다. 첫째, await로 기다리는 동안 UI 스레드는 메시지 루프로 돌아가 있으므로 응답 없음이 되지 않습니다. 둘째, await continuation은 UI 스레드로 돌아오므로, 이후에서 컨트롤을 그대로 건드릴 수 있습니다(워커 스레드에서 컨트롤을 직접 건드리는 것은 금지이며, 필요하면 Control.Invoke / Dispatcher.InvokeAsync를 사용합니다).4 셋째, 실행 중에는 버튼을 비활성화하는 등으로 재진입을 설계로 막는 것입니다.

Win32 네이티브의 경우도 구도는 같고, 워커 스레드에 일을 넘기고, 완료를 PostMessage로 자체 메시지로서 UI 스레드에 알리며, 윈도우 프로시저에서 화면을 갱신합니다. PostMessage는 큐에 넣기만 하고 바로 돌아오므로, 워커 쪽이 블록되지도 않습니다.6 워커 스레드의 대기 자체는 조건 변수 기사에서 다룬 규율로 작성합니다.

멈추지 않는 앱의 역할 분담UI 스레드는 입력 수락·진행 표시·취소 수락만 담당하고, 무거운 처리는 워커 스레드가 실행한 뒤, 완료를 PostMessage나 await continuation으로 UI 스레드에 돌려준다일을 넘긴다PostMessage / await continuationUI 스레드: 입력·진행·취소워커 스레드: 무거운 처리UI 스레드에서의 동기 I/O·긴 계산은 금지

그림 6: UI 스레드는 「접수 담당」에 전념하고, 무거운 일은 반드시 워커에 넘겨 완료 알림만 받는다.

피해야 할 것은, 무거운 처리 사이에 Application.DoEvents()나 PeekMessage 루프를 끼워 넣어 표시만 살리는 기법입니다. 응답 없음은 피할 수 있지만, 처리 도중에 임의의 이벤트 핸들러가 재진입합니다. 버튼 두 번 누르기, 처리 중 폼 닫기, 타이머 실행 ── 모두 처리 중인 데이터를 깨뜨릴 수 있고, 타이밍에 따라 재현하기 어려운 버그가 됩니다. 메시지 펌프를 손으로 돌리는 것은 모달 진행 대화 상자 같은 한정된 구조 안에만 머물게 하고, 원칙은 분리로 해결하세요.

DoEvents에 의한 재진입 버그의 시계열무거운 처리 도중에 DoEvents를 호출하면 쌓여 있던 클릭의 이벤트 핸들러가 끼어들어 실행되고, 처리 중인 데이터를 덮어쓴 뒤에 원래 처리가 재개되므로, 타이밍에 의존하는 데이터 파괴가 일어난다UI 스레드UI 스레드버튼 재클릭 핸들러가 끼어든다무거운 처리를 시작(데이터 가공 중)DoEvents(쌓인 메시지를 처리)끼어든 처리가 데이터를 덮어쓴다원래 처리가 재개(데이터는 이미 불일치)

그림 7: DoEvents는 「응답 없음」을 없애는 대신, 처리 한가운데로 임의의 이벤트를 불러들인다.

장시간 처리에는 진행 표시와 취소도 설계에 넣습니다. IProgress<T>로 진행을 UI에 보내고, CancellationToken으로 중단을 전하는 형태로 두면, 사용자는 「움직이고 있다」는 것을 알 수 있어, 강제 종료(그것은 종종 데이터 손상의 원인이 됩니다)에 손을 뻗지 않게 됩니다.

장시간 처리의 진행과 취소 흐름워커 스레드는 IProgress를 통해 진행을 UI 스레드에 보내고, UI의 취소 조작은 CancellationToken을 통해 워커에 전달되며, 워커는 끊기 좋은 지점에서 중단하고 뒷정리를 한다IProgress로 진행CancellationToken워커: 장시간 처리UI: 진행 표시와 중지 버튼끊는 지점에서 중단하고 뒷정리

그림 8: 진행은 「워커→UI」, 취소는 「UI→워커」. 양방향의 가는 연락 경로를 처음부터 설계에 넣는다.

6. 멈춘 순간의 조사 방법

「가끔 멈춘다」는 조사에서 가장 가치 있는 것은, 멈춰 있는 바로 그 순간의 스레드 상태입니다. 다시 시작해 버리면 증거는 사라집니다.

덤프를 뜬다. 작업 관리자의 세부 정보 탭에서 대상 프로세스를 마우스 오른쪽 단추로 클릭 → 「덤프 파일 만들기」. 이것만으로 모든 스레드의 스택이 들어간 전체 덤프를 뜰 수 있습니다. 문의를 받는 IT 담당 쪽에 「멈추면 닫지 말고 이것을 떠 달라」고 전해 두는 것만으로, 조사 성공률은 크게 달라집니다. 수집 체계를 만드는 방법은 크래시 덤프 수집 기사를 참고하세요.

UI 스레드의 스택을 본다. WinDbg로 덤프를 열고, 메시지 루프를 돌리고 있는 스레드(보통 스레드 0)의 스택을 봅니다. 동기 I/O라면 ReadFile이나 네트워크 API, 락 대기라면 WaitFor… 계열, 스레드 간 SendMessage라면 SendMessage 내부에서 기다리는 모습이 그대로 찍혀 있습니다. 읽는 법은 WinDbg 입문 기사에서 설명합니다.

살아 있는 채로 본다. Process Explorer라면 스레드 목록과 스택을 그 자리에서 확인할 수 있습니다. 상시 묵직한 경우에는 WPR로 트레이스를 떠서 UI 스레드의 대기를 시계열로 분석합니다(WPR/WPA 실전).

응답 없음 조사의 기본 절차멈춰 있는 순간에 덤프를 뜨고, UI 스레드의 스택을 보고, 동기 I/O·락 대기·스레드 간 SendMessage 중 어디에서 멈췄는지를 특정한 뒤, 해당하는 설계 수정으로 연결한다멈춰 있는 순간덤프를 뜬다(닫기 전에)UI 스레드의 스택을 본다동기 I/O·네트워크 대기락 대기스레드 간 SendMessage해당 지점을 워커로 분리

그림 9: 조사의 주역은 「멈춰 있는 순간의 덤프」이며, UI 스레드의 스택이 그대로 원인의 분류가 된다.

증상에서 감을 잡는 방법도 전형화할 수 있습니다. 특정 조작에서 반드시 멈추면, 그 핸들러 안의 동기 I/O를 먼저 의심합니다. 드물게 멈추고 조작과 상관이 없으면, 락 순서나 스레드 간 SendMessage의 deadlock을 의심하고, 덤프에서 양쪽 스레드의 대기 대상을 맞춰 봅니다. 특정 환경에서만 멈추면, 네트워크 드라이브·프록시·백신 등 환경 요인의 타임아웃을 의심하는 흐름입니다.

7. 정리

  • 「응답 없음」은, 5초 동안 메시지를 꺼내지 않는 앱을 OS가 판단해 고스트 창으로 교체하는 구조입니다. 표시를 내는 것은 앱이 아니라 OS입니다.
  • 멈추는 원인은 「UI 스레드가 메시지 루프로 돌아오지 못한다」는 한 점입니다. 동기 I/O·네트워크·락 대기·스레드 간 SendMessage가 전형적입니다.
  • 대책은 UI 스레드에서 무거운 처리를 빼내는 것입니다. C#이라면 async/await + Task.Run, Win32라면 워커 스레드 + PostMessage. 실행 중 재진입은 버튼 비활성화 등의 설계로 막습니다.
  • DoEvents로 피하는 것은 재진입 버그와 맞바꾸는 일입니다. DisableProcessWindowsGhosting은 표시만 없앱니다. 둘 다 근본 대책이 아닙니다.
  • 조사는 「멈춰 있는 순간」의 덤프가 가장 중요합니다. 원인은 UI 스레드의 스택에 거의 그대로 찍혀 있습니다.

「응답 없음」은 사용자에게는 「고장」이지만, 구조를 알면 「UI 스레드가 5초 동안 돌아오지 않았다」는 정확한 한 문장으로 옮길 수 있습니다. 그 한 문장에서 거꾸로 가면, 원인 후보도, 고치는 방법도, 조사 절차도 자연스럽게 정해집니다.

관련 기사

관련 상담 영역

합동회사 코무라소프트에서는 「가끔 멈춘다」, 「응답 없음이 된다」는 업무 앱의 원인 조사(덤프 분석·트레이스 분석), 동기 처리로 가득한 레거시 UI 코드의 async/await화·워커 스레드 분리 개수, 멈추지 않는 UI 설계 리뷰를 다루고 있습니다. 재현 절차가 아직 없는 단계에서도, 증거를 잡는 방법의 설계부터 도울 수 있습니다.

참고 링크

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). 앱이 「입력을 기다리지 않고, 시작 처리 중도 아니며, 내부 타임아웃인 5초 동안 PeekMessage를 호출하지 않은」 경우 응답 없음으로 본다는 판단 기준, 이 5초 기준은 바뀔 수 있다는 점, 고스트 창에 대해서는 항상 TRUE가 반환된다는 점에 대해. ↩ ↩2

  2. Microsoft Learn, GetMessage function (winuser.h). 최상위 창이 수 초 동안 메시지에 응답하지 않게 되면 시스템이 그 창을 응답 없음으로 보고, 같은 Z-order·위치·크기·외관을 가진 고스트 창으로 교체한다는 점, 사용자는 이동·크기 변경·닫기만 할 수 있다는 점, 디버거가 연결된 동안에는 고스트 창이 만들어지지 않는다는 점에 대해. ↩ ↩2 ↩3

  3. Microsoft Learn, About Messages and Message Queues. Windows 앱이 이벤트 구동이며 윈도우 프로시저가 메시지를 처리하는 구조, 큐를 거치는 메시지와 직접 보내지는 메시지의 구별, 응답 없는 창의 고스트 창 교체, 스레드 간에 메시지를 주고받아 생기는 deadlock 절에 대해. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). WinForms 컨트롤이 만들어진 스레드 이외에서는 안전하게 건드릴 수 없다는 점, 다른 스레드에서의 갱신에는 Invoke/BeginInvoke를 사용한다는 점, async/await나 BackgroundWorker를 사용한 안전한 비동기 패턴에 대해. ↩ ↩2

  5. Microsoft Learn, Using Messages and Message Queues. GetMessage·TranslateMessage·DispatchMessage에 의한 표준 메시지 루프 구현 예와, 메시지 큐를 조사하는 방법에 대해. ↩

  6. Microsoft Learn, SendMessage function (winuser.h). SendMessage가 지정 창의 윈도우 프로시저를 호출하고 처리가 끝날 때까지 돌아오지 않는다는 점, 다른 스레드의 창으로 보내면 상대 스레드가 메시지를 처리할 때까지 기다리게 된다는 점, 응답을 기다리지 않고 큐에 넣는 PostMessage와의 차이에 대해. ↩ ↩2 ↩3

  7. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 호출한 GUI 프로세스에 대해, 응답 없는 창을 최소화·이동·닫을 수 있게 하는 고스트 창 기능을 끌 수 있다는 점, 해제는 프로세스 수명 동안 유지된다는 점에 대해. ↩

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). 타임아웃을 두고 메시지를 보낼 수 있다는 점, 응답하지 않는 창(hang으로 판단된 창)에 대해 기다리지 않고 돌아오는 플래그(SMTO_ABORTIFHUNG)가 마련되어 있다는 점에 대해. ↩

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

「응답 없음」은 어떤 조건에서 표시되나요?
OS는 창이 있는 앱이 「입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 메시지 꺼내기(PeekMessage)를 하지 않은」 경우 그 창을 응답 없음으로 판단합니다. 판단된 최상위 창은 숨겨지고, 같은 위치·크기·외관의 「고스트 창」으로 교체됩니다. 제목 표시줄의 「(응답 없음)」과 하얗게 흐려진 모습은 이 고스트 창의 것이며, 이동·최소화·닫기만 할 수 있습니다. 즉 「응답 없음」은 앱 자신의 표시가 아니라 OS가 대신 보여주는 화면입니다.
처리 중에 「응답 없음」이 나오지 않게 하는 설정이 있나요?
DisableProcessWindowsGhosting을 호출하면 그 프로세스에 대한 고스트 창 교체를 끌 수 있습니다. 다만 이는 「멈춰 있다는 사실을 사용자에게 덜 보이게 할」 뿐이며, 창이 조작에 반응하지 않는다는 사실은 그대로입니다. 사용자에게는 이동도 닫기도 할 수 없는 완전한 멈춤으로 보입니다. 근본 대책은 표시를 막는 것이 아니라, UI 스레드를 5초는커녕 0.1초 단위로도 막지 않도록 무거운 처리를 워커 스레드로 옮기는 것입니다. 참고로 디버거가 연결된 동안에는 OS가 고스트 창을 만들지 않으므로, 「디버그 중에는 응답 없음이 되지 않는」 것처럼 보일 수 있습니다.
DoEvents(메시지 펌프를 수동으로 돌리기)로 응답 없음을 피해도 되나요?
권장하지 않습니다. 무거운 처리 도중에 DoEvents나 PeekMessage 루프를 돌리면 응답 없음 판단은 피할 수 있지만, 처리 도중에 버튼 재클릭·창 닫기·타이머 등 임의의 이벤트 핸들러가 재진입합니다. 처리 중인 데이터를 다른 핸들러가 덮어쓰거나, 닫힌 폼을 건드려 예외가 나는 식의 재진입 버그는 타이밍에 따라 재현하기 어렵고, 응답 없음보다 다루기 어렵습니다. 정석은 처리 자체를 Task.Run 등으로 워커 스레드에 옮기고, UI 스레드는 진행 표시와 취소 수락만 맡기는 것입니다.
워커 스레드에서 화면(컨트롤)을 갱신하려면 어떻게 하나요?
WinForms 컨트롤이나 WPF 요소는 만든 스레드(보통 UI 스레드)에서만 건드릴 수 있습니다. 워커 스레드에서 직접 건드리면 예외나 비결정적 동작이 됩니다. C#에서는 async/await가 가장 간단합니다. await continuation은 호출한 UI 스레드로 돌아오므로, await 뒤에서 컨트롤을 그대로 갱신할 수 있습니다. 명시적으로 전환할 때는 WinForms에서는 Control.Invoke/BeginInvoke, WPF에서는 Dispatcher.InvokeAsync를 사용합니다. Win32 네이티브에서는 워커 스레드에서 PostMessage로 자체 완료 메시지를 UI 스레드에 보내고, 윈도우 프로시저 쪽에서 화면을 갱신하는 것이 정석입니다.
「응답 없음」 상태인 앱의 원인은 어떻게 조사하나요?
멈춰 있는 「바로 그 순간」의 상태를 확보하는 것이 중요합니다. 먼저 작업 관리자의 세부 정보 탭에서 「덤프 파일 만들기」로 전체 덤프를 뜬 뒤, WinDbg에서 UI 스레드(메시지 루프를 도는 스레드)의 스택을 봅니다. 동기 I/O·네트워크 대기·락 대기·SendMessage로 다른 스레드를 기다리는지 여부는 스택에 그대로 나타납니다. 살아 있는 상태에서 보려면 Process Explorer의 스레드 목록과 스택 표시가, 시계열로 따라가려면 WPR 트레이스 수집이 유효합니다. 이 사이트의 WinDbg 입문·Process Explorer 실전·WPR/WPA 실전 기사도 참고하세요.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기