[Swift 입문] 20편 — macOS 앱 구조와 App 프로토콜

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [19편] 커스텀 뷰와 애니메이션

iOS와 macOS 개발의 차이

SwiftUI를 배웠으니 이제 macOS 앱을 만드는 방법을 알아봅니다. SwiftUI 코드 대부분은 iOS와 macOS에서 공유되지만, macOS에는 iOS에 없는 개념들이 있습니다.

  • 윈도우 관리: iPhone은 화면 하나지만, Mac은 여러 윈도우를 동시에 열 수 있습니다.
  • 메뉴바: Mac 상단에 항상 표시되는 앱 메뉴 영역
  • Dock: 실행 중인 앱 아이콘 모음
  • 메뉴바 앱: Dock 아이콘 없이 메뉴바 아이콘만 갖는 앱

이번 편에서는 macOS SwiftUI 앱의 기본 구조를 이해합니다.


App 프로토콜 — 앱의 진입점

SwiftUI 앱은 App 프로토콜을 채택한 구조체가 진입점입니다.

import SwiftUI

@main  // 이 구조체가 앱의 시작점임을 나타냄
struct MyMacApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@main은 Swift 5.3+에서 도입된 어트리뷰트로, 컴파일러에게 “여기서 앱 실행을 시작해라”고 알려줍니다. 이전에는 main.swift 파일이 필요했지만, 이제는 이 어트리뷰트 하나로 대체됩니다.


Scene — 앱 UI의 최상위 단위

App.bodyView가 아니라 Scene을 반환합니다. Scene은 앱의 UI 단위를 정의합니다.

WindowGroup — 일반 윈도우 앱

@main
struct NoteApp: App {
    var body: some Scene {
        WindowGroup {
            NoteListView()
        }
    }
}

WindowGroup은 사용자가 여러 윈도우를 열 수 있는 일반적인 Document-based 앱 스타일입니다. macOS에서는 Cmd+N으로 새 윈도우를 열 수 있습니다.

Window — 단일 윈도우

@main
struct UtilityApp: App {
    var body: some Scene {
        Window("설정", id: "settings") {
            SettingsView()
        }
        .defaultSize(width: 400, height: 300)
        .windowResizability(.contentSize)
    }
}

Window는 앱에 하나만 존재하는 단일 윈도우입니다. 유틸리티 앱이나 설정 창에 적합합니다.

MenuBarExtra — 메뉴바 앱

@main
struct MenuBarApp: App {
    var body: some Scene {
        MenuBarExtra("내 앱", systemImage: "star.fill") {
            MenuContent()
        }
    }
}

Dock 아이콘 없이 메뉴바 아이콘으로만 동작하는 앱입니다. 다음 편에서 자세히 다룹니다.

Settings — 환경설정 창

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        Settings {
            AppSettingsView()
        }
    }
}

Settings Scene을 추가하면 자동으로 앱 메뉴의 “환경설정…” 항목(Cmd+,)이 활성화됩니다.


AppDelegate — 앱 생명주기 이벤트

앱이 시작되거나 종료될 때, 다른 앱에서 파일을 열 때 등의 이벤트를 처리하려면 AppDelegate가 필요합니다.

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        // 앱이 완전히 시작된 후 실행
        print("앱 시작됨")
    }

    func applicationWillTerminate(_ notification: Notification) {
        // 앱 종료 직전
        print("앱 종료 예정")
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        // 마지막 윈도우가 닫힐 때 앱도 종료할지 여부
        return true  // macOS 기본 앱 동작
        // false: 윈도우 없어도 앱 유지 (메뉴바 앱 등)
    }
}

// SwiftUI App에 AppDelegate 연결
@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@NSApplicationDelegateAdaptor는 SwiftUI 앱에서 기존 AppKit AppDelegate를 사용할 수 있게 연결해주는 어트리뷰트입니다.


앱 생명주기(Life Cycle)

// Scene 생명주기를 SwiftUI에서 감지
struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("안녕하세요")
            .onChange(of: scenePhase) { _, newPhase in
                switch newPhase {
                case .active:
                    print("앱/윈도우가 활성화됨")
                case .inactive:
                    print("앱/윈도우가 비활성화됨")
                case .background:
                    print("앱이 백그라운드로 감")
                @unknown default:
                    break
                }
            }
    }
}

scenePhase를 통해 앱이 현재 활성 상태인지, 비활성인지, 백그라운드로 전환됐는지 감지할 수 있습니다.


macOS 전용 수정자

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowStyle(.hiddenTitleBar)          // 타이틀바 숨기기
        .defaultSize(width: 800, height: 600)   // 기본 윈도우 크기
        .windowResizability(.contentSize)       // 콘텐츠 크기에 맞게 조정
        .commands {
            // 앱 메뉴 커스터마이징
            CommandGroup(replacing: .newItem) { }  // "새 항목" 메뉴 제거
            CommandMenu("도구") {
                Button("설정 열기") { }
                    .keyboardShortcut(",", modifiers: .command)
            }
        }
    }
}

commands로 메뉴바 커스터마이징

struct AppCommands: Commands {
    @Binding var document: MyDocument

    var body: some Commands {
        CommandMenu("편집") {
            Button("모두 선택") {
                document.selectAll()
            }
            .keyboardShortcut("a", modifiers: .command)

            Divider()

            Button("찾기...") {
                document.showFind()
            }
            .keyboardShortcut("f", modifiers: .command)
        }
    }
}

// App에 적용
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            AppCommands(document: $myDocument)
        }
    }
}

LSUIElement — Dock 아이콘 없는 앱

메뉴바 앱처럼 Dock에 아이콘을 표시하지 않으려면 Info.plist에 설정을 추가합니다.

<!-- Info.plist -->
<key>LSUIElement</key>
<true/>

또는 XcodeGen을 사용한다면 project.yml에서:

targets:
  MyApp:
    info:
      properties:
        LSUIElement: true

LSUIElement = true이면 앱이 실행되어도 Dock에 아이콘이 나타나지 않고, Cmd+Tab 앱 전환기에도 표시되지 않습니다. 메뉴바 유틸리티 앱의 표준 설정입니다.


iOS와 macOS의 주요 차이점 정리

항목 iOS macOS
앱 진입점 App 프로토콜 App 프로토콜
Scene 종류 WindowGroup WindowGroup, Window, MenuBarExtra, Settings
Delegate UIApplicationDelegate NSApplicationDelegate
멀티 윈도우 제한적 기본 지원
메뉴바 없음 항상 존재
Dock 없음 기본 표시 (숨김 가능)

핵심 요약

  • @main + App 프로토콜: 앱의 진입점 선언
  • Scene: 앱 UI의 최상위 단위 — WindowGroup, Window, MenuBarExtra, Settings
  • @NSApplicationDelegateAdaptor: SwiftUI 앱에서 AppDelegate 연결
  • scenePhase: 앱 활성/비활성/백그라운드 상태 감지
  • .commands: 앱 메뉴바 항목 추가/수정
  • LSUIElement: Dock 아이콘 없는 메뉴바 전용 앱 설정

다음 편에서는 MenuBarExtraNSStatusItem으로 메뉴바 앱을 만드는 방법을 자세히 다룹니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다