Windows 데스크톱 앱의 UI 자동 테스트 ── UI Automation의 원리와 FlaUI로 만드는 잘 깨지지 않는 테스트

· · 테스트, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD

「릴리스할 때마다 전체 화면을 손으로 클릭해서 확인하는 작업에 꼬박 2일이 걸린다」「지난달 고친 부분 옆에 있는 다른 화면이 깨져 있었는데, 그걸 고객사에서 발견했다」「웹 팀은 Selenium이나 Playwright로 회귀 테스트를 자동화하고 있는데, 데스크톱 앱은 손도 못 대고 있다」. WinForms / WPF 업무 앱을 오래 유지보수하고 있는 현장에서 이런 상담을 자주 받습니다.

한편, 의욕적으로 전체 화면 테스트를 작성하기 시작했다가 유지보수를 버티지 못하고 1년 만에 포기했다는 실패담도 그에 못지않게 많이 듣습니다. UI 자동 테스트는 도구 선택과 「어디까지 할 것인가」의 선을 잘못 그으면 수동 테스트보다 더 비싸집니다. 반대로 원리를 이해하고 잘 깨지지 않게 설계하며 범위를 스모크 테스트로 좁히면, 「릴리스 전 2일」을 「매일 밤 무인으로 20분」으로 바꿀 수 있는 투자가 됩니다. 이 글에서는 토대가 되는 UI Automation(UIA)의 원리, 도구의 현재 상황(FlaUI / WinAppDriver / Appium), FlaUI를 이용한 구현, 잘 깨지지 않게 만드는 설계, CI에서의 무인 실행까지, 당사가 실제 프로젝트에서 사용하고 있는 패턴을 한 번에 정리합니다.

1. 먼저 결론

  • UI 자동 테스트는 테스트 피라미드의 최상단입니다. 느리고, 잘 깨지고, 실패 원인을 파악하는 데 시간이 걸리기 때문에 단위·통합 테스트의 대체가 될 수 없습니다. 로직은 아래 계층에서 지키고, UI 테스트는 「실행되고 주요 흐름이 통과한다」는 것을 확인하는 스모크 테스트로 범위를 좁힙니다(6장).
  • 원리의 토대는 Windows UI Automation(UIA)입니다. 데스크톱을 루트로 하는 오토메이션 트리에서 요소를 찾고, AutomationId / Name 같은 속성으로 특정하며, 컨트롤 패턴(Invoke / Value / SelectionItem 등)으로 조작합니다.12
  • 대상 앱의 요소가 어떻게 노출되는지는 코드를 작성하기 전에 inspect.exe나 Accessibility Insights for Windows로 확인합니다. inspect는 Windows SDK에 포함된 레거시 도구이며, 현재는 Accessibility Insights가 공식적으로 권장됩니다.3
  • 당사가 추천하는 도구는 FlaUI + xUnit / NUnit입니다. FlaUI는 UIA를 얇게 감싼 MIT 라이선스 OSS로, UIA2 / UIA3 양쪽을 모두 지원하며 지금도 활발히 유지보수되고 있습니다.4
  • 한때 Microsoft의 공식 선택지였던 WinAppDriver는 최종 안정판이 2020년 11월의 v1.2.1에서 멈춰 있으며, 서버 본체가 비공개 소스이기 때문에 커뮤니티 쪽에서 고칠 수도 없습니다. 신규 도입은 권장하지 않습니다(3장).5
  • 잘 깨지지 않는 정도의 80%는 개발 쪽에서 AutomationId를 부여하는 규칙으로 결정됩니다. WPF는 x:Name이나 AutomationProperties.AutomationId, WinForms는 Name / AccessibleName입니다. 표시 문자열(Name)에 의존하는 검색, 좌표 클릭, Thread.Sleep은 금지하고, 조건 대기(Retry)와 Page Object 패턴으로 작성합니다(4~5장).67
  • CI에서의 무인 실행에는 대화형 데스크톱 세션이 필수입니다. 서비스로 구성한 에이전트에서는 동작하지 않으며, 화면 잠금이나 RDP 연결 해제로도 실패합니다. 셀프 호스트 러너를 자동 로그인(autologon)으로 구성하는 것이 기본 형태입니다(7장).8

2. 원리 ── UI Automation의 트리·속성·패턴

2.1 오토메이션 트리

UI 자동 테스트 도구는 화면을 이미지로 인식하는 것이 아닙니다. Windows에는 스크린 리더 같은 보조 기술이 앱의 UI를 프로그램으로 읽고 조작하기 위한 공식 기반인 UI Automation(UIA)이 있으며, 테스트 자동화도 이 같은 레일 위에서 동작합니다.1

UIA에서는 데스크톱을 루트로 하여, 열려 있는 모든 창과 그 안의 컨트롤이 트리 구조로 노출됩니다. 버튼도 텍스트 박스도 그리드의 행도 모두 트리상의 오토메이션 요소입니다. 트리에는 모든 요소를 포함하는 raw 뷰 외에, 조작 대상이 되는 컨트롤만으로 좁힌 컨트롤 뷰, 내용만 보여주는 콘텐츠 뷰라는 필터링된 뷰가 있습니다.1 테스트 코드는 기본적으로 컨트롤 뷰를 따라가며 요소를 찾게 됩니다.

여기서 짚어 두어야 할 것은, 「눈에 보이는 것」이 아니라 「트리에 노출된 것」만 조작할 수 있다는 점입니다. 표준 컨트롤로 구성된 화면은 트리에 깔끔하게 드러나지만, 오너 드로우로 직접 그린 리스트, 그래픽 라이브러리로 그린 도면 영역, 서드파티 그리드의 일부 등은 트리상에서 「그저 하나의 요소」로만 보이는 경우가 있습니다. 이렇게 보이는 범위가 그대로 UI 자동 테스트가 지킬 수 있는 범위의 상한이 됩니다. 그렇기 때문에 코드를 작성하기 전에 트리를 확인하는 작업(2.3절)이 가장 먼저 해야 할 일입니다.

2.2 속성과 컨트롤 패턴

트리에서 원하는 요소를 특정하는 실마리가 속성입니다. 실무에서 사용하는 것은 다음 4가지입니다.

속성 내용 테스트에서의 위치
AutomationId 개발자가 부여하는 식별자. 언어(로케일)에 의존하지 않음 검색 키의 본진. 형제 요소 안에서 유일하게 설정 6
Name 표시 문자열에서 온 이름(버튼 라벨 등) 사람이 보기엔 알기 쉽지만, 문구 변경·다국어화로 깨짐
ControlType Button / Edit / ComboBox 등의 종류 범위를 좁히는 보조 수단
ClassName 구현 클래스 이름(WinForms의 클래스 이름 등) 최후의 수단. 구현 변경으로 잘 깨짐

AutomationId는 「로케일이 바뀌어도 같은 값이어야 한다」「형제 요소 안에서 유일해야 한다」고 공식적으로 정의되어 있으며, UI 자동 테스트가 언어나 버전을 넘나들며 안정적으로 요소를 찾기 위한 전용 속성입니다.6 반대로 말하면, AutomationId가 부여되지 않은 앱의 UI 테스트는 표시 문자열이나 트리상의 위치라는 잘 깨지는 실마리에 의존하게 됩니다. 이것이 5장의 주제입니다.

찾아낸 요소를 조작하는 수단이 컨트롤 패턴입니다. UIA는 「클릭할 수 있다」「값을 가진다」「선택할 수 있다」와 같은 컨트롤의 기능적인 측면을, 컨트롤 종류와는 독립된 패턴의 집합으로 노출합니다. 문서 자체도 「컨트롤 패턴과 UI의 관계는 COM 객체와 인터페이스의 관계와 같다」고 설명하듯이, 요소에 「어떤 패턴을 구현하고 있는지」를 물어본 뒤 그 패턴을 통해 조작하는 설계입니다.2 COM에 익숙한 분이라면 QueryInterface의 UI판이라고 하면 감이 오실 겁니다. 주요 패턴은 다음과 같습니다.

패턴 할 수 있는 것 대표적인 컨트롤
Invoke 기본 동작 실행(클릭에 해당) 버튼, 메뉴 항목
Value 값 가져오기·설정 텍스트 박스
SelectionItem / Selection 항목 선택·선택 상태 가져오기 리스트, 콤보 박스, 탭
Toggle 켜기·끄기 전환 체크박스
ExpandCollapse 펼치기·접기 콤보 박스, 트리 항목
Text 텍스트 내용 읽기 문서, 리치 텍스트
Window 최대화·최소화·닫기 최상위 창
Scroll / ScrollItem 스크롤, 항목을 보이는 위치로 이동 리스트, 그리드

「버튼을 클릭한다」는 테스트 코드는 내부적으로 「그 요소의 Invoke 패턴을 가져와 Invoke()를 호출한다」는 처리입니다. 좌표를 계산해서 마우스 이벤트를 보내는 것이 아니라, 컨트롤 자신이 노출하는 동작을 호출하는 것──이 차이가 창 위치나 DPI에 의존하지 않는 안정적인 테스트의 토대가 됩니다.

2.3 inspect.exe와 Accessibility Insights로 「보이는 것」을 확인한다

대상 앱의 요소가 어떤 AutomationId / Name / ControlType / 패턴으로 노출되는지는 도구로 실제 모습을 보는 것이 가장 확실합니다.

  • inspect.exe: Windows SDK에 포함된 정평이 난 도구입니다(SDK 설치 위치의 bin\<version>\<platform>에 있습니다). 마우스나 키보드 포커스로 요소를 선택하면 UIA 속성과 패턴이 목록으로 표시되고, 트리 탐색도 검증할 수 있습니다. 다만 공식적으로는 「레거시 도구」로 분류되어 있으며, Accessibility Insights로의 전환이 권장됩니다.3
  • Accessibility Insights for Windows: Microsoft가 현재 권장하는 도구입니다. 마우스 호버나 포커스 이동만으로 요소의 UIA 속성을 확인할 수 있는 Live Inspect가 편리하고, 접근성 관점의 자동 검사(FastPass)도 포함되어 있습니다.3
  • FlaUInspect: FlaUI 프로젝트에 딸린 인스펙터입니다. FlaUI가 실제로 사용하는 UIA2 / UIA3 각각의 시점에서 트리를 확인할 수 있으므로, FlaUI로 테스트를 작성한다면 이것도 설치해 두면 편리합니다.4

경험상, UI 자동 테스트를 도입할 때 가장 먼저 해야 할 일은 「테스트 코드를 작성하는 것」이 아니라, 주요 화면을 inspect 계열 도구로 열어서 AutomationId가 얼마나 부여되어 있는지 점검하는 것입니다. 여기서 텅텅 비어 있다면, 먼저 앱 쪽 개선(5장)부터 시작하는 것이 결국 더 빠릅니다.

3. 도구의 선택지 ── FlaUI를 추천하는 이유와 WinAppDriver의 현재 상황

UIA를 직접 COM 경유로 호출할 수도 있지만, 실무에서는 래퍼 라이브러리를 사용합니다. 2026년 시점의 선택지와 현재 상황을, 확인된 사실을 바탕으로 정리합니다.

도구 형태 현재 상황(2026년 시점) 신규 도입 기준
FlaUI .NET 라이브러리(MIT) 활발하게 유지보수되는 OSS. v5.0.0이 2025년 2월 릴리스 4 ◎ 1순위 후보
WinAppDriver WebDriver 프로토콜 서버(Microsoft) 최종 안정판 v1.2.1은 2020년 11월. v1.3은 2020년 7월의 RC 상태 그대로. 미해결 이슈 1,100건 초과 5 △ 실질적으로 정지. 신규는 피할 것
Appium Windows Driver Appium의 Windows용 드라이버 내부적으로 WinAppDriver를 이용하므로 위의 제약을 그대로 물려받음 9 △ Appium 자산이 있는 경우에만
Coded UI 테스트 Visual Studio 기능 VS 2019에서 지원 중단(deprecated), VS 2026에서 제거됨 10 × 이전 대상

3.1 FlaUI ── UIA의 얇은 래퍼로서 지금 가장 실용적

FlaUI는 Windows 앱(Win32 / WinForms / WPF / 스토어 앱)의 UI 자동 테스트를 돕는 .NET 라이브러리로, Microsoft의 네이티브 UIA 라이브러리를 감싸는 래퍼로 설계되었습니다.4 패키지는 공통 부분인 FlaUI.Core와, UIA의 어느 구현을 쓰는지에 따라 나뉘는 FlaUI.UIA2 / FlaUI.UIA3로 구성됩니다.

UIA2와 UIA3의 구분 사용법은 FlaUI의 FAQ에 명시되어 있습니다. UIA2는 매니지드 구현만 있어서 터치 같은 새로운 기능에 대응하지 못하고 WPF나 스토어 앱과의 호환성이 나쁩니다. UIA3는 최신 구현으로 WPF / 스토어 앱에는 최적이지만, WinForms 앱에서는 UIA2에는 없는 버그를 만날 수 있습니다──라는 관계입니다.4 즉 실무 기준은 WPF라면 UIA3, WinForms라면 UIA2부터 시도해 보고, 실제 앱에서 더 안정적인 쪽을 쓰는 것입니다. 양쪽 모두 NuGet에서 설치해서 전환하며 시험해 볼 수 있는 것이, 얇은 래퍼로서 FlaUI가 가진 장점입니다.

테스트 프레임워크를 자체적으로 갖고 있지 않으므로, xUnit / NUnit / MSTest와 결합해서 평범한 테스트 프로젝트로 작성합니다. 단위 테스트와 같은 테스트 러너·같은 CI 파이프라인에 올라간다는 점은 운영상 상당히 큰 이점입니다.

3.2 WinAppDriver ── 솔직히 말하면 실질적으로 멈춰 있다

WinAppDriver는 Microsoft가 만든 UI 테스트 서버로, Selenium과 같은 WebDriver 프로토콜로 Windows 앱을 조작할 수 있다는 점에서 한때는 본진으로 취급받았습니다. Visual Studio의 Coded UI 테스트가 지원 중단되었을 때, Microsoft 자신이 이전 대상으로 「웹은 Selenium, 데스크톱과 UWP는 Appium + WinAppDriver」를 안내했던 경위도 있습니다.10

그러나 GitHub에서 현재 상황을 확인해 보면, 최종 안정판 v1.2.1의 릴리스는 2020년 11월로, 그 이후 안정판은 나오지 않았습니다. v1.3은 2020년 7월의 Release Candidate(v1.2.99) 상태 그대로 정식판이 되지 못했고, 미해결 이슈는 1,100건을 넘습니다.5 더 곤란한 점은, GitHub 저장소에 있는 것이 문서·샘플·이슈 트래커뿐이고 서버 본체의 소스 코드는 공개되지 않았다는 것입니다. 즉 커뮤니티가 버그를 고칠 수도 없습니다. 「공식이니까 안심」이라는 이유로 2026년에 신규 도입할 대상은 아니라는 것이 당사의 판단입니다. 이미 WinAppDriver로 동작 중인 테스트 자산이 있다면 당장 버릴 필요는 없지만, 증축은 그만두고 새로운 흐름의 테스트부터 FlaUI로 옮겨 가는 것을 권장합니다.

3.3 Appium Windows Driver와 그 외의 선택지

Appium Windows Driver(appium-windows-driver)는 Appium에서 Windows 앱을 조작하기 위한 드라이버이지만, 그 실체는 「Microsoft가 제공하는 WinAppDriver로의 인터페이스」이며 무거운 처리는 모두 WinAppDriver 쪽이 담당합니다.9 따라서 WinAppDriver의 정체 상태를 그대로 물려받습니다. 모바일이나 웹에서 Appium으로 통일하고 있는 팀, 테스트 코드를 Java나 Python으로 작성하고 싶은 팀에게는 여전히 하나의 선택지가 될 수 있지만, 이 경우에도 WinAppDriver에서 비롯된 제약과 앞으로의 전망을 파악한 뒤 채택하십시오.

WinUI 3(Windows App SDK) 앱도 UIA를 지원하므로 FlaUI(UIA3)로 테스트할 수 있습니다. 다만 WinForms / WPF에 비하면 사례가 많이 쌓여 있지 않아서, 컨트롤별로 노출되는 방식의 특성은 inspect 계열 도구로 사전에 확인하는 것이 더욱 중요해집니다. UI 프레임워크 자체를 선정하는 방법은 「WinForms/WPF/WinUI 선택 방법 - 실무 판단표」에서 정리한 대로입니다.

4. FlaUI를 이용한 최소 구현 ── 실행·검색·조작·검증

이론은 여기까지 하고 실제로 동작하는 코드를 보겠습니다. NuGet에서 FlaUI.UIA3(WinForms에서 불안정하다면 FlaUI.UIA2)와 xUnit을 설치한, 평범한 테스트 프로젝트입니다.

4.1 스모크 테스트의 기본형

「앱을 실행하고, 수주 등록 대화상자를 열고, 1건을 저장한 뒤, 결과가 상태 표시에 나타난다」는 흐름의 테스트는 다음과 같이 작성할 수 있습니다.

using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
using Xunit;

public class OrderSmokeTest
{
    [Fact]
    public void 주문_등록_주요_흐름이_통과한다()
    {
        using var app = Application.Launch(@"C:\App\OrderManager.exe");
        using var automation = new UIA3Automation();
        try
        {
            // 메인 창이 나타날 때까지 기다려 준다
            var window = app.GetMainWindow(automation);

            // AutomationId로 요소를 찾아 Button으로 조작한다
            window.FindFirstDescendant(cf => cf.ByAutomationId("NewOrderButton"))
                  ?.AsButton().Invoke();

            // 대화상자는 비동기로 열리므로 「나타날 때까지」 조건 대기한다
            var dialog = Retry.WhileNull(
                () => window.FindFirstDescendant(
                          cf => cf.ByAutomationId("OrderDialog"))?.AsWindow(),
                timeout: TimeSpan.FromSeconds(5)).Result;
            Assert.NotNull(dialog);

            // Value 패턴을 통해 입력한다(키보드 에뮬레이션이 아님)
            dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox"))
                  .AsTextBox().Text = "테스트상사";
            dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton"))
                  .AsButton().Invoke();

            // 저장 완료도 상태 표시를 조건 대기로 검증한다
            var saved = Retry.WhileFalse(
                () => window.FindFirstDescendant(
                          cf => cf.ByAutomationId("StatusLabel"))
                          ?.Name.Contains("저장했습니다") == true,
                timeout: TimeSpan.FromSeconds(10));
            Assert.True(saved.Success);
        }
        finally
        {
            app.Close();
        }
    }
}

구성 요소는 4가지뿐입니다.

  • 실행: Application.Launch로 프로세스를 실행합니다(이미 실행 중인 앱에 Application.Attach로 연결할 수도 있습니다). GetMainWindow는 메인 창을 가져올 수 있을 때까지 기다립니다.
  • 검색: FindFirstDescendant(cf => cf.ByAutomationId(...))가 트리 검색입니다. cf는 조건 팩토리로, ByName / ByControlType / And 조건도 조합할 수 있습니다. 검색 키는 앞서 말한 대로 AutomationId를 본진으로 삼습니다.
  • 조작: 찾은 요소를 AsButton() / AsTextBox() / AsComboBox() 같은 타입 지정 래퍼로 변환해 조작합니다. Invoke()는 Invoke 패턴, Text 속성은 Value 패턴으로, 2.2절의 컨트롤 패턴이 그대로 뒤에 있습니다.
  • 검증: UI에 나타나는 결과(라벨, 리스트의 행 수, 창 제목 등)를 assert합니다. DB 내용까지 보고 싶다면 테스트 코드에서 직접 DB를 읽어도 됩니다.

4.2 대기는 Retry로 ── Sleep을 쓰면 지는 것

UI 테스트가 불안정해지는(이른바 flaky test) 가장 큰 원인은 타이밍입니다. 대화상자가 열릴 때까지, 데이터가 로드될 때까지, 버튼이 활성화될 때까지──UI는 항상 비동기적으로 변화하는데 테스트 코드가 「이미 표시되어 있을 것」이라고 단정해 버리면, 느린 머신에서만 실패하는 테스트가 만들어집니다.

그렇다고 해서 Thread.Sleep(3000)을 넣는 것은 최악의 대처입니다. 빠른 머신에서는 불필요하게 기다리고, 느린 머신에서는 시간이 부족해 실패하며, 테스트 전체 실행 시간만 쌓여 갑니다. 답은 조건 대기, 즉 「조건이 충족될 때까지 폴링하고, 타임아웃으로 중단한다」이며, FlaUI에는 전용 Retry 클래스가 있습니다.4

// null이 아니게 될 때까지(=요소가 나타날 때까지) 최대 5초 대기
var element = Retry.WhileNull(
    () => window.FindFirstDescendant(cf => cf.ByAutomationId("ResultGrid")),
    timeout: TimeSpan.FromSeconds(5),
    interval: TimeSpan.FromMilliseconds(200),
    throwOnTimeout: true).Result;

// 조건이 true가 될 때까지(=버튼이 활성화될 때까지) 대기
Retry.WhileFalse(
    () => saveButton.IsEnabled,
    timeout: TimeSpan.FromSeconds(5),
    throwOnTimeout: true);

Retry.WhileNull / WhileFalse / WhileTrue / WhileException 등이 준비되어 있어, 타임아웃·폴링 간격·타임아웃 시 예외를 던질지 여부를 지정할 수 있습니다.4 주의할 점은, FlaUI는 2.0 이후 Find 계열 메서드의 암묵적 재시도를 폐지하고 「어디서 기다릴지는 테스트 코드가 명시한다」는 방침으로 바뀌었다는 것입니다.4 FindFirstDescendant는 「지금 이 순간의 트리」만 보기 때문에, 비동기로 나타나는 요소 검색은 반드시 Retry로 감싼다는 것을 규칙으로 삼으십시오. 참고로 「Sleep으로 때우지 않고 대기 조건을 세운다」는 발상 자체는 UI 테스트에 한정되지 않고 Windows 프로그래밍 전반의 정석입니다(「Windows에서 Sleep(1)보다 이벤트 대기를 우선해야 하는 이유」).

5. 잘 깨지지 않게 만드는 설계 ── 앱 쪽과 테스트 쪽의 규칙

UI 테스트가 「유지보수를 버티지 못하고 포기」당하는 원인은 거의 다음 4가지로 압축됩니다. 표시 문자열에 의존하는 검색, 좌표 클릭, Sleep, 그리고 구조 공유 부족입니다. 각각을 규칙으로 없앱니다.

5.1 AutomationId는 개발 쪽에서 반드시 부여한다

가장 중요한 것이 바로 이것입니다. 테스트가 잘 깨지지 않는 정도는 테스트 코드를 작성하는 방식보다 먼저, 앱이 안정된 식별자를 노출하고 있는가로 결정됩니다. AutomationId는 바로 그것을 위한 속성으로, 로케일에 의존하지 않고 형제 요소 안에서 유일해야 한다는 것이 사양으로 요구됩니다.6

WPF에서는 x:Name을 붙인 요소가 그대로 UIA 쪽 식별자로 사용되므로, 이름이 붙어 있는 컨트롤은 추가 작업 없이 테스트할 수 있습니다. 데이터 템플릿 안 등에서 명시적으로 부여하고 싶다면 첨부 속성 AutomationProperties.AutomationId를 설정합니다.67

<!-- x:Name이 그대로 식별자가 된다 -->
<Button x:Name="SaveButton" Content="저장" Click="OnSave" />

<!-- 템플릿 내부 등에서는 AutomationProperties.AutomationId를 명시 -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
        Content="삭제"
        Command="{Binding DeleteCommand}" />

WinForms에서는 디자이너에서 붙이는 Control.Name(button1saveButton으로 바꾸는, 그 이름입니다)이 UIA 쪽 식별에 사용됩니다. 당사의 경험으로는 Name을 규칙대로 붙여 놓은 폼은 그대로 AutomationId로 검색할 수 있는 경우가 대부분이지만, 프레임워크 세대나 컨트롤에 따라 노출되는 방식에 차이가 있으므로 반드시 inspect 계열 도구로 실제 AutomationId를 확인한 뒤에 테스트의 검색 키로 사용하십시오. 아울러 스크린 리더용 읽기 이름이 되는 UIA의 Name 속성은 많은 컨트롤에서 Text 속성이 그대로 재사용되지만, TextBox나 ListView처럼 재사용되지 않는 종류에서는 AccessibleName을 명시적으로 설정해야 합니다.11 AutomationId를 정비하는 작업이 테스트만을 위한 것이 아니라 접근성 대응과 완전히 같은 작업이라는 점은, 사내에서 예산을 통과시킬 때 좋은 근거가 됩니다.

규칙은 단순해도 충분하며, 당사에서는 다음 2가지를 코딩 규칙에 추가하도록 하고 있습니다.

  • 화면에 배치하는 컨트롤에는, 조작·검증 대상이 될 가능성이 있는 한 의미 있는 Name(WinForms) / x:Name 또는 AutomationProperties.AutomationId(WPF)를 붙인다
  • 한 번 테스트에서 참조된 식별자는 이름을 바꿀 때 테스트 쪽도 동시에 변경한다(식별자는 공개 API로 취급한다)

5.2 좌표 클릭 금지

「화면 좌표 (830, 412)를 클릭한다」는 형태의 조작은 창 위치·해상도·DPI 스케일링·테마·글꼴 설정 중 무엇이 바뀌어도 깨집니다. 특히 DPI는 개발 머신 100% / CI 머신 150% 같은 환경 차이로 인해 「로컬에서는 통과하는데 CI에서는 실패한다」를 대량으로 만들어 냅니다(DPI의 원리는 「WinForms의 고DPI 대응」에서 쓴 대로입니다). 2장에서 본 것처럼 UIA의 컨트롤 패턴을 통한 조작은 좌표에 의존하지 않습니다. FlaUI에도 마우스를 직접 조작하는 API가 있지만, 사용해도 되는 것은 드래그 앤 드롭이나 그림 캔버스처럼 패턴으로 표현할 수 없는 조작뿐이라고 정해 둡니다. 그리고 그 경우에도 화면 좌표가 아니라 요소의 BoundingRectangle에서 상대 위치를 계산합니다.

5.3 Page Object 패턴으로 구조를 한 곳에 모은다

FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) 같은 검색 코드를 테스트 본체에 그대로 박아 넣으면, 화면 구성이 바뀔 때 모든 테스트를 수정해야 하는 처지가 됩니다. 정평이 난 대처법이 Page Object 패턴으로, 「화면(또는 대화상자) 하나당 클래스 하나」를 만들어 요소 검색과 조작을 그 안에 가둡니다.

public sealed class OrderDialogPage
{
    private readonly Window _dialog;
    public OrderDialogPage(Window dialog) => _dialog = dialog;

    // 요소 검색은 이 클래스 안에만 작성한다
    private TextBox CustomerName =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")).AsTextBox();
    private Button Save =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton")).AsButton();
    private Label Status =>
        _dialog.FindFirstDescendant(cf => cf.ByAutomationId("StatusLabel")).AsLabel();

    // 테스트에서 보이는 것은 「업무 용어」로 표현된 조작뿐
    public void Register(string customerName)
    {
        CustomerName.Text = customerName;
        Save.Invoke();
        // 요소 검색과 null 판정까지 재시도 안에서 수행한다. 상태 레이블이
        // 저장 후 생성·다시 그려지는 화면에서는 검색이 한순간 null이나 예외가 되는 것이
        // 정상 동작이기 때문(ignoreException 없이는 첫 예외에서 그대로 실패한다)
        Retry.WhileFalse(() => Status?.Name.Contains("저장했습니다") == true,
            timeout: TimeSpan.FromSeconds(10), throwOnTimeout: true,
            ignoreException: true);
    }
}

테스트 본체는 new OrderDialogPage(dialog).Register("테스트상사")처럼 업무 용어로 작성할 수 있게 되고, 화면 변경의 영향은 Page Object 수정 한 곳으로 끝납니다. 화면이 10개를 넘는 앱에서 UI 테스트를 계속 운영한다면 이 패턴은 사실상 필수입니다. 한 가지만 주의할 점은, 대기 조건에서 참조하는 요소는 위의 Status처럼 속성을 통해 매번 다시 검색하는 형태를 지켜야 한다는 것입니다. 검색 결과를 필드에 캐시하면, 다시 그려지면서 사라진 오래된 요소를 붙잡은 채로 타임아웃까지 계속 기다리게 됩니다.

5.4 테스트의 독립성 ── 상태를 주고받지 않는다

UI 테스트는 하나하나를 독립시킵니다. 「테스트 3은 테스트 2가 만든 데이터를 전제로 한다」와 같은 순서 의존성을 만들면, 하나의 실패가 연쇄적으로 번지고 순서를 바꿀 수도 없게 됩니다. 원칙은 다음과 같습니다.

  • 각 테스트(또는 테스트 클래스)가 스스로 앱을 실행하고, 끝나면 확실히 닫는다(finallyIDisposable 픽스처로)
  • 전제가 되는 데이터는 테스트 쪽이 준비한다. 앱의 설정 파일·DB를 테스트용으로 바꿔 끼울 수 있는 실행 옵션을 앱 쪽에 마련해 두면 훨씬 편해집니다
  • 실패했을 때의 뒷정리를 잊지 않는다. 제대로 닫히지 않은 프로세스나 모달 대화상자의 잔재는 다음 테스트가 실패하는 원인이 됩니다(7장)

6. 어디까지 UI 테스트로 지킬 것인가 ── 스모크 중심의 선 긋기

도구가 갖춰지면 모든 화면을 테스트하고 싶어지지만, 여기가 갈림길입니다. UI 테스트는 단위 테스트에 비해 압도적으로 느리고(1건에 수 초~수십 초), 잘 깨지고(UI가 바뀔 때마다 따라가야 함), 실패 원인을 파악하는 데 시간이 걸립니다(앱의 버그인지, 테스트의 버그인지, 환경 문제인지). 테스트 피라미드의 최상단이 얇게 그려지는 것은 바로 이 비용 구조 때문이며, 아래 계층에서 지킬 수 있는 것을 UI 테스트로 지키는 것은 언제나 손해입니다.

판단표 형태로 정리하면 다음과 같습니다.

지키고 싶은 것 적합한 계층 이유
계산·변환·업무 규칙 단위 테스트 빠르고 안정적. UI를 통해 전부 검증하는 것은 논외
DB 접근·파일 I/O·외부 연동 통합 테스트 실제 대상을 사용하되 UI는 불필요(경계를 긋는 방법)
ViewModel·프레젠테이션 로직 단위 테스트 MVVM이라면 UI 없이 테스트 가능
실행할 수 있다·주요 흐름이 통과한다·저장할 수 있다 UI 스모크 테스트 여기가 UI 테스트의 주전장
과거 수동 확인에서 놓친 치명적인 흐름 UI 테스트(회귀) 실제 피해가 있었던 부분에 한정해서 추가
화면 레이아웃·겉모습이 깨지는 것 눈으로 확인·스크린샷 비교 assert로 작성하면 유지보수 지옥. 매수를 좁힌다
크래시·핸들 누수 등의 이상 계열 별도의 기반 UI 테스트가 지킬 범위 밖(Application Verifier)

당사가 권장하는 시작 방법은 「스모크 10건 전후」입니다. 「실행해서 메인 화면이 나온다」「주요 마스터를 열 수 있다」「대표적인 전표를 1건 등록·검색·인쇄 미리보기할 수 있다」「종료 시 오류가 나지 않는다」──릴리스 전에 반드시 손으로 확인하던 흐름 중 상위 10건을 그대로 자동화합니다. 이 규모라면 작성에도 1~2주, 매일 밤 실행에도 20~30분이면 충분하고, 유지보수 부담도 현실적입니다. 효과를 체감한 뒤, 실제 피해가 있었던 회귀 버그의 재발 방지 테스트를 하나씩 추가해 갑니다. 반대로 「모든 화면·모든 항목」을 목표로 세운 계획은 거의 확실하게 중간에 무너집니다.

또 하나 중요한 것은, UI 테스트를 늘리는 대신 로직을 UI에서 떼어내는 방향의 투자입니다. 이벤트 핸들러에 업무 로직이 직접 작성된 화면은 UI 테스트로만 지킬 수 있지만, 로직을 ViewModel이나 서비스 클래스로 옮기면 단위 테스트로 지킬 수 있게 되고 UI 테스트는 「배선 확인」만으로 충분해집니다. UI 테스트가 많이 필요하다고 느껴진다면, 그것은 테스트의 문제가 아니라 설계의 문제인 경우가 많다는 것이 체감입니다. 테스트 전체를 어떻게 계층화할지는 「유닛 테스트와 통합 테스트의 경계를 어떻게 그을 것인가」, 하위 계층 테스트의 실무는 「자체 제작 로거의 최소 요건과 통합 테스트 체크리스트」도 함께 참조하십시오.

7. CI·무인 실행의 함정 ── 데스크톱이 없으면 UI 테스트는 동작하지 않는다

작성한 UI 테스트를 개발자가 직접 손에서 실행하는 동안은 평화롭습니다. 함정이 몰려 있는 곳은 「CI에서 매일 밤 무인으로 실행한다」는 단계이며, 웹의 UI 테스트(헤드리스 브라우저로 끝나는)와 달리 데스크톱 앱의 테스트는 진짜 대화형 데스크톱 세션을 요구한다는 점이 모든 문제의 근원입니다.

7.1 대화형 세션 필수 ── 서비스로 실행되는 에이전트에서는 동작하지 않는다

CI 에이전트(Azure Pipelines 에이전트, GitHub Actions의 셀프 호스트 러너 등)는 보통 Windows 서비스로 상시 실행합니다. 그런데 서비스에는 사용자 데스크톱이 없기 때문에, 거기서 실행한 앱의 창은 조작할 수 없습니다. Azure Pipelines 공식 문서도, 데스크톱 앱의 UI 테스트를 실행하는 에이전트는 서비스가 아니라 자동 로그인(autologon)을 활성화한 대화형 프로세스로 구성해야 한다고 명시하고 있습니다.8 또한 Microsoft 호스티드 에이전트(클라우드 쪽에서 제공되는 공유 러너)에서는 화면에 보이는 UI 테스트가 지원되지 않으며, 헤드리스 브라우저 테스트만 동작합니다.8데스크톱 앱의 UI 테스트에는 셀프 호스트 머신(물리 머신이든 VM이든)이 사실상 필수입니다.

autologon 구성에는 「그 머신에 물리적으로 접근할 수 있는 사람은 자동 로그인된 계정을 사용할 수 있게 된다」는 보안 위험이 공식 문서에도 명시되어 있습니다.8 테스트 전용 계정·전용 머신(VM)으로 만들고, 프로덕션 자격 증명을 두지 않는 것이 전제입니다.

7.2 화면 잠금·RDP 연결 해제·해상도 ── 흔히 실패하는 패턴

대화형 세션을 준비해도 아직 함정이 남아 있습니다. 증상과 대책을 표로 정리합니다.

함정 증상 대책
에이전트가 서비스로 실행됨 요소가 전혀 발견되지 않음·앱이 실행되지 않음 대화형 프로세스+autologon으로 다시 구성 8
화면 잠금·화면 보호기 입력 계열 조작이 전달되지 않아 실패 autologon 구성에서 화면 보호기 비활성화. 잠금을 유발하는 GPO의 예외 신청 8
RDP를 「×」로 연결 해제 연결을 끊은 순간 세션이 잠기고, 이후 테스트가 전멸 tscon <세션ID> /dest:console로 콘솔로 되돌린 뒤 연결을 해제한다 8
해상도·DPI의 환경 차이 로컬에서는 통과하는 테스트가 CI에서만 실패 해상도를 고정(Azure Pipelines에는 설정 태스크 있음). 스케일링은 100%로 통일 8
테스트의 병렬 실행 마우스·키보드·포커스를 서로 빼앗아 상호 파괴 UI 테스트는 머신 1대당 1건씩 직렬 실행. 병렬화는 머신(VM)을 늘려서 수행
지난번 실패의 잔재 남은 프로세스나 모달 대화상자가 다음 실행을 방해 테스트 시작 전에 대상 프로세스를 정리. 종료 처리를 finally로 반드시 실행

RDP의 함정은 특히 밟기 쉬우므로 보충 설명합니다. 테스트 머신에 원격 데스크톱으로 들어가 조정한 뒤 창을 닫고 나가면, 그 세션은 잠긴 상태가 되어 이후의 UI 테스트가 계속 실패합니다. 공식 문서가 안내하는 회피책은 연결을 끊기 전에 관리자 명령 프롬프트에서 %windir%\System32\tscon.exe <ID> /dest:console을 실행해 세션을 콘솔로 돌려놓는 것입니다.8 테스트 머신의 운영 절차서에 반드시 넣어 두십시오.

DPI·해상도도 주의가 필요합니다. CI용 VM이 1024×768·스케일링 기본값 그대로인 경우가 드물지 않으며, 레이아웃이 바뀌어서 「개발 머신에서는 보이던 버튼이 스크롤하지 않으면 보이지 않는다」와 같은 차이가 발생합니다. 좌표 클릭을 배제해 두었다면 대부분 흡수할 수 있지만, 환경은 고정해 두는 것이 낫습니다. 앱 쪽의 DPI 대응 상황을 포함해, 「WinForms의 고DPI 대응」에서 쓴 확인 관점을 그대로 사용할 수 있습니다.

7.3 실패 시의 증거를 남긴다 ── 스크린샷과 로그

무인 실행 중인 UI 테스트가 실패했을 때, 로그에 「요소를 찾을 수 없습니다」라고만 남아 있으면 원인을 알 수 없습니다. 실패 시 스크린샷 저장을 처음부터 심어 둡니다. FlaUI에는 화면이나 요소를 캡처하는 기능이 있으므로, 테스트 프레임워크의 실패 훅에서 호출해 CI의 산출물(아티팩트)로 저장합니다.

// 실패 시 훅 등에서 호출한다. CI의 아티팩트 저장 위치로 출력한다
FlaUI.Core.Capturing.Capture.Screen()
    .ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));

스크린샷에 더해, 앱 자체의 로그(어디까지 처리가 진행되었는지)와 테스트 쪽 로그(어느 조작까지 성공했는지)를 타임스탬프로 맞춰 볼 수 있게 해 두면 「앱의 버그인지, 테스트의 버그인지, 환경 문제인지」를 가려내는 속도가 훨씬 빨라집니다. 테스트 실행을 작업 스케줄러로 야간에 돌릴 때의 주의점(세션 종류, 0x1로 끝나는 문제 등)은 「작업 스케줄러 작업이 실행되지 않는다·0x1로 끝난다」에서 쓴 대로입니다.

8. 정리

UI 자동 테스트는 「도구를 설치하면 돌아가는」 것이 아니라, 원리의 이해·앱 쪽의 협력·지킬 범위의 선 긋기·실행 환경의 설계가 모두 갖춰져야 비로소 계속 돌아갑니다. 핵심을 압축합니다.

  • 토대는 UI Automation. 트리에서 AutomationId로 찾고, 컨트롤 패턴으로 조작한다. 먼저 inspect / Accessibility Insights로 대상 앱이 어떻게 노출되는지 점검한다
  • 도구는 FlaUI + xUnit / NUnit. WinAppDriver는 2020년 이후 안정판이 나오지 않았으므로 신규 도입은 피한다
  • 잘 깨지지 않는 성질은 규칙으로 만든다. AutomationId를 개발 쪽에서 부여하고, 좌표 클릭을 금지하고, Sleep을 금지하고 Retry로 조건 대기하며, Page Object로 구조를 한 곳에 모은다
  • 지킬 범위는 스모크 10건 전후부터. 로직은 단위·통합 테스트로 옮기고, UI 테스트는 「주요 흐름이 통과한다」는 확인에만 집중한다
  • CI는 셀프 호스트 머신+대화형 세션+autologon이 기본형. 화면 잠금·RDP 연결 해제·해상도 차이의 함정을 운영 절차로 없애고, 실패 시 스크린샷을 반드시 남긴다

「릴리스 전 2일간의 수동 확인」은 제대로 범위를 좁힌 UI 자동 테스트라면 현실적인 비용으로 대체할 수 있습니다. 반대로 현재 앱에 AutomationId가 전혀 부여되어 있지 않다거나, 로직이 화면 이벤트에 직접 작성되어 있는 경우라면, 테스트를 작성하기 전에 앱 쪽의 작은 개선부터 시작하는 것이 결국 지름길입니다. 어디서부터 손을 대야 할지 판단하는 것부터 스모크 테스트 세트를 구축하는 것까지, 앱의 현재 상태 조사부터 도와드릴 수 있습니다.

관련 글

관련 상담 분야

합동회사 코무라소프트는 WinForms / WPF 앱에 대한 UI 자동 테스트 도입(현재 상태 조사, AutomationId 정비, 스모크 테스트 세트 구축, CI 환경 설계), 기존 테스트 자산의 FlaUI 이전, 테스트 전략 전체의 설계 리뷰를 다루고 있습니다.

참고 자료

  1. Microsoft Learn, UI Automation Overview. 데스크톱을 루트로 하는 오토메이션 트리, raw / 컨트롤 / 콘텐츠 뷰, 요소의 속성과 컨트롤 패턴, 보조 기술과 테스트 자동화가 같은 기반을 사용한다는 점에 대해.  2 3

  2. Microsoft Learn, UI Automation Control Patterns Overview. 컨트롤 패턴의 설계(COM의 인터페이스와의 유사성), Invoke / Value / SelectionItem 등의 패턴, 그리고 하나의 컨트롤이 여러 패턴을 구현할 수 있다는 점에 대해.  2

  3. Microsoft Learn, Accessibility tools - Inspect. inspect.exe가 Windows SDK에 포함되어 UIA 속성과 패턴을 확인할 수 있다는 점, 레거시 도구로 분류되어 Accessibility Insights가 권장되고 있다는 점에 대해.  2 3

  4. GitHub, FlaUI/FlaUI. Win32 / WinForms / WPF / 스토어 앱을 지원하는 UIA 래퍼라는 점, FlaUI.Core / UIA2 / UIA3의 패키지 구성, UIA2와 UIA3의 구분 사용법(FAQ), MIT 라이선스, v5.0.0(2025년 2월) 릴리스와 개발의 지속, Retry 유틸리티와 2.0 이후 Find 계열 암묵적 재시도 폐지에 대해.  2 3 4 5 6 7 8

  5. GitHub, microsoft/WinAppDriver. 최종 안정판 v1.2.1이 2020년 11월에 릴리스되었다는 점, v1.3이 2020년 7월의 Release Candidate 상태로 갱신되지 않고 있다는 점, 미해결 이슈가 1,100건을 넘는다는 점, 저장소가 문서·샘플 중심이고 서버 본체의 소스가 공개되어 있지 않다는 점에 대해.  2 3

  6. Microsoft Learn, Use the AutomationID Property. AutomationId가 로케일에 의존하지 않는 식별자라는 점, 형제 요소 안에서 유일해야 한다는 점, 테스트 스크립트에서 검색 키로 사용하는 시나리오, WPF에서 ID(x:Name)나 x:Uid가 없는 컨트롤은 AutomationId가 지원되지 않는다는 점에 대해.  2 3 4 5

  7. Microsoft Learn, AutomationProperties.AutomationId Attached Property. WPF(System.Windows.Automation 네임스페이스)에서 요소를 유일하게 식별하는 문자열을 설정하는 첨부 속성의 정의에 대해.  2

  8. Microsoft Learn, UI testing considerations (Azure Pipelines). 데스크톱 앱의 UI 테스트에는 autologon을 활성화한 대화형 프로세스로서의 에이전트 구성이 필요하다는 점, Microsoft 호스티드 에이전트에서 화면에 보이는 UI 테스트가 지원되지 않는다는 점, RDP 연결 해제로 인한 잠금과 tscon을 통한 회피, 화면 해상도 설정 태스크, 실패 시 스크린샷·동영상 수집에 대해.  2 3 4 5 6 7 8 9

  9. GitHub, appium/appium-windows-driver. Appium Windows Driver가 Microsoft가 제공하는 WinAppDriver로의 인터페이스이며, WinAppDriver 서버가 오랫동안 유지보수되지 않고 있다고 README에서 주의를 주고 있다는 점에 대해.  2

  10. Microsoft Learn, Use Coded UI tests to test your code. Coded UI 테스트가 지원 중단되었으며 Visual Studio 2019가 완전히 지원되는 마지막 버전이라는 점, 이전 대상으로 데스크톱 / UWP 앱에는 Appium + WinAppDriver가 안내되었던 점에 대해(VS 2026에서의 제거는 같은 Learn 문서 내의 이전 가이드에 기재).  2

  11. Microsoft Learn, WinForms: Setting the accessible name on a control. WinForms 컨트롤의 UIA Name 속성이 많은 종류에서 Text 속성으로부터 재사용된다는 점, TextBox / ListBox 등에서는 AccessibleName을 명시적으로 설정해야 한다는 점에 대해. 

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

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

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

자주 묻는 질문

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

WinForms/WPF 앱의 UI 자동 테스트에는 무엇을 사용해야 하나요?
당사의 추천은 FlaUI + xUnit/NUnit입니다. FlaUI는 Windows UI Automation(UIA)을 얇게 감싼 MIT 라이선스 OSS로, UIA2/UIA3 양쪽을 모두 지원하며 2025년 2월에 v5.0.0이 릴리스되는 등 활발하게 유지보수되고 있습니다. 구분해서 사용하는 기준은 WPF라면 UIA3, WinForms라면 UIA2부터 시도해 보는 것입니다. 테스트 프레임워크를 자체적으로 갖고 있지 않으므로, 단위 테스트와 같은 테스트 러너·같은 CI 파이프라인에 태울 수 있습니다. 코드를 작성하기 전에 inspect.exe나 Accessibility Insights for Windows로 대상 앱의 요소가 어떻게 노출되는지 확인하는 것이 첫 작업입니다.
WinAppDriver를 지금 새로 도입해도 되나요?
신규 도입은 권장하지 않습니다. Microsoft가 만든 WinAppDriver는 최종 안정판 v1.2.1이 2020년 11월에 멈춰 있고, v1.3은 2020년 7월의 Release Candidate 상태 그대로이며, 미해결 이슈가 1,100건을 넘습니다. 게다가 GitHub 저장소에 있는 것은 문서와 샘플뿐이고 서버 본체의 소스 코드는 비공개이므로 커뮤니티가 버그를 고칠 수도 없습니다. Appium의 Windows Driver도 내부적으로 WinAppDriver를 사용하기 때문에 같은 제약을 그대로 물려받습니다. 기존 테스트 자산이 있다면 당장 버릴 필요는 없지만, 새로운 흐름의 테스트부터는 FlaUI로 옮겨 가는 것을 권장합니다.
UI 자동 테스트가 잘 깨지는 문제는 어떻게 방지하나요?
잘 깨지지 않는 정도의 80%는 개발 쪽에서 AutomationId를 부여하는 규칙으로 결정됩니다. WPF는 x:Name이나 AutomationProperties.AutomationId, WinForms는 Control.Name이 식별자가 됩니다. 그 위에서, 표시 문자열(Name)에 의존하는 검색과 좌표 클릭을 금지하고, Thread.Sleep 대신 FlaUI의 Retry 클래스를 이용한 조건 대기를 사용하며, 화면마다 요소 검색과 조작을 가두는 Page Object 패턴으로 구조를 한 곳에 모읍니다. FlaUI는 2.0부터 Find 계열 메서드의 암묵적 재시도를 폐지했으므로, 비동기로 나타나는 요소 검색은 반드시 Retry로 감싸는 것을 규칙으로 삼으십시오.
UI 자동 테스트는 어느 범위까지 작성해야 하나요?
스모크 테스트 10건 전후로 시작하는 것을 권장합니다. UI 테스트는 단위 테스트에 비해 압도적으로 느리고 잘 깨지기 때문에, 계산·업무 규칙은 단위 테스트, DB 접근은 통합 테스트로 지키고, UI 테스트는 「실행되고 주요 흐름이 통과한다」는 확인으로만 범위를 좁힙니다. 릴리스 전에 손으로 확인하던 흐름 중 상위 10건을 자동화하면, 작성에 1~2주, 매일 밤 실행에도 20~30분이면 충분합니다. 반대로 「모든 화면·모든 항목」을 목표로 세운 계획은 거의 확실하게 실패합니다. UI 테스트가 많이 필요하다고 느껴진다면, 로직을 ViewModel로 떼어내어 단위 테스트로 지킬 수 있도록 설계를 개선해야 한다는 신호입니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기