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

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

by catfamily 2026. 9. 4.

주변 개발자들에게 "요즘 C# WinForms로 설비 제어 프로그램 만들고 있어요"라고 하면 열에 아홉은 비슷한 반응을 보입니다.

"아직도 WinForms 쓰는 데가 있어요?", "WPF나 웹으로 안 가고요?" 같은 질문들이죠.

저 역시 처음 이 업무를 맡았을 때 비슷한 생각을 했습니다. DB/ETL 쪽 일을 오래 하다가 C#은 사실상 초보 수준에서 시작했는데, 하필 그 첫 프로젝트가 오래된 프레임워크 취급받는 WinForms 기반 산업용 프로그램이었으니까요.

그런데 실제로 현장에 들어가서 일해보니, WinForms가 여전히 선택되는 데는 그럴 만한 이유가 있었습니다. 이 글에서는 왜 산업 현장에서 아직도 WinForms가 쓰이는지, 그리고 초보자 입장에서 실제로 설비 통신 프로그램을 만들면서 부딪혔던 코드 레벨 문제들을 실전 코드와 함께 정리해보려고 합니다. 검색해보면 WinForms 문법 강의는 많은데, 산업 현장에서 실제로 겪는 통신 처리·스레드 문제를 다룬 글은 국내에 거의 없더라고요.

특히 저처럼 DB나 백엔드 쪽 경험만 있다가 갑자기 UI 프로그램, 그것도 하드웨어와 실시간으로 통신해야 하는 프로그램을 맡게 된 분들이라면 공감할 부분이 많을 거라고 생각합니다. 웹 개발이나 서버 개발과는 사고방식 자체가 다르다는 걸 느꼈던 게, 여기서는 "요청-응답"이 아니라 "언제 끊길지 모르는 연결을 계속 붙잡고 있어야 하는" 상황을 기본 전제로 코드를 짜야 했기 때문입니다.

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

왜 지금도 WinForms인가 — 산업 현장에서 마주친 현실적인 이유들

가장 먼저 배운 건, 현장 PC 환경이 우리가 생각하는 "최신 개발 환경"과는 거리가 멀다는 점이었습니다. 설비 옆에 붙어 있는 제어용 PC들은 교체 주기가 매우 길고, 심한 경우 윈도우7이나 윈도우10 초기 버전이 여전히 돌아가고 있는 경우도 있습니다. 이런 환경에서는 최신 .NET 런타임이나 웹 기반 프레임워크를 올리는 것 자체가 부담이거나 아예 불가능한 경우가 많습니다. WinForms는 별도 런타임 설치 없이도 오래된 윈도우에서 안정적으로 돌아간다는 점에서 여전히 현실적인 선택지였습니다.

두 번째 이유는 시리얼 통신, GPIO, 산업용 프로토콜(Modbus, TCP 소켓 기반 커스텀 프로토콜 등) 연동 라이브러리와 예제 자료가 압도적으로 WinForms/.NET Framework 기준으로 쌓여 있다는 점이었습니다. 실제로 설비 제조사에서 제공하는 SDK나 예제 코드도 대부분 오래된 WinForms 샘플인 경우가 많아서, 최신 프레임워크로 새로 포팅하는 것보다 기존 생태계를 그대로 활용하는 게 훨씬 빠르고 안전했습니다.

세 번째는 배포 방식의 단순함입니다. 현장 PC는 인터넷이 아예 안 되거나 극히 제한된 사내망만 연결된 경우가 대부분입니다. 웹 기반 프로그램은 이런 환경에서 오히려 배포와 유지보수가 더 까다로워집니다. 반면 WinForms 실행 파일 하나를 USB로 복사해서 넣고 실행하는 방식은, 세련되진 않아도 확실하게 동작합니다. "예쁘고 최신인 기술"보다 "현장에서 확실히 돌아가는 기술"이 우선순위가 되는 환경이라는 걸 뼈저리게 느꼈습니다.

네 번째로는 현장 작업자들의 사용 습관도 무시할 수 없는 요소였습니다. 오랫동안 비슷한 스타일의 프로그램을 써온 작업자들에게 갑자기 웹 브라우저 기반의 낯선 UI를 들이밀면 오히려 반발이 컸습니다. 버튼 하나, 팝업 메시지 하나까지 기존 프로그램과 비슷한 느낌으로 유지해주는 것만으로도 현장 적응 속도가 눈에 띄게 빨라졌습니다. 기술적으로 더 나은 선택지가 항상 현장에서 더 나은 선택은 아니라는 걸 이때 처음 체감했습니다.

실전 코드로 보는 WinForms 산업용 프로그램 제작기

이론은 이 정도로 하고, 실제로 초보 입장에서 가장 많이 헤맸던 코드 문제 두 가지를 공유합니다.

1) 크로스 스레드 예외. WinForms를 처음 배울 때 가장 먼저 만나는 벽입니다. 설비에서 시리얼로 데이터가 들어오는 이벤트는 UI 스레드가 아닌 별도 스레드에서 발생하는데, 여기서 바로 UI 컨트롤을 건드리면 "크로스 스레드 작업이 잘못되었습니다"라는 예외가 터집니다. 처음엔 이 에러 메시지 뜻도 몰라서 한참을 검색했던 기억이 납니다.

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와 통신하는 부분을 예제 코드와 함께 좀 더 자세히 다뤄보도록 하겠습니다.