SwiftUI만으로 macOS 앱을 만들며 겪은 한계와, 진정으로 Mac다운 앱 경험을 구현하기 어려운 이유를 살펴봅니다.
최근 저는 작년 말 iOS App Store에 처음 출시했던 앱인 Shopie의 macOS 버전을 출시했습니다. Shopie는 위시리스트를 만들고 제품의 가격, 재고 여부, 기타 세부 사항이 바뀔 때 알림을 보내줌으로써 관심 있는 제품을 추적할 수 있게 도와주는 앱입니다.
다른 제 앱들과는 달리, 보통 AppKit(또는 UIKit)과 SwiftUI를 섞어 쓰는 편이지만 Shopie는 전적으로 SwiftUI로 만들어졌습니다. iOS, iPadOS, 그리고 이제 macOS까지 코드 재사용을 최대화하기 위해서 그렇게 유지하고 싶었습니다. 이 글에서는 특히 목표가 플랫폼에 진짜로 자연스럽게 느껴지는 앱을 만드는 것일 때, 2026년의 Mac에서 SwiftUI가 어디까지 가능한지를 살펴봅니다. 이 글은 macOS에서의 SwiftUI를 빠짐없이 검토하려는 의도는 아닙니다. 그저 비교적 작은 앱인 Shopie를 포팅하면서, 그리고 그것을 100% SwiftUI로 유지하면서 제가 마주친 요령들과 문제들을 모아둔 것입니다.
요약부터 말하자면: 아직 거기까지는 못 갔습니다.
"Mac-assed app"이라는 용어는 Collin Donnell이 만들었고, Brent Simmons와 John Gruber가 널리 알렸습니다. 이 말은 단지 네이티브인 앱이 아니라, 시스템의 컨트롤과 관례를 받아들이고 운영체제의 기능들과 흠잡을 데 없이 통합되는 앱을 가리킵니다.
저는 Secrets를 Mac-assed 앱이라고 생각하고, 그것을 자랑스럽게 여깁니다. 이 앱은 네이티브 컨트롤을 사용하면서도 아름답게 보입니다. 메뉴 막대를 적극적으로 활용하고, 키보드 단축키가 풍부하며, 여러 윈도우를 지원하고, 툴팁과 호버 상태를 갖추고, Password AutoFill, AppleScript(다른 앱 제어용), Safari 앱 확장, sudden termination 같은 시스템 기술도 받아들입니다.
오랫동안 Mac을 사용한 사람이라면, 앱이 이런 조건들을 충족할 때 그냥 느껴집니다. 하지만 Electron 기반 앱의 인기, 그리고 심지어 많은 자사 앱들이 제시하는 기준 때문에, 새로운 사용자에게는 이것을 이해하기가 훨씬 더 어려울 수 있습니다.
Shopie를 macOS로 포팅하는 동안 저는 다양한 문제를 만났습니다. “이건 더 쉬워야 하는데”부터 “이건 그냥 불가능하다”까지 폭이 넓었습니다.
Mac에서 선택은 미묘합니다. 항목은 비활성 윈도우에서 선택된 상태일 수도 있고, 선택되어 있지만 더 이상 포커스를 가진 뷰 안에 있지 않을 수도 있으며, 아예 선택되지 않았더라도 현재 컨텍스트 메뉴의 대상일 수 있습니다. SwiftUI는 이 중 일부는 잘 처리하고, 일부는 어색하게 처리하며, 일부는 전혀 처리하지 못합니다.
현재 HIG는 비활성 윈도우가 “가라앉아 보이고 주 윈도우 및 키 윈도우보다 시각적으로 더 멀리 있는 것처럼 보여야 한다”고 말합니다. 오래 Mac을 써온 사용자라면 이것이 대개 더 구체적인 의미라는 것을 압니다. 이전 버전의 HIG는 이것을 더 명시적으로 설명했습니다. “색이 있는 것은 키 윈도우의 컨트롤뿐이다.”

Finder에서의 활성 윈도우와 비활성 윈도우
이것을 무시하는 것은 대개 앱이 Electron으로 만들어졌다는 첫 신호입니다. 지금 이 글을 쓰고 있는 Visual Studio Code는 이 관례를 따르지 않습니다.
이 부분은 사실 SwiftUI에서도 괜찮습니다. AppKit에서와 마찬가지로 List와 Button 같은 많은 시스템 컨트롤은 이를 자동으로 처리하고, 커스텀 컨트롤도 \.appearsActive↗︎를 확인함으로써 같은 방식으로 대응할 수 있습니다.
그다음은 항목이 여전히 선택되어 있지만 그 뷰가 더 이상 포커스를 갖지 않는 경우입니다.

Mail에서 강조가 약해진 선택
이것이 중요한 이유는 포커스가 사용자에게 UI의 어느 부분이 키보드 입력에 반응할지를 알려주기 때문입니다. 위 스크린샷에서는 이메일이 선택되어 있지만 그것을 담고 있는 리스트가 포커스를 갖고 있지 않기 때문에, 화살표 키를 눌러도 그 선택은 이동하지 않습니다.
AppKit에는 이에 대한 기본 해답이 있습니다. NSTableRowView는 isEmphasized↗︎를 제공하며, 이를 통해 포커스가 다른 곳으로 이동했을 때 선택 표시를 조정할 수 있습니다.
SwiftUI에서 List를 쓰는 대신 ScrollView와 LazyVStack으로 직접 리스트를 만들고 있다면, 스크롤 뷰의 포커스 상태를 추적하고 그것을 environment를 통해 아래로 전달함으로써 같은 동작을 구현할 수 있습니다.
ScrollView {
LazyVStack {
// content
}
}
.focusable(true)
.focused($isScrollViewFocused)
.environment(\.isEmphasized, isScrollViewFocused)
그러면 각 행은 \.isEmphasized와 \.appearsActive를 모두 읽어서 그에 맞게 선택 스타일을 조정할 수 있습니다.
불가능한 경우는 컨텍스트 메뉴입니다. 제대로 된 Mac-assed 앱이라면, 컨텍스트 메뉴를 열었을 때 그 메뉴가 적용되는 항목 주위에 포커스 링이 나타나야 합니다. 그 항목이 선택되어 있지 않더라도 말입니다.

macOS Reminders에서의 컨텍스트 메뉴 대상과 선택
위 스크린샷에서 메뉴는 여전히 “Reminders”가 선택되어 있음에도 “Shopping” 리스트에 적용되며, UI는 그 차이를 분명하게 보여줍니다.

macOS Stocks에서의 컨텍스트 메뉴 대상과 선택
예를 들어 Stocks에서는 현재 선택된 “MSFT” 주식이 아니라 “AAPL”에 메뉴가 적용되지만, 인터페이스는 그 사실을 전달하지 않습니다.
Notes 앱은 반대로 한 걸음 더 나아갑니다. 선택되지 않은 노트를 오른쪽 클릭하면 즉시 선택이 바뀝니다. 그건 정말 Mac-assed한 동작이 전혀 아닙니다 😪.
Reminders, Notes, Stocks는 모두 macOS의 SwiftUI 앱이지만, 각각 다르게 동작합니다. Reminders만 이것을 제대로 처리하는 이유는 NSTableView로부터 그 동작을 물려받는 List를 사용하고 있기 때문입니다.
하지만 List 바깥으로 나가면 막히게 됩니다. 5년이 훨씬 지난 지금도 SwiftUI는 컨텍스트 메뉴가 열려 있는지를 알 수 있는 방법을 전혀 제공하지 않습니다. 그리고 그것을 알 수 없다면, UI를 그에 맞게 조정할 수도 없습니다.
제 추측으로는 iOS에서는 이것이 거의 중요하지 않기 때문에 틈새로 빠져버린 것 같습니다. iOS에서는 컨텍스트 메뉴가 나타날 때 시스템이 관련 요소를 자동으로 elevates 하기 때문입니다.

iOS에서의 컨텍스트 메뉴
하지만 macOS에서는 이 빠진 부분이 아주 두드러져 보입니다. 또한 요즘 Apple 내부에서 Mac이 얼마나 중요하게 여겨지는지를 보여주는 신호처럼 느껴지기도 합니다.
계속 가기 전에 SwiftUI의 List를 짚고 넘어갈 가치가 있습니다. 눈치챘을지 모르지만, 위에서 말한 동작 대부분을 List는 거의 공짜로 제공합니다.
문제는 List를 커스터마이즈하기가 엄청나게 어렵다는 점입니다. .listRowBackground()↗︎로 선택 색상만 바꿔도 선택이 사라지는 애니메이션이 깨질 수 있고, 내장된 컨텍스트 메뉴 포커스 링은 전혀 커스터마이즈할 방법이 없습니다.
하지만 꼭 이럴 필요는 없습니다. UITableViewCell, UICollectionViewCell, NSTableRowView에 익숙하다면, List가 왜 \.isHighlighted, \.isSelected, \.isEmphasized 같은 값을 각 행으로 그냥 내려주지 않는지 의아할 수도 있습니다. 그렇게만 해도 훨씬 더 유용해질 것입니다.
드래그 앤 드롭은 Mac 경험의 핵심입니다. 이것은 플랫폼을 직접적이고 촉각적으로 느끼게 해주는 상호작용 중 하나입니다. 파일을 앱으로 끌어 넣고, 리스트의 순서를 바꾸고, 탭을 새 윈도우로 떨구고, 이미지를 텍스트 필드에 드래그합니다. 그래서 SwiftUI가 이 부분에서 여전히 불안정하게 느껴진다는 점은 조금 불편합니다.
실제로 SwiftUI는 이미 세 번의 드래그 앤 드롭 시대를 거쳤습니다. 처음에는 NSItemProvider를 중심으로 구성된 API인 onDrag(_:)↗︎와 onDrop(...)↗︎로 시작했습니다.
그다음 iOS 16과 macOS 13에서 Apple은 NSItemProvider 대신 Transferable을 도입했고, 새로운 draggable(_:)↗︎ 및 dropDestination(for:action:isTargeted:)↗︎ API도 함께 내놓았습니다.
마지막으로 iOS 26과 macOS 26에서는 세 번째 반복이 도입되었습니다. DropSession을 받는 새로운 dropDestination(for:isEnabled:action:)↗︎ 오버로드와, 여러 항목 드래그를 위한 새로운 컨테이너 기반 드래그 API가 추가된 것입니다.
하지만 _세 가지 모두_의 문제는 드롭 대상이 아닌 이상 드래그 세션을 들여다볼 수 없다는 점입니다. UI 요소를 드래그하기 시작하면, 드래그가 진행되는 동안 그것을 희미하게 만들거나 심지어 인터페이스에서 아예 숨기고 싶을 수도 있습니다. iOS에서는 두 동작 모두의 예를 볼 수 있습니다.

iOS의 Reminders에서 리스트를 드래그하면 드래그 중에는 그것이 사라진다
하지만 SwiftUI에서는 이것을 올바르게 구현하는 것이 불가능합니다. .onDrag()를 써서 뷰를 희미하게 만들고 싶을 수도 있지만, 사용자가 항목을 내 윈도우 바깥에 드롭하면 그 사실을 알 방법이 없어서 항목이 계속 희미한 상태로 남아버릴 수 있습니다.
반면 AppKit의 NSDraggingSource↗︎는 시작부터 필요한 모든 정보를 제공합니다. SwiftUI가 왜 아직도 그러지 못하는지는 저로서는 이해할 수 없습니다.
얼마나 키보드에 의존하는지를 보면 Mac 고급 사용자와 일반 사용자를 구분할 수 있는 경우가 많습니다. 그들은 단지 ⌘C와 ⌘V만 쓰지 않습니다. 화살표 키로 리스트를 탐색하고, 패널 사이를 이동하고, 마우스를 건드리지 않고 메뉴에서 명령을 실행하며, 전반적으로 앱이 그 속도를 따라오기를 기대합니다.
SwiftUI도 이것을 지원할 수는 있지만, 여전히 필요 이상으로 번거롭게 느껴집니다. 좋은 예가 화살표 키입니다. macOS에서는 .onMoveCommand↗︎를 써야 합니다. 하지만 iPad 앱들도 화살표 키가 달린 하드웨어 키보드와 함께 일상적으로 사용되는데도, 이것은 여전히 iOS에서는 사용할 수 없습니다 🤷.
그래서 개념적으로는 완전히 같은 사용자 상호작용인데도 macOS용 코드 경로 하나와 iPadOS용 코드 경로 하나를 따로 써야 합니다. 이런 플랫폼 분리는 SwiftUI가 약속했던 통합 UI 프레임워크라기보다 덜 통합된 무언가처럼 느껴지게 만듭니다.
TextField가 포커스를 가지면 상황은 더 나빠집니다. 그 시점이 되면 키보드 이벤트를 신나게 먹어버리고, 개발자가 개입할 여지를 거의 주지 않습니다. 좋은 예가 검색입니다. Spotlight에서는 검색 필드에 계속 입력하면서 동시에 위아래 화살표 키로 결과를 이동할 수 있습니다. 이건 완전히 표준적인 Mac 상호작용이며, 저는 10년 전 Secrets의 첫 출시 때부터 이것을 넣어두었습니다. 하지만 안타깝게도, 현재 순수 SwiftUI로는 불가능합니다.
다시 말해 문제는 SwiftUI에서 키보드 지원이 불가능하다는 것이 아닙니다. 단순한 경우를 처리하기엔 딱 충분한 만큼만 제공하고, Mac 앱들이 수십 년 동안 해오던 수준을 맞추려는 순간 프레임워크가 발목을 잡는다는 데 있습니다.
툴바 역시 SwiftUI가 iPhone과 iPad에서는 훨씬 더 편안해 보이지만 Mac에서는 그렇지 않은 영역입니다. macOS에서는 툴바 레이아웃이 중요합니다. 사용자는 어떤 동작이 어디에 있는지, 어떤 항목이 사이드바에 속하고 어떤 항목이 세부 보기 쪽에 속하는지를 몸으로 익힙니다.
이 점은 특히 3단 분할 보기에서 더 어색해집니다. SwiftUI는 .primaryAction, .secondaryAction, .navigation 같은 placement를 사용해 툴바 아이템을 의미적으로 설명하라고 요구합니다. 이론상으로는 좋아 보이지만, 실제로는 그 placement가 플랫폼마다 다른 의미를 가지며, 심지어 macOS 안에서도 항목이 실제로 어디에 놓일지 예측하기 어려울 수 있습니다.
더 나쁜 점은 툴바가 뷰 계층 전체에 흩어진 .toolbar modifier들을 모아서 사실상 자동 조립된다는 것입니다. 단순한 화면에서는 편리하지만, 정밀함이 필요해지는 순간 꽤 답답하게 느껴질 수 있습니다. 하나의 일관된 Mac 툴바를 설계하는 대신, SwiftUI가 내 계층 구조를 어떻게 해석하는지와 협상하면서 최종 배치가 내가 의도한 것과 맞기를 바라는 상황이 되기 쉽습니다.
다시 말하지만 문제는 SwiftUI가 툴바를 불가능하게 만든다는 것이 아닙니다. Mac 앱을 의도적으로 잘 설계된 것처럼 느끼게 만들 때 중요한 바로 그 부분들을 프레임워크가 추상화해버린다는 데 있습니다.
한때 Mac 앱들은 거리낌 없이 Mac다웠습니다. Panic, Omni, Cultured Code, Bare Bones, Sofa. iPhone SDK가 나오기 직전 몇 년이 아마도 Mac-assed함의 정점이었을 것입니다. 그러다 Apple의 무게중심은 iPhone 쪽으로 옮겨갔습니다.
이제 Mac에는 Electron, Catalyst, 그리고 iPadOS 앱들이 있습니다. 그리고 Apple의 SwiftUI 앱들조차도 한때 Mac 소프트웨어를 훌륭하게 느끼게 만들었던 바로 그 동작들을 종종 깎아내고 있습니다.
지난 Apple Design Awards를 돌아보면, 2018년의 Agenda가 아마도 마지막으로 진정한 Mac-assed 수상작이었을지도 모릅니다. 그것은 많은 것을 시사합니다.
여기서 Apple은 실책을 했습니다. AppKit은 시대를 앞서갔고 UIKit은 AppKit의 더 다듬어진 버전이었습니다. 둘을 통합하는 진지한 크로스플랫폼 프레임워크는 SwiftUI보다 훨씬 이전에 나왔어야 했습니다. 하지만 Apple은 AppKit을 화석처럼 굳어가게 내버려둔 뒤, 그 문제를 한 번에 뛰어넘으려 했습니다.
그 결과는 어디에나 보입니다. SwiftUI는 생산적이고 현대적이며 종종 즐겁기까지 합니다. 정말 좋은 Mac 앱을 만들려고 하기 전까지는 말입니다. 그러다 갑자기, Mac이 20년 전에 이미 해결한 문제들 때문에 프레임워크와 씨름하게 됩니다.