🟣 present를 부르던 습관을 버려야 한다
UIKit에서 얼럿을 띄우려면 present(alert, animated: true)를 호출했다. 화면 전환도, 액션 시트도 전부 "지금 띄워라"라고 명령하는 함수 호출이었다. SwiftUI로 넘어와 처음 얼럿을 띄우려 할 때 이 함수를 찾다가 막힌다. 그런 함수가 없기 때문이다.
선언형에서는 화면에 무언가가 떠 있는지 여부도 하나의 상태다. 얼럿을 띄우고 싶으면 얼럿을 띄우라고 명령하는 게 아니라, "얼럿이 떠 있어야 한다"는 상태를 참으로 만든다. 그러면 그 상태를 감시하던 뷰가 알아서 얼럿을 띄운다. 닫는 것도 마찬가지다. 상태를 거짓으로 되돌리면 사라진다. 앞 장의 데이터 흐름 원칙이 화면 제어에 그대로 적용되는 셈이다.
🟣 조건부 뷰: 화면 자체를 상태로 가른다
가장 기본이 되는 상태 기반 제어는 조건부 뷰다. body 안에서 if로 상태를 갈라, 상황마다 다른 뷰를 돌려준다. 로딩 중이면 스피너를, 끝나면 목록을 보여주는 화면을 생각해 보자. UIKit이라면 스피너를 startAnimating 하고 나중에 isHidden으로 감췄겠지만, SwiftUI에서는 상태에 따라 아예 다른 화면을 그린다.
struct FeedView: View {
@State private var isLoading = true
@State private var posts: [Post] = []
var body: some View {
if isLoading {
ProgressView("불러오는 중")
} else {
List(posts) { post in
Text(post.title)
}
}
}
}isLoading이 참인 동안에는 ProgressView만 존재하고 List는 화면에 없다. 값이 거짓으로 바뀌면 body가 다시 계산되면서 ProgressView가 사라지고 List가 나타난다. 스피너를 감추는 코드를 따로 쓰지 않는다. 화면에 무엇이 있고 없는지를 상태 하나가 결정한다.
경우가 셋 이상이면 if 나열보다 switch가 읽기 좋다. 로딩 상태를 열거형으로 두면 화면 분기가 한눈에 들어온다. 이 패턴은 마지막 장에서 다시 다룬다.
🟣 Alert: 떠 있음을 상태로 묶는다
얼럿은 두 가지 방식으로 붙인다. 하나는 참과 거짓만 다루는 isPresented 방식이다.
struct DeleteButton: View {
@State private var showConfirm = false
var body: some View {
Button("삭제") {
showConfirm = true // 함수 호출이 아니라 상태 변경
}
.alert("정말 삭제할까요?", isPresented: $showConfirm) {
Button("삭제", role: .destructive) { /* 삭제 처리 */ }
Button("취소", role: .cancel) { }
}
}
}버튼을 눌러 하는 일은 showConfirm = true 하나다. 얼럿을 띄우라고 지시하지 않는다. 사용자가 버튼을 누르거나 바깥을 탭해 얼럿이 닫히면 SwiftUI가 showConfirm을 다시 거짓으로 돌려놓는다. 상태와 화면이 바인딩으로 묶여 양쪽이 자동으로 맞는다.
다른 하나는 데이터에 따라 내용이 달라지는 item 방식이다. 어떤 항목에서 오류가 났는지 함께 보여줘야 한다면, 참·거짓 대신 옵셔널 값을 두고 그 값이 있을 때만 얼럿을 띄운다.
struct DownloadView: View {
@State private var failure: DownloadError? // 값이 있으면 얼럿
var body: some View {
List { /* ... */ }
.alert(item: $failure) { error in
Alert(title: Text(error.message))
}
}
}failure에 값을 넣으면 얼럿이 뜨고, 닫히면 다시 nil이 된다. 오류가 여러 종류일 때 이 방식이 깔끔하다. 액션 시트도 원리는 같다. 요즘은 confirmationDialog로 여러 선택지를 상태에 묶어 띄운다.
🟣 sheet: 화면 띄우기도 같은 문법
모달로 다른 화면을 띄우는 sheet도 얼럿과 똑같은 구조다. isPresented로 참·거짓을 묶거나, item으로 넘길 데이터가 있을 때만 띄운다.
struct ProfileView: View {
@State private var editing = false
var body: some View {
Button("프로필 편집") { editing = true }
.sheet(isPresented: $editing) {
EditProfileView() // 이 화면이 모달로 올라온다
}
}
}얼럿이든 액션 시트든 모달이든 문법이 하나로 수렴한다는 점이 중요하다. 떠 있는 무언가는 전부 상태에 묶고, 그 상태를 참으로 만들어 띄운다. UIKit처럼 종류마다 다른 present 함수를 외울 필요가 없다. 화면 편집 상태를 데이터로 다룬다는 감각 하나면 얼럿, 시트, 다이얼로그가 모두 같은 방식으로 풀린다.
🟣 정리
- 선언형에서는 화면에 무언가가 떠 있는지 여부도 상태다.
present를 부르지 않고 상태를 참으로 만든다. - 조건부 뷰는
body안if·switch로 상황마다 다른 화면을 돌려준다. 감추는 코드를 따로 쓰지 않는다. Alert은isPresented로 참·거짓을 묶거나,item으로 넘길 데이터가 있을 때만 띄운다.- 사용자가 닫으면 SwiftUI가 상태를 되돌린다. 상태와 화면이 바인딩으로 양방향으로 맞는다.
sheet,confirmationDialog도 같은 문법이다. 떠 있는 것은 전부 상태에 묶는다.
면접에서 "UIKit의 present와 SwiftUI의 차이"를 물으면, 명령과 선언의 차이로 답할 수 있다. 명령형은 띄우라고 호출하고 닫으라고 다시 호출하지만, 선언형은 떠 있음을 상태로 표현하고 그 상태만 관리한다는 것이다. 실무에서 화면 전환 로직이 꼬이는 이유는 대개 이 상태를 여러 곳에서 따로 건드리기 때문이다. 마지막 장에서는 이렇게 만든 뷰를 어떻게 잘게 나누고 조립하는지, 실전에서 반복해 쓰는 구성 기법을 모아 본다.