Flutter 튜토리얼 61편: 데스크톱 앱 개발

개요#

Flutter로 Windows, macOS, Linux 세 가지 데스크톱 플랫폼용 앱을 개발하는 방법을 알아봅니다. 각 플랫폼의 특성, 네이티브 통합, 빌드 및 배포 준비까지 포괄적으로 다룹니다.

graph TB subgraph "Flutter 데스크톱 플랫폼" A[Flutter] --> B[Windows] A --> C[macOS] A --> D[Linux] end subgraph "네이티브 통합" B --> E[Win32 API / COM] C --> F[Cocoa / Swift] D --> G[GTK / C] end

1. 데스크톱 개발 환경 설정#

Why: 왜 데스크톱 앱을 개발하나요?#

데스크톱 앱은 모바일 앱보다 더 많은 시스템 리소스와 기능에 접근할 수 있습니다. 파일 시스템 접근, 멀티 윈도우, 시스템 트레이 등 데스크톱 특화 기능을 활용할 수 있습니다.

What: 플랫폼별 요구사항#

플랫폼개발 환경필수 도구
WindowsWindows 10+Visual Studio 2022, CMake
macOSmacOS 10.14+Xcode, CocoaPods
LinuxUbuntu 18.04+Clang, CMake, GTK 3, pkg-config

How: 환경 설정 방법#

Windows 개발 환경#

Terminal window
# Visual Studio 설치 확인
flutter doctor
# Windows 데스크톱 활성화 (기본 활성화됨)
flutter config --enable-windows-desktop
# 프로젝트에 Windows 플랫폼 추가
flutter create --platforms=windows .

macOS 개발 환경#

Terminal window
# Xcode 설치 확인
xcode-select --install
# macOS 데스크톱 활성화
flutter config --enable-macos-desktop
# 프로젝트에 macOS 플랫폼 추가
flutter create --platforms=macos .

Linux 개발 환경#

Terminal window
# 필수 패키지 설치 (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install clang cmake ninja-build pkg-config \
libgtk-3-dev liblzma-dev libstdc++-12-dev
# Linux 데스크톱 활성화
flutter config --enable-linux-desktop
# 프로젝트에 Linux 플랫폼 추가
flutter create --platforms=linux .

2. Windows 앱 개발#

Why: 왜 Windows 특화 기능이 필요한가요?#

Windows는 전 세계에서 가장 많이 사용되는 데스크톱 OS입니다. Windows 사용자에게 익숙한 UI와 기능을 제공하면 사용자 경험이 향상됩니다.

What: Windows 통합 기능#

  • Win32 API: 시스템 수준 기능 접근
  • COM 인터페이스: Windows 컴포넌트 활용
  • Fluent Design: Windows 디자인 가이드라인

How: Windows 앱 구현#

dart 통한 네이티브 통합#

import 'dart:ffi';
import 'package:ffi/ffi.dart';
// Win32 API 호출 예제
typedef MessageBoxNative = Int32 Function(
IntPtr hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
Uint32 uType,
);
typedef MessageBoxDart = int Function(
int hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
int uType,
);
void showWindowsMessageBox(String title, String message) {
final user32 = DynamicLibrary.open('user32.dll');
final messageBox = user32
.lookupFunction<MessageBoxNative, MessageBoxDart>('MessageBoxW');
final titlePointer = title.toNativeUtf16();
final messagePointer = message.toNativeUtf16();
messageBox(0, messagePointer, titlePointer, 0);
calloc.free(titlePointer);
calloc.free(messagePointer);
}

win32 패키지 활용#

pubspec.yaml
dependencies:
win32: ^5.0.0
import 'package:win32/win32.dart';
void showNotification() {
// Windows 토스트 알림, 레지스트리 접근 등
// win32 패키지가 제공하는 간편한 API 사용
}

Fluent Design UI 적용#

pubspec.yaml
dependencies:
fluent_ui: ^4.0.0
import 'package:fluent_ui/fluent_ui.dart';
class WindowsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FluentApp(
title: 'Windows App',
theme: FluentThemeData.light(),
darkTheme: FluentThemeData.dark(),
home: NavigationView(
appBar: NavigationAppBar(
title: Text('Windows Style App'),
),
pane: NavigationPane(
selected: 0,
items: [
PaneItem(
icon: Icon(FluentIcons.home),
title: Text('Home'),
body: Center(child: Text('홈 화면')),
),
PaneItem(
icon: Icon(FluentIcons.settings),
title: Text('Settings'),
body: Center(child: Text('설정 화면')),
),
],
),
),
);
}
}

호스트 애플리케이션 커스터마이징#

windows/runner/main.cpp
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); // 창 크기 조정
if (!window.CreateAndShow(L"My App", origin, size)) {
return EXIT_FAILURE;
}

Watch out: Windows 주의사항#

  • Visual C++ 재배포 가능 패키지: 배포 시 함께 포함 필요
  • 앱 아이콘: windows/runner/resources/app_icon.ico 교체
  • 실행 파일명: windows/CMakeLists.txtBINARY_NAME 수정

3. macOS 앱 개발#

Why: 왜 macOS 특화 기능이 필요한가요?#

macOS 사용자는 일관된 Apple 경험을 기대합니다. App Store 배포와 시스템 통합을 위해 macOS 특화 설정이 필요합니다.

What: macOS 통합 기능#

  • Cupertino 위젯: iOS/macOS 스타일 UI
  • App Sandbox1: 앱 보안 격리
  • Entitlements2: 시스템 권한 설정
  • Hardened Runtime3: 보안 강화

How: macOS 앱 구현#

Cupertino 스타일 UI#

import 'package:flutter/cupertino.dart';
class MacOSApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'macOS App',
theme: CupertinoThemeData(
primaryColor: CupertinoColors.systemBlue,
),
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('macOS Style App'),
),
child: SafeArea(
child: Center(
child: CupertinoButton.filled(
child: Text('버튼'),
onPressed: () {},
),
),
),
),
);
}
}

macos_ui 패키지 활용#

pubspec.yaml
dependencies:
macos_ui: ^2.0.0
import 'package:macos_ui/macos_ui.dart';
class MacOSNativeApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MacosApp(
title: 'macOS Native App',
theme: MacosThemeData.light(),
darkTheme: MacosThemeData.dark(),
home: MacosWindow(
sidebar: Sidebar(
minWidth: 200,
builder: (context, scrollController) {
return SidebarItems(
currentIndex: 0,
items: [
SidebarItem(
leading: MacosIcon(CupertinoIcons.home),
label: Text('Home'),
),
SidebarItem(
leading: MacosIcon(CupertinoIcons.settings),
label: Text('Settings'),
),
],
onChanged: (index) {},
);
},
),
child: Center(child: Text('Content')),
),
);
}
}

Entitlements 설정#

<!-- macos/Runner/Release.entitlements -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 앱 샌드박스 활성화 -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- 네트워크 클라이언트 접근 -->
<key>com.apple.security.network.client</key>
<true/>
<!-- 사용자 선택 파일 읽기 -->
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<!-- 카메라 접근 -->
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

macOS Entitlements 설정 가이드#

graph TB subgraph "Debug/Profile 모드" A[DebugProfile.entitlements] --> B[JIT 허용] A --> C[네트워크 서버 허용] end subgraph "Release 모드" D[Release.entitlements] --> E[App Sandbox] D --> F[필요한 권한만] end
Entitlement용도
com.apple.security.network.client외부 네트워크 요청
com.apple.security.network.server들어오는 연결 수신
com.apple.security.files.user-selected.read-only사용자 선택 파일 읽기
com.apple.security.files.user-selected.read-write사용자 선택 파일 읽기/쓰기
com.apple.security.device.camera카메라 접근
com.apple.security.device.microphone마이크 접근

Watch out: macOS 주의사항#

  • 두 개의 Entitlements 파일: Debug용과 Release용을 별도로 관리
  • App Sandbox: App Store 배포 시 필수
  • Hardened Runtime: 공증(Notarization) 시 필수
  • 서명: 배포 시 유효한 개발자 인증서 필요

4. Linux 앱 개발#

Why: 왜 Linux를 지원하나요?#

Linux는 개발자와 기업 환경에서 널리 사용됩니다. 오픈소스 생태계와 잘 어울리며, 서버 환경에서의 GUI 도구 개발에 유용합니다.

What: Linux 통합 기능#

  • dart: C 라이브러리 호출
  • GTK4: Linux 네이티브 UI 프레임워크
  • Canonical 패키지: Ubuntu/Linux 특화 기능

How: Linux 앱 구현#

dart 통한 C 라이브러리 호출#

import 'dart:ffi';
// C 함수 시그니처 정의
typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
typedef DartAdd = int Function(int a, int b);
int callNativeAdd(int a, int b) {
// 공유 라이브러리 로드
final dylib = DynamicLibrary.open('libmylib.so');
// 함수 룩업
final add = dylib.lookupFunction<NativeAdd, DartAdd>('add');
return add(a, b);
}

Canonical 패키지 활용#

pubspec.yaml
dependencies:
# Ubuntu 데스크톱 알림
desktop_notifications: ^0.6.0
# D-Bus 통신
dbus: ^0.7.0
# 네트워크 관리
nm: ^0.5.0
import 'package:desktop_notifications/desktop_notifications.dart';
Future<void> showLinuxNotification() async {
final client = NotificationsClient();
await client.notify(
'Flutter 앱',
body: '알림 내용입니다.',
expireTimeout: Duration(seconds: 5),
);
await client.close();
}

Linux 빌드 출력 구조#

build/linux/x64/release/bundle/
├── my_app # 실행 파일
├── lib/
│ ├── libflutter_linux_gtk.so
│ └── ... # 기타 .so 라이브러리
└── data/
├── flutter_assets/ # Flutter 에셋
└── icudtl.dat # ICU 데이터

Watch out: Linux 주의사항#

  • 라이브러리 의존성: ldd 명령으로 확인
  • GTK 버전: 대상 시스템의 GTK 3 버전 확인
  • 배포 형식: AppImage, Flatpak, Snap 등 선택

5. 플랫폼별 빌드 명령#

빌드 및 실행#

Terminal window
# 개발 모드 실행
flutter run -d windows
flutter run -d macos
flutter run -d linux
# 릴리스 빌드
flutter build windows --release
flutter build macos --release
flutter build linux --release

빌드 결과물 위치#

플랫폼경로
Windowsbuild/windows/x64/runner/Release/
macOSbuild/macos/Build/Products/Release/
Linuxbuild/linux/x64/release/bundle/

6. 공통 데스크톱 기능 구현#

창 관리#

pubspec.yaml
dependencies:
window_manager: ^0.3.0
import 'package:window_manager/window_manager.dart';
Future<void> setupWindow() async {
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(1200, 800),
minimumSize: Size(800, 600),
center: true,
backgroundColor: Colors.transparent,
title: 'My Desktop App',
titleBarStyle: TitleBarStyle.hidden, // 커스텀 타이틀바
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}

시스템 트레이#

pubspec.yaml
dependencies:
system_tray: ^2.0.0
import 'package:system_tray/system_tray.dart';
Future<void> initSystemTray() async {
final systemTray = SystemTray();
await systemTray.initSystemTray(
title: 'My App',
iconPath: 'assets/app_icon.png',
);
final menu = Menu();
await menu.buildFrom([
MenuItemLabel(label: '열기', onClicked: (menuItem) => showWindow()),
MenuSeparator(),
MenuItemLabel(label: '종료', onClicked: (menuItem) => exitApp()),
]);
await systemTray.setContextMenu(menu);
systemTray.registerSystemTrayEventHandler((eventName) {
if (eventName == kSystemTrayEventClick) {
systemTray.popUpContextMenu();
}
});
}

파일 시스템 접근#

import 'package:file_selector/file_selector.dart';
import 'package:path_provider/path_provider.dart';
Future<void> openFile() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: ['jpg', 'png', 'gif'],
);
final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
print('선택된 파일: ${file.path}');
}
}
Future<void> saveFile() async {
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/my_file.txt';
// 파일 저장 로직
}

단축키 처리#

class ShortcutExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
SaveIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyO):
OpenIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyQ):
QuitIntent(),
},
child: Actions(
actions: {
SaveIntent: CallbackAction<SaveIntent>(
onInvoke: (intent) => saveDocument(),
),
OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (intent) => openDocument(),
),
QuitIntent: CallbackAction<QuitIntent>(
onInvoke: (intent) => quitApp(),
),
},
child: Focus(
autofocus: true,
child: MyApp(),
),
),
);
}
}
class SaveIntent extends Intent {}
class OpenIntent extends Intent {}
class QuitIntent extends Intent {}

7. 종합 예제: 크로스 플랫폼 데스크톱 앱#

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 플랫폼별 초기화
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
// 데스크톱 전용 초기화 (window_manager 등)
}
runApp(const DesktopApp());
}
class DesktopApp extends StatelessWidget {
const DesktopApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cross-Platform Desktop App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
String get platformName {
if (Platform.isWindows) return 'Windows';
if (Platform.isMacOS) return 'macOS';
if (Platform.isLinux) return 'Linux';
return 'Unknown';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('데스크톱 앱'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getPlatformIcon(),
size: 100,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 20),
Text(
'$platformName에서 실행 중',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 40),
ElevatedButton.icon(
icon: const Icon(Icons.folder_open),
label: const Text('파일 열기'),
onPressed: () => _openFile(context),
),
],
),
),
);
}
IconData _getPlatformIcon() {
if (Platform.isWindows) return Icons.desktop_windows;
if (Platform.isMacOS) return Icons.desktop_mac;
if (Platform.isLinux) return Icons.computer;
return Icons.device_unknown;
}
void _openFile(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('파일 선택 대화상자를 열었습니다')),
);
}
}

마무리#

이번 튜토리얼에서는 Flutter로 데스크톱 앱을 개발하는 방법을 살펴보았습니다.

핵심 요약#

플랫폼핵심 패키지주요 설정
Windowswin32, fluent_uiVisual Studio, CMakeLists.txt
macOSmacos_uiEntitlements, App Sandbox
Linuxdesktop_notificationsGTK 의존성, .so 라이브러리

다음 단계#

  • 62편에서는 배포 준비와 코드 난독화를 다룹니다.
  • 각 플랫폼별 디자인 가이드라인을 학습해보세요.
  • window_manager, system_tray 등 데스크톱 특화 패키지를 활용해보세요.

참고 자료#

Footnotes#

  1. App Sandbox: macOS의 보안 기능으로, 앱이 접근할 수 있는 시스템 리소스를 제한합니다.

  2. Entitlements: 앱이 특정 시스템 기능에 접근할 수 있도록 허용하는 권한 설정입니다.

  3. Hardened Runtime: macOS 앱의 보안을 강화하는 기능으로, App Store 외부 배포 시 필수입니다.

  4. GTK (GIMP Toolkit): Linux에서 가장 널리 사용되는 GUI 툴킷으로, GNOME 데스크톱의 기반입니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 61편: 데스크톱 앱 개발
https://moodturnpost.net/posts/flutter/flutter-platform-desktop/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차