🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [17편] 상태 관리 2부
앱의 화면 구조 만들기
단일 화면만 있는 앱은 드뭅니다. 대부분의 앱은 목록 → 상세 화면으로 이동하거나, 버튼을 누르면 시트(하단에서 올라오는 화면)가 표시됩니다. 이번 편에서는 SwiftUI에서 이런 화면 전환을 구현하는 방법을 배웁니다.
List — 스크롤 가능한 목록
앞 편에서 List를 간단히 봤는데, 더 자세히 살펴봅니다.
기본 List
struct Note: Identifiable {
let id = UUID()
var title: String
var body: String
var date: Date
}
struct NoteListView: View {
let notes: [Note]
var body: some View {
List(notes) { note in
VStack(alignment: .leading, spacing: 4) {
Text(note.title)
.font(.headline)
Text(note.body)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
Text(note.date, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
}
삭제와 이동
struct EditableList: View {
@State private var items = ["사과", "바나나", "오렌지", "포도", "딸기"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { offsets in
items.remove(atOffsets: offsets)
}
.onMove { from, to in
items.move(fromOffsets: from, toOffset: to)
}
}
.toolbar {
EditButton() // 편집 모드 토글 버튼
}
}
}
EditButton()을 툴바에 넣으면 SwiftUI가 알아서 편집 모드를 관리합니다. 편집 모드에서는 삭제 버튼과 드래그 핸들이 자동으로 나타납니다.
섹션으로 그룹화
struct GroupedList: View {
var body: some View {
List {
Section("과일") {
Text("🍎 사과")
Text("🍊 오렌지")
}
Section("채소") {
Text("🥕 당근")
Text("🥦 브로콜리")
}
}
.listStyle(.insetGrouped) // iOS 스타일
}
}
NavigationStack — 화면 전환
NavigationStack은 스택 기반의 화면 전환을 담당합니다. 화면을 “쌓아 올리고(push)” 뒤로 가기로 “꺼내는(pop)” 방식입니다.
기본 내비게이션
struct BookListView: View {
let books = [
Book(title: "Swift 프로그래밍", author: "Apple", rating: 4),
Book(title: "클린 코드", author: "Robert Martin", rating: 5),
Book(title: "리팩터링", author: "Martin Fowler", rating: 5)
]
var body: some View {
NavigationStack {
List(books) { book in
NavigationLink(destination: BookDetailView(book: book)) {
HStack {
VStack(alignment: .leading) {
Text(book.title).font(.headline)
Text(book.author).font(.subheadline).foregroundColor(.secondary)
}
Spacer()
StarRating(rating: book.rating)
}
}
}
.navigationTitle("내 서재")
}
}
}
struct BookDetailView: View {
let book: Book
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(book.title)
.font(.largeTitle)
.bold()
Text("저자: \(book.author)")
.font(.title3)
StarRating(rating: book.rating)
Spacer()
}
.padding()
.navigationTitle(book.title)
.navigationBarTitleDisplayMode(.inline)
}
}
NavigationLink를 탭하면 destination에 지정한 뷰로 이동합니다. 뒤로 가기 버튼은 자동으로 생성됩니다.
프로그래밍 방식 내비게이션 (navigationDestination)
데이터 타입으로 목적지를 정의하는 더 현대적인 방식입니다.
struct BookListView: View {
let books: [Book]
@State private var path = NavigationPath() // 내비게이션 스택 상태
var body: some View {
NavigationStack(path: $path) {
List(books) { book in
Button(book.title) {
path.append(book) // 코드로 직접 이동
}
}
.navigationTitle("서재")
.navigationDestination(for: Book.self) { book in
BookDetailView(book: book)
}
}
}
}
navigationDestination을 사용하면 버튼 클릭, 알림 수신 등 다양한 트리거로 화면을 전환할 수 있습니다.
시트(Sheet)와 모달 표시
시트는 현재 화면 위에 새 화면을 표시하는 방식입니다. iOS에서 하단에서 올라오는 카드 형태가 대표적입니다.
기본 시트
struct MainView: View {
@State private var showAddNote = false
@State private var notes: [String] = []
var body: some View {
NavigationStack {
List(notes, id: \.self) { note in
Text(note)
}
.navigationTitle("메모")
.toolbar {
Button {
showAddNote = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddNote) {
AddNoteView { newNote in
notes.append(newNote)
}
}
}
}
struct AddNoteView: View {
@State private var text = ""
@Environment(\.dismiss) private var dismiss // 시트 닫기
var onSave: (String) -> Void
var body: some View {
NavigationStack {
TextEditor(text: $text)
.padding()
.navigationTitle("새 메모")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") {
onSave(text)
dismiss()
}
.disabled(text.isEmpty)
}
}
}
}
}
@Environment(\.dismiss)는 현재 화면을 닫는 액션입니다. 시트 안에서 호출하면 시트가 닫히고, 풀스크린 커버 안에서 호출하면 커버가 닫힙니다.
다른 표시 방식
struct ModalExamples: View {
@State private var showSheet = false
@State private var showFullScreen = false
@State private var showAlert = false
@State private var showConfirmation = false
var body: some View {
VStack(spacing: 16) {
// 하단 시트
Button("시트 열기") { showSheet = true }
.sheet(isPresented: $showSheet) {
Text("하단 시트").presentationDetents([.medium, .large])
}
// 풀스크린
Button("전체 화면") { showFullScreen = true }
.fullScreenCover(isPresented: $showFullScreen) {
VStack {
Text("전체 화면 커버")
Button("닫기") { showFullScreen = false }
}
}
// 경고창
Button("알림") { showAlert = true }
.alert("확인", isPresented: $showAlert) {
Button("확인") { }
Button("취소", role: .cancel) { }
} message: {
Text("계속 진행하시겠습니까?")
}
// 확인 다이얼로그
Button("삭제") { showConfirmation = true }
.confirmationDialog("정말 삭제하시겠습니까?",
isPresented: $showConfirmation) {
Button("삭제", role: .destructive) { }
Button("취소", role: .cancel) { }
}
}
}
}
.presentationDetents로 시트가 절반(.medium)이나 전체(.large)로 열리도록 제어할 수 있습니다.
탭 바 (TabView)
여러 주요 섹션을 탭으로 전환하는 앱 구조입니다.
struct AppTabView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
Label("홈", systemImage: "house")
}
.tag(0)
SearchView()
.tabItem {
Label("검색", systemImage: "magnifyingglass")
}
.tag(1)
ProfileView()
.tabItem {
Label("프로필", systemImage: "person")
}
.tag(2)
}
}
}
실전 조합 — 노트 앱
@Observable
class NoteStore {
var notes: [Note] = [
Note(title: "첫 번째 메모", body: "SwiftUI를 배우고 있습니다.", date: .now),
Note(title: "두 번째 메모", body: "내비게이션이 생각보다 쉽네요.", date: .now)
]
func add(title: String, body: String) {
notes.append(Note(title: title, body: body, date: .now))
}
func delete(at offsets: IndexSet) {
notes.remove(atOffsets: offsets)
}
}
struct NoteApp: View {
@State private var store = NoteStore()
@State private var showAdd = false
var body: some View {
NavigationStack {
List {
ForEach(store.notes) { note in
NavigationLink(destination: NoteDetailView(note: note)) {
VStack(alignment: .leading) {
Text(note.title).font(.headline)
Text(note.body).font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
.onDelete(perform: store.delete)
}
.navigationTitle("메모장")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button { showAdd = true } label: {
Image(systemName: "square.and.pencil")
}
}
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
}
.sheet(isPresented: $showAdd) {
AddNoteSheet { title, body in
store.add(title: title, body: body)
}
}
.environment(store)
}
}
이 구조가 실제 앱의 기본 뼈대입니다: NavigationStack + List + NavigationLink로 목록/상세를 구성하고, sheet로 입력 화면을 표시합니다.
핵심 요약
- List: 스크롤 가능한 목록.
onDelete,onMove,Section으로 기능 확장 - NavigationStack: 스택 기반 화면 전환.
NavigationLink로 이동,navigationDestination으로 타입 기반 라우팅 - sheet: 하단 카드 형태의 화면.
isPresentedBool 바인딩으로 제어 - fullScreenCover: 전체 화면 모달
- @Environment(\.dismiss): 현재 화면(시트/커버) 닫기
- TabView: 탭 바 기반 최상위 화면 구조
다음 편에서는 커스텀 뷰 컴포넌트를 만들고 애니메이션을 추가해 더 풍부한 UI를 만드는 방법을 배웁니다.
🤖 Generated with Claude Code