Dependencies: SwiftUI는 무엇이 바뀌었는지 어떻게 알까?
앞선 글에서는 SwiftUI가 View를 구분하는 방식인 Identity와, Identity가 유지되는 기간인 Lifetime을 정리했다. 이번에는 SwiftUI가 View를 업데이트하는 기준인 Dependencies를 살펴본다.
먼저 결론부터 말하면 SwiftUI의 업데이트 과정은 다음과 같다.
Dependency 변경
↓
관련된 View 무효화
↓
body 재평가
↓
새로운 View Value와 이전 Value 비교
↓
실제로 변경된 화면만 업데이트Identity는 같은 View인지 판단하고, Lifetime은 상태를 유지하며, Dependency는 어떤 View를 다시 계산해야 하는지 결정한다. 이 세 가지를 함께 이해하면 단순히 API 사용법을 외우는 것보다 SwiftUI의 동작을 더 잘 예측할 수 있다.
강아지 간식 예제
struct DogView: View {
@Binding var dog: Dog
var treat: Treat
var body: some View {
Button {
dog.reward(treat)
} label: {
PawView()
}
}
}DogView의 body는 dog와 treat를 사용해 만들어진다. 따라서 두 값은 DogView의 Dependencies다.
Dependency가 변경되면 SwiftUI는 해당 View의 body가 더 이상 최신 상태가 아니라고 판단하고, 이후 업데이트 과정에서 body를 다시 평가한다.

버튼을 탭하면 강아지에게 간식을 주는 Action이 실행되고 dog의 상태가 바뀐다. dog가 DogView의 Dependency라면 SwiftUI는 DogView를 무효화하고 body를 다시 계산한다.
SwiftUI는 무효화된 View에서 body를 호출하고 새로운 View Value를 생성한다. 그런 다음 새로운 View Value와 이전 View Value를 비교해 실제 화면에서 필요한 부분만 업데이트한다.
View Value는 body를 계산할 때마다 새로 만들어질 수 있는 짧은 값이다. 반면 View의 Lifetime은 Identity를 따라 더 길게 이어진다. 그래서 body가 다시 계산되더라도 Identity가 유지되면 State와 같은 저장 공간은 계속 유지될 수 있다.
모든 View에는 명시적이든 구조적이든 Identity가 있다. SwiftUI는 이 Identity를 이용해 변경 사항을 올바른 View에 전달하고 UI를 업데이트한다.
Dependency에는 여러 종류가 있다.
@Binding
@Environment
@State
@StateObject
@ObservedObject
@EnvironmentObjectIdentifier Stability
SwiftUI가 변경된 데이터를 올바른 View에 전달하려면 Identifier가 안정적이어야 한다. 2편에서 정리했듯이 View의 Lifetime은 Identity가 유지되는 기간이므로, Identifier가 불안정하면 Lifetime도 끊길 수 있다.
안정적인 Identifier는 State와 애니메이션을 유지하는 데 도움이 되고, View의 저장 공간과 Dependency Graph를 불필요하게 다시 구성하는 상황도 줄인다.
좋아하는 펫 예제
enum Animal { case dog, cat }
struct Pet: Identifiable {
var name: String
var kind: Animal
var id: UUID { UUID() }
}
struct FavoritePets: View {
var pets: [Pet]
var body: some View {
List {
ForEach(pets) { pet in
PetView(pet)
}
}
}
}Pet에는 Identifier가 있지만 문제가 있다. id에 접근할 때마다 새로운 UUID가 만들어지므로, 같은 Pet도 매번 다른 항목처럼 보인다.
Index를 Identifier로 사용하면 어떨까?
struct FavoritePets: View {
var pets: [Pet]
var body: some View {
List {
ForEach(pets.indices, id: \.self) { index in
PetView(pets[index])
}
}
}
}이것도 좋은 해결책은 아니다. Index는 데이터 자체가 아니라 컬렉션 안에서 데이터가 놓인 위치이기 때문이다.
새로운 펫을 가장 앞에 추가하면 기존 펫들의 Index가 모두 밀리고, SwiftUI는 항목이 이동한 것이 아니라 여러 항목의 Identity가 바뀐 것으로 해석할 수 있다.
이런 경우에는 데이터베이스에서 가져온 ID처럼 영속적인 Identifier를 사용해야 한다.
ForEach(pets, id: \.databaseID) { pet in
PetView(pet)
}유일한 Identifier
struct TreatJar: View {
var treats: [Treat]
var body: some View {
ScrollView {
LazyVGrid(...) {
ForEach(treats, id: \.name) { treat in
TreatCell(treat)
}
}
}
}
}간식에는 이름, 이모지, 유통기한, 일련번호가 있다고 하자. 간식을 이름으로 식별하면 같은 이름을 가진 간식이 두 개 이상 있을 때 문제가 생길 수 있다.

간식을 추가해도 화면에 나타나지 않거나, 애니메이션과 업데이트가 이상하게 동작할 수 있다.
ForEach(treats, id: \.serialNumber) { treat in
TreatCell(treat)
}대신 각 간식의 일련번호처럼 안정적이고 유일한 값을 사용하면 모든 간식 데이터를 올바르게 추적할 수 있다.
일반적으로 Identifier는 시간이 지나도 바뀌지 않아야 하고, 같은 시점에 여러 항목이 공유해서도 안 된다. 새로운 Identifier는 SwiftUI에게 새로운 Lifetime을 가진 새로운 항목이 생겼다는 의미가 될 수 있기 때문이다.
Structural Identity
Identifier는 데이터가 제공하는 Explicit Identity만을 의미하지 않는다. View를 작성한 코드의 구조도 Identity를 만든다.

ForEach(treats, id: \.serialNumber) { treat in
TreatCell(treat)
.modifier(ExpirationModifier(date: treat.expiryDate))
}
struct ExpirationModifier: ViewModifier {
var date: Date
func body(content: Content) -> some View {
if date < .now {
content.opacity(0.3)
} else {
content
}
}
}유통기한이 지나면 Cell을 흐리게 만드는 Modifier다. 하지만 이 코드에는 서로 다른 두 개의 content 분기가 존재한다.
조건이 바뀌면 SwiftUI는 같은 View의 속성이 변경되었다고 보기보다, 서로 다른 구조 중 하나가 선택되었다고 해석할 수 있다.
실제 프로젝트에서는 분기가 서로 다른 파일이나 Modifier 안에 흩어져 있을 수 있다. 분기가 Modifier 안에 있다는 사실을 놓치기 쉬운 이유다.
Modifier의 body 역시 View를 반환하므로, 그 안의 조건문도 Structural Identity를 만든다.
Inert Modifier로 구조 유지하기
이 경우에는 두 분기를 하나로 합치고 조건을 opacity Modifier 내부로 옮길 수 있다.
struct ExpirationModifier: ViewModifier {
var date: Date
func body(content: Content) -> some View {
content.opacity(date < .now ? 0.3 : 1.0)
}
}첫 번째 코드는 서로 다른 두 View 구조 중 하나를 선택한다. 두 번째 코드는 하나의 View 구조를 유지한 채 opacity 값만 변경한다.
따라서 지금 표현하려는 것이 새로운 View가 아니라 같은 View의 상태 변화라면, 구조를 갈아끼우기보다 Modifier의 값을 변경하는 편이 SwiftUI의 Identity 모델과 더 잘 맞는다.
이처럼 렌더링 결과에 아무런 영향을 주지 않는 Modifier를 Inert Modifier라고 한다.
opacity(1)
padding(0)
transformEnvironment(...) { }조건 분기는 유용하고 SwiftUI에 존재하는 이유가 분명하다. 다만 필요하지 않은 곳에서 사용하면 다음과 같은 문제가 생길 수 있다.
- 성능 저하
- 예상치 못한 애니메이션
- State 손실
그러므로 조건 분기를 추가할 때는 잠시 멈추고, 지금 표현하려는 것이 정말 서로 다른 View인지, 아니면 같은 View의 다른 상태인지 구분해야 한다.
하나의 View를 표현할 때는 조건 분기보다 Inert Modifier를 사용하는 편이 더 잘 동작하는 경우가 많다.
마무리
SwiftUI의 View 업데이트를 이해하려면 “화면을 다시 그린다”는 표현만으로는 부족하다.
- Identity로 이전 View와 같은 View인지 판단한다.
- 같은 Identity가 유지되는 동안 Lifetime을 이어간다.
- Lifetime에 연결된 State와 저장 공간을 보존한다.
- Dependency가 변경되면 관련된 View를 무효화한다.
- 무효화된 View의
body를 다시 계산한다. - 새로운 View Value와 이전 Value를 비교해 필요한 부분만 업데이트한다.
결국 중요한 질문은 이것이다.
지금 만들고 있는 것은 새로운 View인가, 아니면 같은 View의 다른 상태인가?
이 질문을 기준으로 Identity와 조건 분기를 바라보면, State가 초기화되거나 애니메이션이 이상하게 동작하는 이유도 더 쉽게 추적할 수 있다.