본문 바로가기
C.W.K.
Stream
Lesson 01 of 06 · published

맥 앱을 묶는 네 가지 방식

~15 min · appkit-swiftui, architecture, swiftui, appkit, menu-bar

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"SwiftUI냐 AppKit이냐는 틀린 질문이야. 창은 누가 갖고, 잎사귀는 누가 그리냐가 맞는 질문이지."

가족 Mac 앱은 넷을 다 써

튜토리얼은 SwiftUI랑 AppKit을 양자택일처럼 보여줘. 실제 맥 앱은 둘을 섞어 써. 그러니 쓸모 있는 질문은 경계를 어디에 긋느냐야. 가족 Mac 앱들은 네 가지 형태로 나뉘어.

  1. AppKit만. 두 패널짜리 파일 작업대와 터미널은 위에서 아래까지 AppKit이야. NSTableView, NSCollectionView, NSSplitViewController, 직접 만든 탭 줄, 세밀한 키보드 처리까지 전부. SwiftUI가 필요한 곳은 없었고, 오히려 SwiftUI로는 손댈 수 없는 제어가 필요한 곳이 여럿이었어.
  2. AppKit 뼈대에 SwiftUI 말단 뷰. AppKit 텍스트 시스템 위에 만든 글쓰기 편집기와 메뉴 막대 유틸리티 몇 개가 여기 속해. 앱 뼈대는 AppKit으로 두고, SwiftUI가 잘하는 부분(설정 창, 폼, 작은 패널)만 NSHostingViewNSHostingController로 품어.
  3. SwiftUI 앱에 AppKit 호스트. 비디오 플레이어, 창작 보조 앱, 에이전트 클라이언트는 SwiftUI App이랑 창을 쓰고, 무거운 네이티브 뷰 하나씩(비디오 레이어, 웹 뷰)을 NSViewRepresentable로 감싸.
  4. 순수 SwiftUI. 작은 런처 하나는 MenuBarExtra, Window, 얇은 앱 델리게이트 어댑터가 전부야. 그 정도로 작은 화면이면 그냥 SwiftUI 쪽이 코드가 적어.

고르는 법

  • 글자가 곧 제품(편집기, 터미널)이면 AppKit이 뷰를 가져. 입력기, 조합 중인 글자, 되돌리기에서 SwiftUI 추상화가 제일 먼저 새거든.
  • 창 모양을 직접 만들어야 하면(색 입힌 제목 막대, 직접 만든 탭 줄, 크기를 기억하는 중첩 분할 뷰) AppKit이 창을 가져.
  • 대부분 평범한 창에 특별한 뷰 하나면 SwiftUI 앱으로 가고, 그 뷰만 AppKit으로 품어.
  • 폼, 설정, 단순한 행 목록은 어디에 품기든 SwiftUI로 짜.
  • 팝오버랑 설정 창 있는 메뉴 막대 유틸리티라면 MenuBarExtra로 순수 SwiftUI를 쓰면 돼. 아이템, 전역 단축키, 패널을 정밀하게 다뤄야 하면 AppKit NSStatusItem이 맞아.

이 형태들은 단계별로 올라가는 사다리가 아니야. 가족에서 제일 복잡한 앱은 AppKit만 쓰고 제일 작은 앱은 순수 SwiftUI인데, 둘 다 옳은 선택이야. 문제는 형태를 정하지 않고 이리저리 떠다닐 때 생겨. 아무도 모르는 사이에 SwiftUI 뷰가 AppKit 객체의 수명을 쥐게 되거나, AppKit 앱 여기저기에 SwiftUI 섬이 제멋대로 자라서 저마다 상태 사본을 들고 있게 되지. 이 트랙 나머지는 그 이음매 얘기야.

Code

형태 4: 창 하나 달린 순수 SwiftUI 메뉴 막대 런처·swift
import SwiftUI
import AppKit

@main
struct SparkLauncher: App {
    @NSApplicationDelegateAdaptor(LauncherDelegate.self) private var delegate
    @State private var model = LauncherModel()

    var body: some Scene {
        MenuBarExtra("Spark", systemImage: "flame") {
            LauncherMenu()
        }
        Window("Capture", id: "composer") {
            ComposerView(model: model)
        }
    }
}

struct LauncherMenu: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("New Capture…") {
            openWindow(id: "composer")
            NSApp.activate()                         // an accessory app never comes forward on its own
        }
        .keyboardShortcut("n")
        Divider()
        Button("Quit Spark") { NSApplication.shared.terminate(nil) }
    }
}

@MainActor
@Observable
final class LauncherModel {
    var draft = ""
}

struct ComposerView: View {
    @Bindable var model: LauncherModel
    var body: some View {
        TextField("What happened?", text: $model.draft, axis: .vertical).padding()
    }
}

final class LauncherDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        NSApp.setActivationPolicy(.accessory)      // no Dock icon
    }
}

External links

Exercise

Spark의 Mac 앱에 네 형태 중 어느 게 맞는지 적어. 그리고 앱이 주로 뭘 보여주는지, 창 모양을 직접 만들거나 글자 편집이 필요한지, 메뉴 막대에 상주하는지를 따져서 세 문장으로 근거를 대봐. 이어서 코드 블록의 순수 SwiftUI 런처를 새 실행 타깃으로 만들어 돌려봐. Dock 아이콘이 안 뜨는지, 메뉴 막대 아이템이 작성 창을 여는지 확인해.
Hint
@Environment(\.openWindow)는 App 구조체에선 못 읽어서 메뉴를 따로 뷰로 뺐어. accessory 앱은 스스로 맨 앞에 오지 않으니까, NSApp.activate()가 없으면 창이 쓰던 앱 뒤에 열려. Dock 아이콘이 뜨면, 실행할 때 활성화 정책이 .accessory로 설정되는지 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.