스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI API 디자인 기술: 단계적 공개
SwiftUI의 핵심 원리 중 하나인 단계적 공개에 대해 알아보고 이것이 Apple의 API 디자인에 어떤 영향을 미치는지 배워보겠습니다. 단계적 공개의 사용 방법을 보여드리고, 이를 통해 빠른 반복 작업과 탐색을 지원하는 방법에 대해 논의하며, 나만의 코드에서 이점을 활용할 수 있도록 도와드립니다.
리소스
-
다운로드
안녕하세요, 제 이름은 Sam입니다 SwiftUI 팀의 엔지니어죠 SwiftUI를 설계할 때 명확하게 정의된 원칙을 바탕으로 결정을 내리려고 노력했는데요 오늘은 그 원칙 중 하나인 ‘단계적 공개’를 다루겠습니다 SwiftUI 팀에서는 많은 시간을 고민하며 새로운 API를 만들지만 여러분이 몰랐을 수도 있는 사실은 여러분이 다시 사용할 수 있는 요소나 추상화 작업을 하는 순간 여러분도 API 디자이너라는 사실이죠 이 세션은 우리 팀의 설계 과정이 어떻게 진행되는지 보여 드리고 단계적 공개에 관해 배운 걸 공유하겠습니다 여러분이 다음에 재사용 요소나 추상화 작업 시 새로운 도구를 활용할 수 있는 거죠
먼저 단계적 공개가 어떤 의미인지부터 이야기하겠습니다 API 설계에서만 사용하는 용어가 아니죠 일반적인 macOS UI에서도 볼 수 있는 개념입니다 저장하기 창이죠 처음에 저장하기 창이 나타날 때 기본 저장 위치가 설정되어 있습니다 또한, 창의 드롭다운 메뉴에는 주로 사용하는 위치가 있어 선택 가능성이 큰 위치에 쉽게 접근할 수 있죠 원하는 경로를 찾기 위해 파일 시스템을 검색하려고 창을 더 확대하면 포괄적인 UI가 나타납니다 필요할 때마다 단계적으로 더 복잡한 UI가 드러나죠 이런 경험을 API에서도 제공하고자 합니다 멋진 UI 경험을 코드에서 제공하려면 API 사용하는 느낌이 좋아야 합니다
개발자들은 코드를 작성하는 공간인 '선언 사이트'의 입장에서 코드를 바라봅니다 하지만 코드 사용의 느낌을 좋게 하려면 다른 관점에서 봐야 하죠 코드가 실제로 사용되는 곳 또는 '호출 사이트'라고 부르는 곳입니다
API 설계에서는 활용 사례의 복잡도에 따라 호출 사이트의 복잡도가 증가하며 단계적 공개를 적용하죠
이상적인 API는 간단하고 접근성이 좋지만 강력한 활용 사례도 수용해야 합니다
이는 개발자들에게 큰 이점을 주죠 먼저 첫 빌드와 실행에 걸리는 시간을 최소화하여 API를 빠르게 사용할 수 있습니다 또한 코드에 쉽게 익숙해져 API가 활용 사례와 상관없는 개념으로 가득해질 염려가 없죠
마지막으로 피드백 주기가 깔끔해집니다 단계적 공개를 채택하는 API는 각 단계에 만든 걸 보고 한 부분씩 추가할 수 있죠
이러한 이점을 통해 빠른 주기로 앱을 개선하여 개발할 수 있고 처음부터 큰 노력을 투입할 필요가 없습니다
이처럼 단계적 공개는 유용한 지침이지만 그 원칙을 특정 API 설계에 어떻게 적용할까요? SwiftUI 팀은 일반적인 활용 사례부터 고려합니다 기능을 단계적으로 공개하기 위해 단순한 케이스의 형태를 파악해야 하죠
또한 지능적인 기본값을 제공하여 필요할 때만 일반적인 사례를 구체화합니다 다음은 호출 사이트를 최적화하여 호출 사이트의 모든 글자에 목적을 부여하죠 마지막으로 API를 설계하여 가능성을 열거하는 대신 각 조각을 구성합니다 그러면 바로 SwiftUI의 예를 살펴보죠 먼저 일반적 활용 사례를 고려하는 방법입니다 SwiftUI는 레이블에서 이러한 고려를 잘하죠
여러분이 버튼을 만들면 우리는 버튼의 레이블의 만들라고 요구합니다 레이블은 보통 버튼의 목적을 설명하는 텍스트이며 SwiftUI로 쉽게 작성할 수 있죠 하지만 버튼을 더욱 커스텀화하고 싶다면 SwiftUI가 다른 오버로드를 제공하여 레이블 형태의 임의의 뷰로 나타냅니다
이를 통해 간단한 컨트롤에도 복잡한 기능을 넣을 수 있죠 하지만 이 API는 일반적 활용 사례를 고려하며 99%의 경우에는 간단한 버전만 필요합니다
이러한 레이블 패턴은 SwiftUI 전체에 나타나죠 여기서 전체라는 표현은 그냥 하는 말이 아닙니다 일반적인 활용 사례를 고려할 때 전체 프레임워크에서 고려하죠 다음은 지능적인 기본값 제공에 관해 알아보죠 일반적인 활용 사례를 간결하게 만들기 위해 명시적으로 구체화하지 않는 모든 항목의 지능적인 기본값을 제공합니다 SwiftUI 전체에서 가장 많이 사용되는 API에서 발견할 수 있죠 텍스트(Text)입니다 텍스트는 지능적인 기본값을 나타내기 좋은 예죠 이런 코드를 수백 번 작성하시는 동안 따로 지정해야 하는 것들을 생각할 필요도 없으셨을 겁니다
이 코드만 보더라도 SwiftUI가 환경 로케일의 앱 번들 안에 있는 현지화 스트링을 검색하여 텍스트를 현지화하죠 현재 설정한 색상에 자동으로 맞춰서 다크 모드를 바로 지원합니다 또한 유동적 글자 크기의 설정에 따라 글자 크기가 자동으로 커지거나 작아집니다 이런 동작에 관해 이전에도 다루었지만 텍스트의 배경에서는 더 많은 일이 벌어지죠
2개의 텍스트를 위아래로 배치한 경우 텍스트 사이의 간격을 현재 텍스트의 맥락에 맞게 자동으로 조정합니다 모든 동작은 수동으로 지정할 수 있지만 SwiftUI의 지능적인 기본값 덕분에 일반적 케이스와 관련 없는 내용은 호출 사이트에도 나타나지 않죠
텍스트는 API 중에서도 굉장히 미니멀한 예시지만 지능적인 기본값은 모든 호출 사이트에 적용되죠 도구모음을 예로 들겠습니다 이건 버튼이 여러 개 있는 도구모음이죠 각 버튼의 위치를 명시적으로 지정하지 않아도 각 버튼이 플랫폼에 맞게 배치됩니다 macOS에서는 도구모음 왼쪽에 나타나지만 iOS에서는 내비게이션 바에 나타나죠 오른쪽 구석에서 시작합니다 마지막으로 watchOS에서는 첫 번째 항목만이 내비게이션 바 아래 고정돼 있죠 이는 대부분의 케이스에 훌륭하게 적용될 겁니다 하지만 항목을 제어하고 싶다면 항목의 위치를 지정할 수 있는 추가 API를 제공하죠 원하신다면 언제든지 맞춤 설정할 수 있지만 지능적인 기본값이 대개의 사례를 처리합니다
일반적인 활용 사례를 고려하고 지능적인 기본값을 제공하면 멋진 경험을 만들 수 있지만 API가 무겁게 느껴지거나 정제되지 않으면 그 경험을 망칠 수 있죠 그래서 마지막 전략이 필요합니다 호출 사이트 최적화죠 이를 위해 다른 API인 표를 살펴보겠습니다
열이 여러 개인 표는 기능이 풍부한 컨트롤이죠 설정할 게 많고 기능도 많습니다 하지만 표 대부분은 훨씬 간단하고 이런 기능이 필요 없죠 물론 표에서 복잡한 기능도 수행하기를 원합니다 가장 장황한 양식에서는 그런 기능이 있죠 정렬 기능도 제공하고 풍부한 내용의 컬럼들 행 섹션도 제공합니다
하지만 더 일반적인 사례에도 훌륭한 경험을 제공하고 싶죠 그러면 간단한 표에 관해 완벽히 작성한 코드를 보면서 호출 사이트를 어떻게 최적화할지 알아봅시다 먼저 이 예시를 살펴보죠 표는 각 행에 데이터를 생성하는 방법부터 명시합니다
이 표에서는 현재 읽는 책을 열거하고 있죠 각 책에 대한 행도 만들고 있습니다 다음에는 각 행의 데이터를 열에 배치하는 방식을 명시하죠 이 표에서는 제목 열과 작가 열을 만들었습니다
또한 정렬 순서를 지정하여 사용자가 열의 제목을 클릭할 때 정렬이 변경되도록 했죠
마지막으로 정렬 순서가 바뀌었을 때 표를 재정렬하는 코드를 추가했습니다 정보가 많으니까 일단 살펴보면서 단계적 공개를 적용하여 호출 사이트를 최적화해 보죠
가장 일반적인 활용 사례는 행과 관련이 있습니다 대개는 행 영역이 이 예시와 비슷할 거예요 컬렉션에 ForEach 문을 돌려 각 항목에 행을 제공하죠
하지만 개발자가 이 과정을 루핑할 필요가 없습니다 그래서 SwiftUI가 편리하게 이 작업을 배경에서 처리하죠 컬렉션을 표에 직접 통과시켜 ForEach 동작은 배경에서 수행되도록 하여 호출 사이트를 단순화했죠 하지만 더 단순화할 수 있습니다 다른 일반적인 활용 사례가 뭘까요? 사실 대부분은 표에 나타내려는 값이 스트링이면 텍스트를 이용하여 열에 표시합니다 이 케이스에 맞게 호출 사이트를 최적화했죠
값의 키패스가 스트링을 지정할 때 TableColumn과 관련된 뷰를 삭제할 수 있습니다
이것도 많이 단순화했지만 더 최적화할 수 있죠 호출 사이트에 있는 정보 중에서 모든 표에 필요하지 않은 정렬 기능이 있습니다 가장 단순한 활용 사례에서 정렬에는 관심이 없었죠 따라서 정렬도 고려하지 않는 버전의 표를 제공합니다 이게 최종 코드군요 훨씬 간단합니다 이 호출 사이트의 모든 글자가 명확한 목적을 수행하는데 단계별로 중요한 질문 2개를 던졌기 때문이죠 편의를 제공하는 대상에게 일반적인 활용 사례가 뭔지와 언제나 요구되는 핵심적인 정보가 무엇인지입니다 지침이 되는 이 질문들은 호출 사이트를 최적화할 수 있지만 적용에 주의가 필요합니다 API에 주는 영향을 제대로 고려하지 않으면 길을 잃을 수도 있죠 그래서 마지막 전략이 필요합니다 열거 대신 ‘구성’하는 거죠 이를 설명하기 위해 SwiftUI의 레이아웃 시스템인 스택스(stacks)의 설계에 관해 얘기하겠습니다 여기서는 HStack이죠 먼저 HStack에서 가장 핵심 정보가 무엇인지 생각해 봅시다 일단 스택에 어떤 콘텐츠가 들어가는지 알아야 하고 그 안에서 어떻게 정렬할지 알아야 하죠 HStack의 콘텐츠를 지정하는 뷰 빌더가 있으니 정렬에 초점을 맞춥시다 지침이 되는 질문을 다시 생각해 보죠 Hstack의 요소를 정렬하는 일반적인 활용 사례가 뭘까요? 때로는 이렇게 나타내고 싶을 겁니다 왼쪽 모서리부터 상자를 하나씩 보여 주는 거죠
중앙에 배치하는 것도 일반적인 케이스입니다 요소를 오른쪽에 맞춰 정렬할 수도 있죠
VStack에는 이미 정렬 관련 API가 있는데 비슷한 이넘을 사용하여 스택 요소에 사용하고 싶을 수도 있습니다 이는 우리가 언급한 모든 케이스를 지원하죠 HStack의 정렬을 지정하여 왼쪽으로 정렬하거나 오른쪽 또는 중앙에 정렬할 수 있습니다 만약 요소의 간격을 일정하게 하고 싶거나 요소 사이에만 간격을 두거나 마지막 요소 전에만 간격을 두고 싶으면 어떨까요? 항목이 너무 많아집니다 이런 구조를 유지하기도 힘들죠 원하는 모든 케이스에 관한 이넘을 추가해야 하는데 유용한 케이스를 생각해내지 못할 수도 있죠 일반적인 케이스만 열거하고 편의성을 제공하지 않는다면 API를 구성할 수 있는 조각으로 나누어 해결책을 만들어 보십시오 열거 대신 구성하세요
스택의 경우 SwiftUI는 Spacer를 제공하여 우리가 열거했던 모든 정렬 방식은 물론 훨씬 많은 정렬을 구성할 수 있죠 현재 API에 반영했습니다
단계적 공개를 통해 최고의 경험을 설계하는 건 호출 사이트를 최소화하는 것뿐만 아니라 호출 사이트에서 모든 케이스를 다룰 수 있도록 신중하게 고려했고 구성으로 해결책을 찾았죠
직접 코드를 작성할 때 여러분이 만드는 요소에 관해 신중하게 고려한다면 적용이 훨씬 쉽겠죠 정리하면, 일반적 활용 사례를 고려하는 것에서 시작합니다 단계적 공개를 적용하면 여러분이 작성하는 코드가 일반적 활용 사례에서 시간을 아껴 주죠 지능적인 기본값으로 일반적 케이스에 관한 세부 사항을 고려하지 않아도 됩니다 여러분이 만드는 호출 사이트를 최적화하여 반복 작업을 빠르게 수행하죠 마지막으로 구성을 통해 모든 활용 사례를 처리하는 유연한 API를 만들 수 있습니다
여러분도 API 설계자이므로 이러한 내용을 작성하는 코드에 적용할 수 있죠 다른 사람을 위해 설계할 수도 있고 개인 용도일 수도 있습니다 시청해 주셔서 감사합니다
-
-
1:59 - Declaration Site Example
struct BookView: View { let pageNumber: Int let book: Book init(book: Book, pageNumber: Int) { self.book = book self.pageNumber = pageNumber } var body: some View { ... } }
-
2:13 - Call Site Example
VStack { BookView(book: favoriteBook, page: 1) BookView(book: savedBook, page: 234) }
-
4:18 - Button Label
Button("Next Page") { currentPage += 1 }
-
4:36 - Button label expanded
Button { currentPage += 1 } label: { Text("Next Page") }
-
4:43 - Button label advanced case
Button { currentPage += 1 } label: { HStack { Text("Next Page") NextPagePreview() } }
-
4:56 - Button label common case
Button("Next Page") { currentPage += 1 }
-
5:30 - Text example
Text("Hello WWDC22!")
-
6:12 - Stacks of Text
VStack { Text("Hello WWDC22!") Text("Call to Code.") }
-
6:46 - Toolbar
.toolbar { Button { addItem() } label: { Label("Add", systemImage: "plus") } Button { sort() } label: { Label("Sort", systemImage: "arrow.up.arrow.down") } Button { openShareSheet() }: label: { Label("Share", systemImage: "square.and.arrow.up") } }
-
7:20 - Toolbar with explicit placement
.toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { addItem() } label: { Label("Add", systemImage: "plus") } Button { sort() } label: { Label("Sort", systemImage: "arrow.up.arrow.down") } Button { openShareSheet() }: label: { Label("Share", systemImage: "square.and.arrow.up") } } }
-
8:09 - Advanced use case table
@State var sortOrder = [KeyPathComparator(\Book.title)] var body: some View { Table(sortOrder: $sortOrder) { TableColumn("Title", value: \Book.title) { book in Text(book.title).bold() } TableColumn("Author", value: \Book.author) { book in Text(book.author).italic() } } rows: { Section("Favorites") { ForEach(favorites) { book in TableRow(book) } } Section("Currently Reading") { ForEach(currentlyReading) { book in TableRow(book) } } } .onChange(of: sortOrder) { newValue in favorites.sort(using: newValue) currentlyReading.sort(using: newValue) } }
-
8:41 - Simpler table use case
@State var sortOrder = [KeyPathComparator(\Book.title)] var body: some View { Table(sortOrder: $sortOrder) { TableColumn("Title", value: \Book.title) { book in Text(book.title) } TableColumn("Author", value: \Book.author) { book in Text(book.author) } } rows: { ForEach(currentlyReading) { book in TableRow(book) } } .onChange(of: sortOrder) { newValue in currentlyReading.sort(using: newValue) } }
-
9:58 - Table collection convenience
@State var sortOrder = [KeyPathComparator(\Book.title)] var body: some View { Table(currentlyReading, sortOrder: $sortOrder) { TableColumn("Title", value: \.title) { book in Text(book.title) } TableColumn("Author", value: \.author) { book in Text(book.author) } } .onChange(of: sortOrder) { newValue in currentlyReading.sort(using: newValue) } }
-
10:23 - Table string key path convenience
@State var sortOrder = [KeyPathComparator(\Book.title)] var body: some View { Table(currentlyReading, sortOrder: $sortOrder) { TableColumn("Title", value: \.title) TableColumn("Author", value: \.author) } .onChange(of: sortOrder) { newValue in currentlyReading.sort(using: newValue) } }
-
10:51 - Table without sorting
var body: some View { Table(currentlyReading) { TableColumn("Title", value: \.title) TableColumn("Author", value: \.author) } }
-
13:37 - Stack example: leading
struct StackExample: View { var body: some View { HStack { // leading Box().tint(.red) Box().tint(.green) Box().tint(.blue) } } }
-
13:40 - Stack example: centered
struct StackExample: View { var body: some View { HStack { // centered Spacer() Box().tint(.red) Box().tint(.green) Box().tint(.blue) Spacer() } } }
-
13:42 - Stack example: evenly spaced
struct StackExample: View { var body: some View { HStack { // evenly spaced Spacer() Box().tint(.red) Spacer() Box().tint(.green) Spacer() Box().tint(.blue) Spacer() } } }
-
13:43 - Stack example: space only between elements
struct StackExample: View { var body: some View { HStack { // space only between elements Box().tint(.red) Spacer() Box().tint(.green) Spacer() Box().tint(.blue) } } }
-
13:46 - Stack example: space only before last element
struct StackExample: View { var body: some View { HStack { // space only before last element Box().tint(.red) Box().tint(.green) Spacer() Box().tint(.blue) } } }
-
13:47 - Stack example: space only after first element
struct StackExample: View { var body: some View { HStack { // space only after first element Box().tint(.red) Spacer() Box().tint(.green) Box().tint(.blue) } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.