본문 바로가기
카테고리 없음

"아직도 WinForms로 만든다고요?" — 산업용 프로그램 개발자가 겪은 현실

by catfamily 2026. 9. 4.

동료 개발자들에게 “현재 C# WinForms를 사용해 시설 제어 프로그램을 만들고 있다”고 말하면, 열 명 중 아홉 명은 비슷한 반응을 보입니다.

그들은 “아직도 WinForms를 사용하는 곳이 있나요?”라거나 “WPF나 웹으로 전환하지 않나요?” 같은 질문을 합니다.

저도 이 일을 처음 맡았을 때 비슷한 생각을 했습니다. 오랫동안 DB/ETL 분야에서 일한 후, 사실상 초보자 수준에서 C#을 시작했는데, 운명처럼 제 첫 프로젝트는 종종 구식이라고 여겨지는 WinForms 기반의 산업용 프로그램이었습니다.

하지만 실제로 현장에 나가 일을 시작하고 나니, WinForms가 여전히 선택되는 데에는 타당한 이유가 있다는 것을 깨달았습니다. 이 글에서는 산업 현장에서 WinForms가 여전히 사용되는 이유를 설명하고, 초보자의 관점에서 장치 통신 프로그램을 실제로 개발하면서 마주친 코드 수준의 난제들을 실제 코드 예제와 함께 정리해 보겠습니다. 온라인에는 WinForms 구문에 대한 튜토리얼이 많지만, 산업 현장에서 실제로 발생하는 통신 및 스레딩 문제를 다루는 한국 내 글은 거의 없다는 것을 알게 되었습니다.

저처럼 데이터베이스나 백엔드 개발 경험만 있다가 갑자기 하드웨어와 실시간으로 통신해야 하는 UI 프로그램을 맡게 된 분들께 이 글이 특히 공감을 불러일으킬 것이라 생각합니다. 단순한 ‘요청-응답’ 모델이 아니라, “언제든 끊어질 수 있는 연결을 유지해야 한다”는 전제 하에 코드를 작성해야 했기 때문에, 웹이나 서버 개발과는 사고방식이 근본적으로 다르다는 것을 깨달았습니다.

"아직도 WinForms로 만든다고요?" — 산업용 프로그램 개발자가 겪은 현실
아직도 WinForms로 만든다고요?

왜 우리는 여전히 WinForms를 사용하고 있을까? — 산업 현장에서 마주치는 현실적인 이유들

제가 가장 먼저 깨달은 점은, 생산 현장의 PC 환경이 우리가 생각하는 “최첨단 개발 환경”과는 거리가 멀다는 것이었습니다. 장비에 연결된 제어용 PC는 교체 주기가 매우 길며, 극단적인 경우 여전히 Windows 7이나 Windows 10 초기 버전을 실행하고 있기도 합니다. 이러한 환경에서는 최신 .NET 런타임이나 웹 기반 프레임워크를 배포하는 것이 종종 부담스럽거나 심지어 불가능하기까지 합니다. WinForms는 별도의 런타임 설치 없이도 구형 Windows 시스템에서 안정적으로 실행되기 때문에 여전히 실용적인 선택으로 남아 있었습니다.

두 번째 이유는 시리얼 통신, GPIO, 산업용 프로토콜(Modbus 및 사용자 정의 TCP 소켓 기반 프로토콜 등)을 통합하기 위한 대다수의 라이브러리와 예제 코드가 WinForms와 .NET Framework를 기반으로 구축되었기 때문입니다. 사실, 장비 제조업체에서 제공하는 대부분의 SDK와 샘플 코드는 종종 구식 WinForms 샘플인 경우가 많기 때문에, 모든 것을 최신 프레임워크로 이식하는 것보다 기존 생태계를 있는 그대로 활용하는 것이 훨씬 빠르고 안전했습니다.

세 번째 이유는 배포의 간편성입니다. 현장용 PC는 대개 완전히 오프라인 상태이거나, 접근이 엄격히 제한된 내부 네트워크에만 연결되어 있습니다. 이러한 환경에서는 오히려 웹 기반 프로그램을 배포하고 유지 관리하기가 더 어려워집니다. 반면, 단일 WinForms 실행 파일을 USB 드라이브에 복사하여 실행하는 방식은 가장 정교한 방법은 아니지만 안정적으로 작동합니다. 저는 이 환경에서 “현장에서 안정적으로 작동하는 기술”이 “멋지고 최첨단인 기술”보다 우선시된다는 사실을 뼈저리게 깨달았습니다.

넷째, 현장 근로자들의 사용 습관 또한 무시할 수 없는 요인이었습니다. 오랫동안 비슷한 스타일의 프로그램을 사용해 온 근로자들에게 낯선 웹 브라우저 기반 UI를 갑자기 도입했을 때, 반발이 컸습니다. 기존 프로그램과 유사한 느낌을 — 모든 버튼과 팝업 메시지 하나하나까지 — 단순히 유지하는 것만으로도 그들이 새로운 시스템에 적응하는 속도가 눈에 띄게 빨라졌습니다. 그때야말로 기술적으로 우월한 옵션이 현장에서 항상 최선의 선택은 아니라는 사실을 처음으로 깨달았습니다.

실제 코드를 활용한 산업용 WinForms 프로그램 구축 실용 가이드

이론은 이쯤 하고, 초보 시절 저에게 가장 큰 골칫거리였던 두 가지 코드 문제를 살펴보겠습니다.

1) 스레드 간 예외. 이것은 WinForms를 배우면서 마주치게 될 첫 번째 난관입니다. 장비로부터 시리얼 통신을 통해 데이터가 들어오는 이벤트는 UI 스레드가 아닌 별도의 스레드에서 발생합니다. 이 스레드에서 직접 UI 컨트롤을 조작하려고 하면 “스레드 간 작업 실패(Cross-thread operation failed)”라는 예외가 발생합니다. 처음에는 그 오류 메시지가 무슨 뜻인지조차 몰라서 해답을 찾느라 꽤 오랜 시간을 보냈던 기억이 납니다.

private SerialPort _port;

private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    string data = _port.ReadExisting();

    // 이렇게 바로 UI를 건드리면 크로스 스레드 예외 발생
    // lblStatus.Text = data;

    // InvokeRequired로 체크 후 안전하게 UI 스레드에서 실행
    if (lblStatus.InvokeRequired)
    {
        lblStatus.Invoke(new Action(() => lblStatus.Text = data));
    }
    else
    {
        lblStatus.Text = data;
    }
}

2) 시리얼 데이터가 버스트 형태로 도착하는 문제. 장비에서 전송된 데이터는 한 번에 모두 도착하기보다는 종종 조각난 형태로 도착했습니다. DataReceived 이벤트가 발생할 때마다 그 순간 수신된 데이터만 읽고 파싱하면 문장이 중간에 끊겨 지속적인 파싱 오류가 발생했습니다. 우리는 데이터를 버퍼에 지속적으로 축적한 후, 종료 문자(예: 줄바꿈 문자)가 수신되었을 때만 파싱하도록 하여 이 문제를 해결했습니다.

private StringBuilder _buffer = new StringBuilder();

private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    string chunk = _port.ReadExisting();
    _buffer.Append(chunk);

    string full = _buffer.ToString();
    int idx;
    while ((idx = full.IndexOf('\n')) >= 0)
    {
        string line = full.Substring(0, idx).Trim();
        full = full.Substring(idx + 1);
        ProcessLine(line);
    }
    _buffer.Clear();
    _buffer.Append(full);
}

3) UI 업데이트 빈도가 지나치게 높아 화면이 끊기는 현상. 초당 수십 번씩 측정값을 전송하는 장비의 경우, 새로운 데이터가 도착할 때마다 DataGridView를 업데이트하면 화면이 심하게 끊어졌습니다. 우리는 System.Windows.Forms.Timer를 사용하여 값을 내부 변수에만 저장하고, 일정한 간격(예: 200ms마다)으로 화면 업데이트를 한꺼번에 처리함으로써 이 문제를 해결했습니다. 이를 통해 인터페이스는 실시간으로 표시되는 것처럼 보이면서도 UI 스레드의 부하를 크게 줄일 수 있었습니다.

시행착오를 거쳐 정리한 WinForms 산업용 프로그램 설계 모범 사례

몇 달간의 시행착오 끝에 도출한 원칙들을 공유하고자 합니다.

통신 로직과 UI 로직을 철저히 분리하십시오. 처음에는 모든 통신 로직을 폼 코드에 직접 넣었지만, 나중에는 유지보수가 악몽처럼 힘들어졌습니다. 그 이후로는 시리얼/소켓 통신을 처리하는 별도의 클래스를 만들고, 이 클래스가 이벤트나 콜백을 통해 폼에 결과만 전달하도록 시스템을 재구성했습니다. 덕분에 통신 프로토콜이 변경되더라도 UI 코드를 수정할 필요가 거의 없습니다.

설계 단계부터 항상 재연결 로직을 포함시키세요. 현장에서는 생각보다 자주 케이블이 풀리거나 장비가 재부팅됩니다. 통신이 끊어졌을 때 프로그램이 단순히 멈추기만 한다면, 현장 작업자들은 매번 프로그램을 강제 종료하고 다시 시작해야 하며, 이러한 불편함은 신뢰성을 크게 떨어뜨립니다. 저는 연결 상태를 주기적으로 확인하고, 연결이 끊어졌을 때 지수적 백오프 알고리즘을 사용하여 재연결을 시도하는 로직이 포함된 기본 템플릿을 만들어 이후 프로젝트에서 재사용하고 있습니다.

사용자 친화적인 방식으로 예외를 처리하고, 개발자를 위해 세부 정보를 철저히 기록하십시오. 처음에는 예외가 발생하면 단순히 스택 트레이스를 메시지 상자에 있는 그대로 표시했지만, 이 정보는 현장 작업자에게는 아무런 의미가 없었습니다. 그 이후로 저는 사용자 인터페이스에는 “장치 연결을 확인해 주세요”와 같은 간단한 안내문만 표시하고, 상세한 예외 정보와 타임스탬프는 별도의 로그 파일에 기록하는 방식으로 접근 방식을 변경했습니다.

빌드 출력을 가능한 한 단순하게 유지하세요. 외부 패키지 의존성이 증가함에 따라 구형 현장 PC에서 애플리케이션이 실행되지 않는 사례도 늘어났습니다. 현장 배포의 경우 표준 라이브러리를 중심으로 빌드하고, 필요한 외부 패키지는 최소한으로만 사용하는 것이 훨씬 안전하다는 것을 깨달았습니다.

구성 값을 코드에 하드코딩하지 말고 별도의 파일로 분리하십시오. 처음에는 포트 번호, 통신 속도, 타임아웃 값 등을 코드에 직접 하드코딩했지만, 현장마다 장비 사양이 약간씩 달라 매번 프로그램을 다시 빌드하고 재배포해야 하는 번거로움이 발생했습니다. 그 이후로 저는 이러한 설정들을 간단한 INI 또는 JSON 구성 파일로 옮겼고, 덕분에 현장 직원이 프로그램을 재배포하지 않고도 설정을 수정할 수 있게 되었습니다. 사소한 변화처럼 보일 수 있지만, 이는 운영 부담을 크게 줄여준 개선 사항이었습니다.

저는 C# 초보자로 시작했고 지금도 배우고 있지만, 한 가지 확실한 것은 한때 “구식 기술”로 치부되었던 WinForms가 산업 현장에서는 여전히 가장 실용적이고 안정적인 선택이라는 점입니다. 화려하지는 않더라도, 하루 24시간 가동되는 장비와 함께 조용히 제 역할을 수행하는 프로그램을 만드는 것—그것이야말로 이 일의 진정한 즐거움이었습니다. 처음에는 트렌드에 뒤처지고 있다는 생각에 다소 낙담하기도 했지만, 이제는 이 직접 경험하고 힘들게 쌓아온 노하우가 타인이 쉽게 따라 할 수 없는 자산이라고 믿습니다. 다음 글에서는 Modbus 프로토콜을 사용하여 실제 PLC와 통신하는 방법을 샘플 코드와 함께 더 자세히 다룰 예정입니다..