본문 바로가기

[WPF] 프로젝트, 기본 파일 3개를 싹 지우고 시작하는 이유

@devBong2026. 8. 14. 07:15

"MainWindow.xaml, App.xaml, AssemblyInfo.cs를 왜 지우냐고요? 그게 DI를 막고 있거든요."


들어가며

Visual Studio에서 WPF 프로젝트를 새로 만들면 파일 3개가 딸려옵니다.

📁 WpfExplorer
 ├── App.xaml (+ App.xaml.cs)
 ├── MainWindow.xaml (+ MainWindow.xaml.cs)
 └── AssemblyInfo.cs

그런데 제 프로젝트는 이렇게 생겼습니다.

📁 WpfExplorer
 ├── App.cs
 └── Starter.cs

셋 다 지우고, 파일 두 개를 손으로 만들었습니다. 처음 보면 "굳이 왜?" 싶으실 겁니다. 이 글에서 그 이유를 처음부터 차근차근 풀어보겠습니다. 결론부터 말씀드리면 딱 한 문장입니다.

윈도우를 누가 만들 것인가 — 그 주도권을 WPF 프레임워크에서 뺏어와 DI 컨테이너에게 넘기기 위해서.

나머지 이야기는 전부 이 한 문장에서 파생됩니다. 자, 시작해 볼까요.


1. 그런데 WPF 앱은 대체 어디서 시작되나요?

C# 콘솔 앱을 만들어 보신 분은 아실 겁니다. 모든 .NET 프로그램은 Main 메서드에서 시작합니다. 예외는 없습니다.

static void Main(string[] args)
{
    Console.WriteLine("여기서 시작!");
}

그럼 질문 하나 드리겠습니다.

WPF 프로젝트를 열어보면 Main이 어디에 있나요?

한번 찾아보세요. App.xaml.cs를 열어봐도 없고, MainWindow.xaml.cs를 열어봐도 없습니다. 그런데 F5를 누르면 앱이 실행됩니다. 이상하죠?

정답: 빌드할 때 자동으로 만들어집니다

비밀은 App.xaml에 있습니다. 이 파일을 우클릭해서 속성을 보면 빌드 작업(Build Action) 이 ApplicationDefinition으로 되어 있습니다. 다른 XAML 파일들이 Page인 것과 다릅니다.

이 표시를 본 MSBuild는 빌드할 때 몰래 코드를 하나 생성합니다. obj 폴더에 있는 App.g.cs라는 파일인데요, 열어보면 대충 이렇게 생겼습니다.

// obj/Debug/net8.0-windows/App.g.cs  ← 자동 생성 파일
public partial class App : System.Windows.Application
{
    public void InitializeComponent()
    {
        this.StartupUri = new System.Uri("MainWindow.xaml", UriKind.Relative);
    }

    [System.STAThreadAttribute()]
    public static void Main()
    {
        App app = new App();
        app.InitializeComponent();
        app.Run();
    }
}

여기 있었네요, Main이!

정리하면 이렇습니다.

내가 쓴 것 빌드가 만들어 준 것

App.xaml (ApplicationDefinition) Main 메서드
StartupUri="MainWindow.xaml" 앱 시작 시 MainWindow를 자동으로 띄우는 코드

이걸 알고 나면 다음 이야기가 술술 풀립니다.


2. 문제의 발단 — new를 손으로 치기 시작하면

이제 실전 앱을 만든다고 해봅시다. 파일 탐색기를 만들 거라, 파일 목록을 읽어오는 서비스가 필요합니다.

public class MainWindow : Window
{
    private readonly FileService _fileService;

    public MainWindow()
    {
        _fileService = new FileService();  // 일단 이렇게
    }
}

잘 돌아갑니다. 그런데 시간이 지나면서 요구사항이 붙습니다.

  • 파일 목록을 읽을 때 로그를 남겨야 한다 → FileService가 Logger를 필요로 함
  • 로그는 파일로 저장해야 한다 → Logger가 LogWriter를 필요로 함
  • 설정 파일에서 로그 경로를 읽어야 한다 → LogWriter가 ConfigService를 필요로 함

그럼 코드가 이렇게 됩니다.

public MainWindow()
{
    _fileService = new FileService(
        new Logger(
            new LogWriter(
                new ConfigService()
            )
        )
    );
}

이걸 의존성 지옥이라고 부릅니다. 그리고 진짜 문제는 이겁니다.

  • Logger를 쓰는 곳이 10군데면, 이 new 덩어리를 10번 복붙해야 합니다.
  • Logger 생성자에 인자가 하나 추가되면? 10군데를 전부 고쳐야 합니다.
  • 테스트할 때 FileService를 가짜(Mock)로 바꾸고 싶은데, new로 박아놨으니 바꿀 방법이 없습니다.

해결책: 조립은 남에게 맡기자

그래서 나온 게 DI(Dependency Injection, 의존성 주입) 입니다. 발상은 아주 단순합니다.

"나는 FileService가 필요하다고만 말할게. 그걸 만드는 건 네가 해."

말로 하면 이렇게 바뀝니다.

public class MainWindow : Window
{
    private readonly FileService _fileService;

    // 필요한 걸 생성자로 "받기만" 한다. 만들지 않는다.
    public MainWindow(FileService fileService)
    {
        _fileService = fileService;
    }
}

그럼 누가 만들어 주냐고요? DI 컨테이너가 만들어 줍니다. 이렇게 미리 등록만 해두면 됩니다.

services.AddSingleton<ConfigService>();
services.AddSingleton<LogWriter>();
services.AddSingleton<Logger>();
services.AddSingleton<FileService>();
services.AddSingleton<MainWindow>();

이제 컨테이너에게 MainWindow 하나만 달라고 하면, 컨테이너가 알아서 순서를 계산해서 ConfigService → LogWriter → Logger → FileService → MainWindow 순으로 조립해서 완성품을 건네줍니다.

이게 우리가 쓸 Microsoft.Extensions.DependencyInjection 패키지가 하는 일입니다.

dotnet add package Microsoft.Extensions.DependencyInjection

3. 그런데 StartupUri가 이걸 막습니다

자, 이제 핵심입니다. 이 글의 심장부니까 집중해 주세요.

방금 배운 대로 MainWindow에 생성자를 만들어 봅시다.

public partial class MainWindow : Window
{
    public MainWindow(FileService fileService)  // 파라미터 추가!
    {
        InitializeComponent();
    }
}

F5를 누릅니다. 그리고 터집니다.

1강에서 본 자동 생성 코드를 다시 떠올려 보세요.

this.StartupUri = new System.Uri("MainWindow.xaml", UriKind.Relative);

StartupUri는 WPF에게 이렇게 말하는 겁니다.

"앱이 켜지면 MainWindow.xaml을 찾아서, 직접 new로 만들어서 화면에 띄워줘."

여기서 문제가 발생합니다. WPF는 이 윈도우를 만들 때 매개변수 없는 생성자만 호출할 수 있습니다. FileService를 어디서 구해다 넣어줄지 WPF는 알 방법이 없으니까요.

정리하면 이렇습니다.

윈도우를 만드는 주체 결과

StartupUri 방식 WPF 프레임워크 매개변수 없는 생성자만 가능 → DI 불가
우리가 원하는 방식 DI 컨테이너 생성자 주입 가능

주도권 다툼입니다. 둘 다 가질 수는 없어요. 그래서 우리는 StartupUri를 버리기로 합니다.

그런데 StartupUri는 App.xaml 안에 있고, App.xaml은 Main도 같이 만들어 주고 있었죠. 그래서 App.xaml을 지우면 Main도 같이 사라집니다.

바로 이 지점에서 Starter.cs가 등장합니다.


4. Starter.cs — 사라진 Main을 되찾기

App.xaml을 지웠으니 Main을 직접 써줘야 합니다.

namespace WpfExplorer
{
    internal class Starter
    {
        [STAThread]
        private static void Main(string[] args)
        {
            _ = new App().Run();
        }
    }
}

딱 5줄입니다. 그런데 이 5줄 안에 알아야 할 게 세 가지 있습니다.

① [STAThread]는 절대 빼먹으면 안 됩니다

STA는 Single-Threaded Apartment의 약자로, 윈도우의 오래된 COM 기술에서 온 개념입니다. 어렵게 생각할 필요 없고, 그냥 이렇게 기억하세요.

"윈도우 UI는 반드시 한 스레드에서만 다뤄야 한다" 는 규칙을 지키겠다는 선언.

클립보드, 드래그 앤 드롭, 파일 열기 대화상자 같은 기능들이 전부 이 규칙 위에서 동작합니다. 이걸 빼면 앱이 켜지자마자 이런 예외를 던지고 죽습니다.

System.InvalidOperationException:
The calling thread must be STA, because many UI components require this.

원래 자동 생성 Main에 붙어 있던 거라 있는지도 몰랐던 건데, 직접 쓰게 되니 챙겨야 하는 겁니다.

② Run()이 실제로 앱을 굴립니다

new App()은 그냥 객체 하나 만든 것뿐이고, 실제로 창을 띄우고 사용자 입력을 기다리는 건 Run()입니다. Run() 안에서는 대략 이런 무한 루프가 돌아갑니다.

메시지 대기 → 마우스 클릭 도착 → 버튼에게 전달 → 다시 대기 → ...

사용자가 창을 닫으면 이 루프가 끝나고, Run()이 반환되면서 프로그램이 종료됩니다. 그래서 Main이 5줄이어도 앱이 바로 꺼지지 않는 겁니다.

③ 왜 App에 안 넣고 굳이 Starter를 따로 만들까?

솔직히 말씀드리면, App 클래스 안에 static Main을 넣어도 똑같이 동작합니다. 이건 취향과 설계 철학의 문제인데, 이유를 대자면 이렇습니다.

  • App의 역할 = 애플리케이션의 생명주기 관리 + DI 컨테이너 구성
  • Starter의 역할 = 운영체제가 프로세스를 시작하는 진입 지점

이 둘은 성격이 다른 일입니다. 섞어놓으면 App 클래스가 "앱 그 자체"이면서 동시에 "앱을 켜는 사람"이 되어버려서, 읽을 때 살짝 혼란스럽습니다. 파일을 나눠두면 역할이 눈에 딱 보이죠.


5. App.cs 한 줄씩 뜯어보기

이제 본체입니다. 전체 코드를 먼저 보고, 블록별로 나눠서 보겠습니다.

using Microsoft.Extensions.DependencyInjection;
using System.Windows;

namespace WpfExplorer
{
    internal class App : Application
    {
        private readonly ServiceProvider _provider;

        public App()
        {
            ServiceCollection services = new();
            RegisterTypes(services);
            _provider = services.BuildServiceProvider();
        }

        private void RegisterTypes(IServiceCollection services)
        {
        }

        private Window CreateShell()
        {
            return new Window();
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            Window shell = CreateShell();
            MainWindow = shell;
            shell.Show();
        }

        protected override void OnExit(ExitEventArgs e)
        {
            _provider.Dispose();
            base.OnExit(e);
        }
    }
}

생성자 — 부품 등록하고 공장 짓기

public App()
{
    ServiceCollection services = new();   // ① 부품 목록표를 꺼낸다
    RegisterTypes(services);              // ② 목록표에 부품을 적는다
    _provider = services.BuildServiceProvider();  // ③ 목록표로 공장을 짓는다
}

비유하자면 이렇습니다.

  • ServiceCollection = 주문서. "이런 부품들이 필요합니다"라고 적는 종이.
  • BuildServiceProvider() = 공장 준공. 주문서를 보고 실제로 물건을 찍어낼 수 있는 공장을 세우는 것.
  • ServiceProvider = 완성된 공장. 이제 여기다 "MainWindow 하나 주세요" 하면 나옵니다.

여기서 중요한 건 순서입니다. 공장을 지은 뒤에는 주문서를 고쳐도 소용없습니다. 등록은 반드시 BuildServiceProvider() 이전에 끝나야 합니다.

RegisterTypes — 여기가 여러분이 채울 자리

지금은 비어 있지만, 실제 프로젝트에서는 이렇게 채웁니다.

private void RegisterTypes(IServiceCollection services)
{
    // 인터페이스 ↔ 구현체 연결
    services.AddSingleton<IFileService, FileService>();
    services.AddSingleton<IDialogService, DialogService>();

    // 뷰모델
    services.AddSingleton<MainViewModel>();
    services.AddTransient<FileItemViewModel>();

    // 윈도우도 등록 대상입니다
    services.AddSingleton<MainWindow>();
}

생명주기 3종 세트도 짚고 넘어가겠습니다.

메서드 의미 언제 쓰나

AddSingleton 앱 전체에서 딱 하나만 설정, 로거, 공용 서비스, 메인 윈도우
AddTransient 요청할 때마다 새로 목록의 각 항목 뷰모델, 매번 새로 띄우는 대화상자
AddScoped 스코프 안에서 하나 웹에서 주로 쓰고, 데스크톱에선 드묾

💡 : 매개변수가 ServiceCollection이 아니라 IServiceCollection(인터페이스)인 걸 눈여겨보세요. "너는 등록만 할 수 있어, 공장을 짓거나 다른 짓은 못 해"라고 권한을 좁혀준 겁니다. 작지만 좋은 습관입니다.

CreateShell — 3강에서 뺏어온 주도권을 행사하는 곳

Shell(셸)은 "앱의 껍데기", 즉 메인 윈도우를 뜻하는 이름입니다. 지금은 빈 창을 만들고 있지만,

private Window CreateShell()
{
    return new Window();   // 임시
}

실제로는 이렇게 바꿉니다.

private Window CreateShell()
{
    return _provider.GetRequiredService<MainWindow>();
}

이 한 줄이 이 글 전체의 결론입니다. 3강에서 WPF에게서 뺏어온 "윈도우 생성 권한"을, 여기서 DI 컨테이너에게 넘겨주고 있는 겁니다. 이제 MainWindow는 생성자에 뭘 요구하든 자유롭습니다.

참고로 RegisterTypes, CreateShell이라는 이름은 제가 지은 게 아닙니다. Prism이라는 유명한 WPF 프레임워크가 쓰는 이름을 그대로 가져온 겁니다. 즉 이 구조는 "Prism을 통째로 쓰긴 부담스러우니, 부트스트랩 부분만 손으로 흉내 내자" 에 가깝습니다. 나중에 Prism으로 갈아탈 때도 자연스럽게 이어집니다.

OnStartup — StartupUri의 대체품

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);        // ① 부모 먼저 (관례)
    Window shell = CreateShell();  // ② 컨테이너에서 윈도우 꺼내기
    MainWindow = shell;       // ③ 대표 창으로 지정
    shell.Show();             // ④ 화면에 띄우기
}

③번이 뭔지 궁금하실 텐데요. Application.MainWindow 프로퍼티는 "이 앱의 대표 창이 누구냐" 를 알려주는 값입니다. 이걸 지정해두면 Owner를 설정하지 않은 대화상자가 알아서 이 창을 기준으로 뜨고, 기본 종료 정책(ShutdownMode.OnMainWindowClose)도 제대로 동작합니다. StartupUri를 쓸 땐 WPF가 알아서 채워주던 값인데, 이제 우리가 채워야 합니다.

OnExit — 뒷정리

protected override void OnExit(ExitEventArgs e)
{
    _provider.Dispose();
    base.OnExit(e);
}

컨테이너가 만들어낸 객체 중에는 IDisposable을 구현한 것들이 있습니다. DB 커넥션, 파일 핸들, 파일 감시자(FileSystemWatcher), 네트워크 소켓 같은 것들이죠.

_provider.Dispose()를 호출하면 컨테이너가 자기가 만든 애들을 역순으로 돌면서 전부 정리해 줍니다. 이 한 줄이 없으면 앱을 껐는데도 파일이 잠겨 있거나, 커넥션이 남아 있는 문제가 생길 수 있습니다.

OnStartup은 base를 먼저 부르고, OnExit은 base를 나중에 부르는 것도 눈여겨보세요. 열 때는 안쪽부터, 닫을 때는 바깥쪽부터 — 자연스러운 순서입니다.


6. AssemblyInfo.cs는 왜 지우나요?

이건 앞의 이야기와 무관한, 별개의 정리 작업입니다.

옛날 .NET Framework 시절에는 어셈블리 버전, 회사명, 저작권 같은 정보를 이 파일에 적었습니다.

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyCompany("MyCompany")]

그런데 요즘 SDK 스타일 프로젝트(<Project Sdk="Microsoft.NET.Sdk">)에서는 이 정보를 csproj에서 관리하고, 빌드할 때 자동으로 생성해 줍니다.

<PropertyGroup>
  <Version>1.0.0</Version>
  <Company>MyCompany</Company>
</PropertyGroup>

그래서 AssemblyInfo.cs를 그대로 두면 같은 속성이 두 번 정의됐다며 컴파일 에러가 나거나, 운 좋게 안 나더라도 버전 정보를 관리하는 곳이 두 군데로 쪼개져서 헷갈립니다.

WPF 템플릿의 AssemblyInfo.cs에 실제로 남아 있는 내용은 보통 이거 하나입니다.

[assembly: ThemeInfo(ResourceDictionaryLocation.None,
                     ResourceDictionaryLocation.SourceAssembly)]

이건 커스텀 컨트롤 라이브러리가 Themes/Generic.xaml을 어디서 찾을지 알려주는 속성입니다. 라이브러리를 만드는 프로젝트에는 필요하지만, 실행 파일(애플리케이션) 프로젝트에는 사실상 의미가 없습니다. 그래서 지워도 무방합니다.


7. 잃어버린 것도 있습니다 — 리소스 딕셔너리

여기서 솔직하게 짚고 넘어가야 할 부분이 있습니다.

App.xaml을 지우면 InitializeComponent()도 함께 사라집니다. 그 말은 App.xaml에 있던 전역 리소스도 사라진다는 뜻입니다.

<!-- 이런 걸 넣을 곳이 없어졌습니다 -->
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Themes/Dark.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

전역 스타일이나 테마가 필요하다면 코드로 넣어주면 됩니다.

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    Resources.MergedDictionaries.Add(new ResourceDictionary
    {
        Source = new Uri("pack://application:,,,/Themes/Dark.xaml", UriKind.Absolute)
    });

    Window shell = CreateShell();
    MainWindow = shell;
    shell.Show();
}

XAML 한 줄이면 될 걸 코드로 쓰는 게 번거롭긴 합니다. 이건 트레이드오프예요. 테마 전환 기능이 있는 앱이라면 오히려 코드로 관리하는 게 편하고, 정적인 스타일만 쓴다면 App.xaml을 남겨두는 절충안도 충분히 합리적입니다.

💡 절충안: App.xaml은 남기되 StartupUri 속성만 지우는 방법도 있습니다. 그럼 리소스는 XAML로 관리하면서 DI도 쓸 수 있습니다. 다만 이 경우 Main이 자동 생성되므로 Starter.cs는 필요 없습니다.


8. 실전 — 실제로 굴러가는 예제

여태 배운 걸 전부 합쳐 보겠습니다.

1) 서비스와 뷰모델 준비

public interface IFileService
{
    IEnumerable<string> GetFiles(string path);
}

public class FileService : IFileService
{
    public IEnumerable<string> GetFiles(string path)
        => Directory.GetFiles(path);
}

public class MainViewModel
{
    private readonly IFileService _fileService;

    // 컨테이너가 알아서 넣어줍니다
    public MainViewModel(IFileService fileService)
    {
        _fileService = fileService;
    }
}

2) 윈도우도 주입받게 만들기

public partial class MainWindow : Window
{
    public MainWindow(MainViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;   // 바인딩 연결
    }
}

3) 등록하고 꺼내 쓰기

private void RegisterTypes(IServiceCollection services)
{
    services.AddSingleton<IFileService, FileService>();
    services.AddSingleton<MainViewModel>();
    services.AddSingleton<MainWindow>();
}

private Window CreateShell()
{
    return _provider.GetRequiredService<MainWindow>();
}

끝입니다. GetRequiredService<MainWindow>() 한 번 호출하면 컨테이너가 FileService → MainViewModel → MainWindow 순서로 알아서 조립해서 돌려줍니다. new를 한 번도 안 썼다는 걸 보세요.


9강. 실무에서 챙기면 좋은 것들

기본 골격은 완성됐습니다. 여기서 몇 가지만 더 얹으면 훨씬 단단해집니다.

① 종료 코드를 살려두기

[STAThread]
private static int Main(string[] args)
{
    return new App().Run();
}

원래 코드는 _ = new App().Run();으로 반환값을 버리고 있는데요, Run()은 Shutdown(exitCode)로 전달된 종료 코드를 돌려줍니다. 배치 스크립트나 CI에서 "이 앱이 정상 종료했나?"를 판별해야 한다면 살려두는 게 좋습니다. 그럴 일이 없다면 지금 코드도 문제없습니다.

② 컨테이너 검증 켜기

_provider = services.BuildServiceProvider(new ServiceProviderOptions
{
    ValidateOnBuild = true,
    ValidateScopes  = true
});

등록을 깜빡한 타입이 있으면 앱 시작하자마자 알려줍니다. 이걸 안 켜면 한참 쓰다가 특정 화면을 열었을 때야 터지는데, 디버깅이 훨씬 괴로워집니다.

③ 전역 예외 처리 붙이기

지금 구조에서는 UI 스레드에서 예외가 하나 터지면 앱이 그대로 죽습니다. 최소한 로그는 남기고 죽어야죠.

protected override void OnStartup(StartupEventArgs e)
{
    DispatcherUnhandledException += (s, args) =>
    {
        MessageBox.Show($"오류가 발생했습니다:\n{args.Exception.Message}");
        args.Handled = true;   // 앱을 죽이지 않고 계속 진행
    };

    base.OnStartup(e);
    // ...
}

④ csproj 확인하기

App.xaml이 없어도 이 두 줄은 반드시 있어야 합니다.

<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>

혹시 진입점 후보가 여러 개라 컴파일러가 헷갈려한다면 명시해 주세요.

<StartupObject>WpfExplorer.Starter</StartupObject>

⑤ 컨테이너를 여기저기서 만지지 않기

가장 흔히 하는 실수입니다.

// ❌ 이러지 마세요
public class SomeViewModel
{
    public void DoWork()
    {
        var service = App.Provider.GetService<IFileService>();
    }
}

이걸 서비스 로케이터 안티패턴이라고 부릅니다. 겉보기엔 편한데, 이 클래스가 뭘 필요로 하는지 생성자만 봐서는 알 수 없게 되고 테스트도 어려워집니다.

원칙은 이겁니다.

컨테이너를 직접 만지는 곳은 CreateShell() 딱 한 군데. 그 아래로는 전부 생성자 주입으로 흘려보낸다.

대화상자처럼 동적으로 만들어야 하는 게 있다면 Func<T>나 팩토리 인터페이스를 주입받으세요.

services.AddTransient<SettingsDialog>();
services.AddSingleton<Func<SettingsDialog>>(
    sp => () => sp.GetRequiredService<SettingsDialog>());

마무리 — 한 장으로 정리

지운 것 그게 하던 일 대체품

App.xaml Main 자동 생성 + StartupUri로 창 띄우기 Starter.cs의 Main + OnStartup
MainWindow.xaml 매개변수 없는 생성자로 만들어지는 창 컨테이너가 조립해 주는 창
AssemblyInfo.cs 어셈블리 메타데이터 csproj의 <Version> 등

흐름은 이렇게 흘러갑니다.

OS가 프로세스 시작
  → Starter.Main()          [STAThread 선언]
  → new App()               [부품 등록 → 컨테이너 생성]
  → app.Run()               [메시지 루프 시작]
  → OnStartup()             [컨테이너에서 창 꺼내서 Show]
  → ... 사용자가 앱을 사용 ...
  → 창 닫힘
  → OnExit()                [컨테이너 Dispose]
  → Run() 반환, 프로세스 종료

처음엔 "파일 3개 지우고 2개 만드는 게 뭐 대단한 일이냐" 싶지만, 실은 WPF가 자동으로 해주던 일을 전부 눈에 보이는 곳으로 끄집어낸 것입니다. 코드가 몇 줄 늘어난 대신, 앱이 어떻게 시작하고 끝나는지가 전부 내 손 안에 들어옵니다.

작은 앱이라면 굳이 이렇게까지 안 해도 됩니다. 하지만 화면이 열 개를 넘어가고, 서비스가 얽히기 시작하고, 테스트를 쓰기 시작하는 순간부터는 이 구조가 확실히 값을 합니다.

다음 단계로 관심이 있다면 — MVVM 툴킷(CommunityToolkit.Mvvm)으로 뷰모델 보일러플레이트 줄이기, IHostBuilder로 설정 파일과 로깅까지 통합하기, 그리고 이 모든 걸 이미 만들어둔 Prism 도입하기 순으로 넘어가시면 자연스럽습니다.



출처: 이재웅 & 구양 WPF Inside Out <-링크를 누르면 사이트로 이동합니다.

devBong
@devBong :: 괴발자 데브봉

고양이와 볼링을 좋아하는 개발자 데브봉입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차