ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI's diffing
    SwiftUI 2025. 9. 27. 06:31

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SwiftUI의 성능에 관련된 핵심 메커니즘인 diffing 알고리즘과 뷰 재생성 최적화에 대해 정리해보려 합니다 🙋🏻


    SwiftUI's diffing

    SwiftUI를 사용하면서 이런 궁금증들이 생기지 않나요?

    • "List를 스크롤할 때 왜 이렇게 버벅거리지? 🤔"
    • "상태가 조금만 바뀌어도 전체 화면이 다시 그려지는 것 같은데?"
    • "ForEach에서 id: .self를 쓰면 안 된다고 하는데 정확히 왜지?"
    • "Airbnb에서 15% 성능 개선했다는 방법임 뭘가?

     

    이 부분에서 답을 찾으려면 SwiftUI의 diffing 알고리즘을 이해해야 합니다.


    Why SwiftUI Diffing Matters More Than Ever?

    📈 Performance Issues

    • 복잡한 리스트에서의 스크롤 히치 현상
    • 불필요한 뷰 재생성으로 인한 배터리 소모
    • 대규모 앱에서의 상태 변경 시 전체 UI 렌더링
    • 애니메이션 끊김과 사용자 경험 저하

    🚀 Change from SwiftUI

    • iOS 17에서 새로운 관찰 시스템(@Observable) 도입
    • 복잡한 뷰 계층구조를 가진 앱들의 증가
    • 실시간 데이터 업데이트가 중요한 앱들의 부상
    • 성능에 민감한 비즈니스 앱들의 SwiftUI 도입 확산

    SwiftUI Diffing 알고리즘의 내부 동작

    기본 메커니즘 이해하기

    SwiftUI는 선언형 UI 프레임워크로, 상태가 변경될 때마다 새로운 뷰 트리를 생성하고 이전 것과 비교합니다.

     

    핵심 동작 과정

    // 상태 변경 시 SwiftUI 내부 동작
    // 1. @State, @ObservableObject 등의 변경 감지
    // 2. 영향받는 뷰의 body 재실행
    // 3. 새로운 뷰 트리 생성
    // 4. 이전 뷰 트리와 diffing 수행
    // 5. 변경된 부분만 실제 UI 업데이트
    

     


    타입 시스템 기반 Diffing

    React와 달리 SwiftUI는 정적 타입 시스템을 활용해 빠르고 명확한 diffing을 수행합니다:

    // SwiftUI의 타입 기반 최적화
    struct UserView: View {
        let name: String
        let age: Int
        
        var body: some View {
            VStack {
                Text(name)    // 타입이 Text로 고정
                Text("\(age)") // 컴파일 타임에 구조 확정
            }
        }
    }
    
    // 같은 타입 구조라면 빠른 비교 가능
    // 다른 타입이라면 즉시 재생성 필요함을 인지
    

     


    리플렉션 기반 프로퍼티 비교

    SwiftUI는 뷰의 저장 프로퍼티들을 리플렉션으로 비교합니다.

    SwiftUI의 내장 비교 알고리즘

    // SwiftUI 내부 diffing 로직 (의사코드)
    func shouldUpdateView<V: View>(_ oldView: V, _ newView: V) -> Bool {
        // 1. Equatable 타입이면 == 연산자 사용
        if V.self is Equatable.Type {
            return oldView != newView
        }
        
        // 2. 값 타입(struct)이면 재귀적으로 프로퍼티 비교
        if V.self is ValueType {
            return compareProperties(oldView, newView)
        }
        
        // 3. 참조 타입(class)이면 참조 동일성 비교
        if V.self is ReferenceType {
            return oldView !== newView
        }
        
        // 4. 클로저는 비교 불가능 - 항상 다르다고 가정
        if containsClosures(V.self) {
            return true
        }
    }

     


    실제 비교 성능 측정하기

    뷰가 얼마나 자주 재생성되는지 시각적으로 확인하는 방법

    // 🎯 뷰 재생성 디버깅용 모디파이어
    extension View {
        func debugViewUpdates(_ label: String = "") -> some View {
            self.background(
                Color.random.opacity(0.3)
                    .animation(.easeInOut(duration: 0.2), value: UUID())
            )
            .onAppear {
                print("🔄 View Updated: \(label)")
            }
        }
    }
    
    // 사용 예시
    struct ProfileView: View {
        @State private var name = "John"
        
        var body: some View {
            VStack {
                Text(name)
                    .debugViewUpdates("ProfileName")
                
                ExpensiveView()
                    .debugViewUpdates("ExpensiveComponent")
            }
        }
    }

     


    ForEach와 View Identity 최적화

    Identity의 중요성

    SwiftUI에서 가장 많은 성능 문제를 일으키는 부분이 ForEach의 잘못된 사용입니다.

    // ❌ 성능을 파괴하는 패턴들
    struct BadListView: View {
        @State private var items = ["A", "B", "C"]
        
        var body: some View {
            List {
                // 1. id: \.self로 중복 데이터 처리
                ForEach(["Apple", "Apple", "Banana"], id: \.self) { item in
                    Text(item) // 애니메이션 버그와 상태 손실 발생
                }
                
                // 2. UUID()를 매번 생성
                ForEach(items, id: \.id(UUID())) { item in
                    Text(item) // 매번 새로운 뷰로 인식, 애니메이션 깨짐
                }
                
                // 3. 배열 순서 변경시 문제
                ForEach(items.shuffled(), id: \.self) { item in
                    ExpensiveRowView(item: item)
                    // 셔플 시 모든 뷰가 재생성됨
                }
            }
        }
    }
    
    // ✅ 올바른 Identity 관리
    struct GoodListView: View {
        @State private var items: [Item] = [
            Item(id: UUID(), name: "Apple"),
            Item(id: UUID(), name: "Banana"),
            Item(id: UUID(), name: "Cherry")
        ]
        
        var body: some View {
            List {
                // Identifiable 프로토콜 활용
                ForEach(items) { item in
                    ItemRowView(item: item)
                }
                .onMove(perform: moveItems)
                .onDelete(perform: deleteItems)
            }
        }
        
        private func moveItems(from: IndexSet, to: Int) {
            items.move(fromOffsets: from, toOffset: to)
            // 안정적인 ID로 인해 부드러운 애니메이션
        }
        
        private func deleteItems(at offsets: IndexSet) {
            items.remove(atOffsets: offsets)
            // 정확한 아이템 식별로 올바른 삭제 애니메이션
        }
    }
    
    struct Item: Identifiable {
        let id = UUID()
        let name: String
    }

     


    인덱스 기반 안전한 처리

    중복 데이터를 다뤄야 할 때의 해결책

    // 중복 데이터 안전 처리법
    struct SafeDuplicateHandling: View {
        let items = ["Apple", "Apple", "Banana", "Apple"]
        
        var body: some View {
            List {
                // 방법 1: 인덱스 활용
                ForEach(items.indices, id: \.self) { index in
                    HStack {
                        Text("\(index + 1).")
                        Text(items[index])
                    }
                }
                
                // 방법 2: 커스텀 래퍼 사용
                ForEach(items.enumerated().map(IndexedItem.init), id: \.index) { item in
                    Text(item.value)
                }
            }
        }
    }
    
    struct IndexedItem: Identifiable {
        let index: Int
        let value: String
        
        var id: Int { index }
    }

     


    EquatableView를 활용한 커스텀 Diffing

    기본 EquatableView 사용법

    SwiftUI의 기본 diffing을 커스텀 로직으로 대체할 수 있습니다.

    // 기본 EquatableView 패턴
    struct ExpensiveListView: View, Equatable {
        let tasks: [Task]
        let categories: [Category]
        let isLoading: Bool
        
        var body: some View {
            List {
                if isLoading {
                    ProgressView()
                } else {
                    ForEach(categories, id: \.id) { category in
                        Section(header: Text(category.name)) {
                            ForEach(filteredTasks(for: category), id: \.id) { task in
                                TaskRowView(task: task)
                            }
                        }
                    }
                }
            }
            .listStyle(.grouped)
        }
        
        private func filteredTasks(for category: Category) -> [Task] {
            tasks.filter { $0.categoryID == category.id }
        }
        
        // 커스텀 비교 로직
        static func == (lhs: ExpensiveListView, rhs: ExpensiveListView) -> Bool {
            // 로딩 상태가 다르면 즉시 업데이트
            if lhs.isLoading != rhs.isLoading {
                return false
            }
            
            // 카테고리 수가 다르면 업데이트
            if lhs.categories.count != rhs.categories.count {
                return false
            }
            
            // 태스크 수만 비교 (내용 변경은 무시)
            return lhs.tasks.count == rhs.tasks.count
        }
    }
    
    // 사용할 때는 .equatable() 모디파이어 추가
    struct TaskListContainer: View {
        @StateObject private var store = TaskStore()
        
        var body: some View {
            ExpensiveListView(
                tasks: store.tasks,
                categories: store.categories,
                isLoading: store.isLoading
            )
            .equatable() // EquatableView로 래핑
            .onAppear {
                store.loadData()
            }
        }
    }

     


    Airbnb의 @Equatable 매크로 패턴

    Airbnb에서 실제 사용하는 최적화 방법

    // @Equatable 매크로 활용 (개념적 구현)
    @Equatable
    struct SearchResultView: View {
        let searchQuery: String
        let results: [SearchResult]
        let filters: SearchFilters
        
        // @State는 자동으로 비교에서 제외
        @State private var selectedItem: SearchResult?
        
        // @ObservedObject도 자동으로 제외
        @ObservedObject var analytics: AnalyticsManager
        
        // 커스텀 제외 프로퍼티
        @EquatableIgnored var onTap: (SearchResult) -> Void
        
        var body: some View {
            LazyVStack {
                ForEach(results) { result in
                    SearchResultCard(result: result)
                        .onTapGesture {
                            onTap(result)
                        }
                }
            }
        }
    }
    
    // 매크로가 생성하는 코드 (예상)
    extension SearchResultView: Equatable {
        nonisolated public static func == (lhs: SearchResultView, rhs: SearchResultView) -> Bool {
            // 최적화된 순서로 비교 (빠른 비교부터)
            lhs.searchQuery == rhs.searchQuery &&
            lhs.filters == rhs.filters &&
            lhs.results.count == rhs.results.count &&
            lhs.results == rhs.results
        }
    }

     


    ObservableObject와의 조합 이슈

    // ❌ ObservableObject와 EquatableView의 함정
    struct ProblematicView: View, Equatable {
        @ObservedObject var model: DataModel
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            // 이 비교는 무시됨!
            // @ObservedObject는 별도 메커니즘으로 업데이트를 트리거
            return true
        }
        
        var body: some View {
            Text(model.title)
                // model 변경시 항상 body가 호출됨
        }
    }
    
    // ✅ 해결 방법: 데이터 분리
    struct OptimizedView: View, Equatable {
        let title: String // 실제 필요한 데이터만 전달
        let subtitle: String
        
        var body: some View {
            VStack {
                Text(title)
                Text(subtitle)
            }
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
        }
    }
    
    struct ContainerView: View {
        @ObservedObject var model: DataModel
        
        var body: some View {
            OptimizedView(
                title: model.title,
                subtitle: model.subtitle
            )
            .equatable()
        }
    }

     


    뷰 복잡도 관리와 성능 최적화

    뷰 복잡도 측정

    Airbnb에서 사용하는 뷰 복잡도 측정 방법

    // 복잡도가 높은 뷰 (안티패턴)
    struct ComplexView: View {
        @State private var data: [Item] = []
        @State private var filter: String = ""
        @State private var sortOption: SortOption = .name
        
        var body: some View {
            VStack {
                // 복잡도 +1: computed property 사용
                searchHeader
                
                // 복잡도 +1: function call
                filteredContent()
                
                // 복잡도 +1: closure in ForEach
                ForEach(processedData) { item in
                    // 복잡도 +1: 조건부 렌더링
                    if item.isVisible {
                        // 복잡도 +1: inline 복잡한 뷰
                        HStack {
                            AsyncImage(url: item.imageURL) { image in
                                image.resizable()
                            } placeholder: {
                                ProgressView()
                            }
                            .frame(width: 50, height: 50)
                            
                            VStack(alignment: .leading) {
                                Text(item.name)
                                Text(item.description)
                            }
                            
                            Spacer()
                            
                            Button("Action") {
                                // 복잡도 +1: closure
                                performAction(item)
                            }
                        }
                    }
                }
            }
        }
        
        // 총 복잡도: 6 (경고 발생!)
    }
    
    // ✅ 복잡도 분산된 최적화 버전
    struct OptimizedComplexView: View {
        @State private var data: [Item] = []
        @State private var filter: String = ""
        @State private var sortOption: SortOption = .name
        
        var body: some View {
            VStack {
                SearchHeaderView(filter: $filter, sortOption: $sortOption)
                
                ItemListView(items: processedData)
            }
        }
        
        private var processedData: [Item] {
            data.filter { filter.isEmpty || $0.name.contains(filter) }
                .sorted { sortOption.compare($0, $1) }
        }
    }
    
    struct SearchHeaderView: View, Equatable {
        @Binding var filter: String
        @Binding var sortOption: SortOption
        
        var body: some View {
            HStack {
                TextField("Search", text: $filter)
                Picker("Sort", selection: $sortOption) {
                    ForEach(SortOption.allCases, id: \.self) { option in
                        Text(option.rawValue)
                    }
                }
            }
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.filter == rhs.filter && lhs.sortOption == rhs.sortOption
        }
    }
    
    struct ItemListView: View, Equatable {
        let items: [Item]
        
        var body: some View {
            LazyVStack {
                ForEach(items) { item in
                    ItemRowView(item: item)
                        .equatable()
                }
            }
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.items.count == rhs.items.count &&
            lhs.items.map(\.id) == rhs.items.map(\.id)
        }
    }

     


    SwiftLint 커스텀 룰

    // 뷰 복잡도 체크 SwiftLint 룰 (의사코드)
    struct ViewComplexityRule: SwiftSyntaxRule {
        static let description = RuleDescription(
            identifier: "view_complexity",
            name: "View Complexity",
            description: "Checks if SwiftUI view body is too complex for efficient diffing",
            kind: .performance
        )
        
        func makeVisitor(file: Syntax) -> ViewComplexityVisitor {
            ViewComplexityVisitor(viewMode: .allFiles)
        }
    }
    
    class ViewComplexityVisitor: SyntaxVisitor {
        private var complexity = 0
        private let maxComplexity = 10
        
        override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
            complexity += 1
            return .visitChildren
        }
        
        override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
            complexity += 1
            return .visitChildren
        }
        
        override func visit(_ node: IfExprSyntax) -> SyntaxVisitorContinueKind {
            complexity += 1
            return .visitChildren
        }
        
        // 복잡도 초과시 경고 생성
    }

     


    또 다른 성능 최적화 패턴

    지연 로딩과 온디맨드 렌더링

    // 무거운 컴포넌트의 지연 로딩
    struct LazyHeavyComponent: View {
        let data: ComplexData
        @State private var isInitialized = false
        
        var body: some View {
            Group {
                if isInitialized {
                    ReallyHeavyView(data: data)
                        .equatable()
                } else {
                    PlaceholderView()
                        .onAppear {
                            // 메인 스레드를 블록하지 않고 초기화
                            DispatchQueue.main.async {
                                isInitialized = true
                            }
                        }
                }
            }
        }
    }
    
    // 스크롤 기반 가시성 최적화
    struct VisibilityOptimizedList: View {
        let items: [Item]
        @State private var visibleRange: Range<Int> = 0..<10
        
        var body: some View {
            ScrollViewReader { proxy in
                LazyVStack {
                    ForEach(Array(items.enumerated()), id: \.1.id) { index, item in
                        if visibleRange.contains(index) {
                            ItemView(item: item)
                                .equatable()
                        } else {
                            PlaceholderItemView(height: 80)
                        }
                    }
                }
                .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
                    updateVisibleRange(for: offset)
                }
            }
        }
        
        private func updateVisibleRange(for offset: CGFloat) {
            let itemHeight: CGFloat = 80
            let startIndex = max(0, Int(offset / itemHeight) - 5)
            let endIndex = min(items.count, startIndex + 20)
            
            if visibleRange != startIndex..<endIndex {
                visibleRange = startIndex..<endIndex
            }
        }
    }

     


    메모이제이션과 캐싱

    // computed property 캐싱
    struct MemoizedView: View {
        let rawData: [RawItem]
        @State private var cachedProcessedData: [ProcessedItem]?
        @State private var lastDataHash: Int?
        
        var body: some View {
            LazyVStack {
                ForEach(processedData) { item in
                    ProcessedItemView(item: item)
                        .equatable()
                }
            }
        }
        
        private var processedData: [ProcessedItem] {
            let currentHash = rawData.hashValue
            
            if let cached = cachedProcessedData,
               lastDataHash == currentHash {
                return cached
            }
            
            let processed = rawData.map { item in
                // 무거운 변환 로직
                ProcessedItem(from: item)
            }
            
            cachedProcessedData = processed
            lastDataHash = currentHash
            
            return processed
        }
    }
    
    // 이미지 캐싱과 메모리 관리
    class ImageCache: ObservableObject {
        private var cache: [URL: UIImage] = [:]
        private let maxCacheSize = 50
        
        func image(for url: URL) -> UIImage? {
            return cache[url]
        }
        
        func setImage(_ image: UIImage, for url: URL) {
            if cache.count >= maxCacheSize {
                // LRU 정책으로 오래된 이미지 제거
                let oldestKey = cache.keys.first!
                cache.removeValue(forKey: oldestKey)
            }
            cache[url] = image
        }
    }

     


    상태 분할과 지역화

    // ❌ 모놀리틱 상태 (전체 리렌더링 유발)
    class MonolithicStore: ObservableObject {
        @Published var users: [User] = []
        @Published var posts: [Post] = []
        @Published var comments: [Comment] = []
        @Published var uiState: UIState = .loading
        
        // users 변경 시 posts, comments 관련 뷰도 재렌더링됨
    }
    
    // ✅ 분할된 상태 관리
    class UserStore: ObservableObject {
        @Published var users: [User] = []
    }
    
    class PostStore: ObservableObject {
        @Published var posts: [Post] = []
    }
    
    class UIStateStore: ObservableObject {
        @Published var currentState: UIState = .loading
    }
    
    // 각 뷰는 필요한 상태만 구독
    struct UserListView: View {
        @StateObject private var userStore = UserStore()
        
        var body: some View {
            LazyVStack {
                ForEach(userStore.users) { user in
                    UserRowView(user: user)
                        .equatable()
                }
            }
        }
    }
    
    struct PostListView: View {
        @StateObject private var postStore = PostStore()
        
        var body: some View {
            LazyVStack {
                ForEach(postStore.posts) { post in
                    PostRowView(post: post)
                        .equatable()
                }
            }
        }
    }

     


    최적화 전략

    iOS 17의 @Observable 활용

    // 새로운 관찰 시스템 활용
    @Observable
    class ModernDataStore {
        var users: [User] = []
        var selectedUser: User?
        var isLoading = false
        
        // 세밀한 의존성 추적이 자동으로 이뤄짐
    }
    
    struct ModernView: View {
        @State private var store = ModernDataStore()
        
        var body: some View {
            VStack {
                // users 변경시에만 이 부분이 업데이트됨
                UserCountView(count: store.users.count)
                    .equatable()
                
                // isLoading 변경시에만 이 부분이 업데이트됨
                if store.isLoading {
                    ProgressView()
                } else {
                    UserListView(users: store.users)
                        .equatable()
                }
            }
        }
    }
    
    struct UserCountView: View, Equatable {
        let count: Int
        
        var body: some View {
            Text("Users: \(count)")
                .font(.headline)
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.count == rhs.count
        }
    }

     


    SwiftData와의 통합 최적화

    // SwiftData와 최적화된 통합
    @Model
    class OptimizedTask {
        @Attribute(.unique) var id: UUID
        var title: String
        var isCompleted: Bool
        var priority: TaskPriority
        var createdAt: Date
        
        // 계산 프로퍼티는 @Transient로 캐싱
        @Transient var displayTitle: String {
            "\(priority.emoji) \(title)"
        }
        
        init(title: String, priority: TaskPriority = .medium) {
            self.id = UUID()
            self.title = title
            self.isCompleted = false
            self.priority = priority
            self.createdAt = Date()
        }
    }
    
    struct SwiftDataOptimizedView: View {
        @Query(sort: \OptimizedTask.createdAt) private var tasks: [OptimizedTask]
        @State private var selectedPriority: TaskPriority?
        
        var body: some View {
            NavigationView {
                TaskListView(
                    tasks: filteredTasks,
                    selectedPriority: selectedPriority
                )
                .equatable()
                .toolbar {
                    PriorityFilterView(selection: $selectedPriority)
                        .equatable()
                }
            }
        }
        
        private var filteredTasks: [OptimizedTask] {
            if let priority = selectedPriority {
                return tasks.filter { $0.priority == priority }
            }
            return tasks
        }
    }
    
    struct TaskListView: View, Equatable {
        let tasks: [OptimizedTask]
        let selectedPriority: TaskPriority?
        
        var body: some View {
            List {
                ForEach(tasks, id: \.id) { task in
                    TaskRowView(task: task)
                        .equatable()
                }
            }
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.tasks.count == rhs.tasks.count &&
            lhs.selectedPriority == rhs.selectedPriority &&
            lhs.tasks.map(\.id) == rhs.tasks.map(\.id)
        }
    }

     


    프로젝트 마이그레이션 가이드

     

    마지막으로 기존 프로젝트에서 이 부분들을 적용시키기 위한 마이그레이션 가이드를 한번 정리해볼께요!

    단계별 마이그레이션 전략

    // Phase 1: 성능 문제 식별
    struct PerformanceAudit {
        static func auditView<V: View>(_ view: V) -> ViewAuditResult {
            var issues: [PerformanceIssue] = []
            
            // 1. EquatableView 미사용 체크
            if !(V.self is Equatable.Type) {
                issues.append(.missingEquatableConformance)
            }
            
            // 2. ForEach identity 체크
            // 3. 복잡한 computed property 체크
            // 4. 불필요한 @State 체크
            
            return ViewAuditResult(issues: issues)
        }
    }
    
    // Phase 2: 점진적 마이그레이션 적용
    extension View {
        func optimizeIfNeeded() -> some View {
            if #available(iOS 17.0, *) {
                // 새로운 방법 적용
                return self.modernOptimizations()
            } else {
                // 기존 방법 적용
                return self.legacyOptimizations()
            }
        }
        
        @available(iOS 17.0, *)
        private func modernOptimizations() -> some View {
            self.sensoryFeedback(.impact, trigger: self)
        }
        
        private func legacyOptimizations() -> some View {
            self.equatable()
        }
    }
    
    // Phase 3: 자동화된 모니터링
    class PerformanceMonitor: ObservableObject {
        @Published var frameRate: Double = 60.0
        @Published var memoryUsage: Int64 = 0
        @Published var scrollHitches: Int = 0
        
        private var displayLink: CADisplayLink?
        private var frameCount = 0
        private var lastTimestamp: CFTimeInterval = 0
        
        func startMonitoring() {
            displayLink = CADisplayLink(target: self, selector: #selector(updateMetrics))
            displayLink?.add(to: .main, forMode: .common)
        }
        
        @objc private func updateMetrics(_ displayLink: CADisplayLink) {
            frameCount += 1
            
            if lastTimestamp == 0 {
                lastTimestamp = displayLink.timestamp
                return
            }
            
            let elapsed = displayLink.timestamp - lastTimestamp
            if elapsed >= 1.0 {
                frameRate = Double(frameCount) / elapsed
                frameCount = 0
                lastTimestamp = displayLink.timestamp
                
                // 60fps 미만이면 성능 이슈 기록
                if frameRate < 55.0 {
                    recordPerformanceIssue()
                }
            }
        }
        
        private func recordPerformanceIssue() {
            // 성능 이슈를 로깅하거나 분석 시스템에 전송
            print("⚠️ Performance issue detected: \(frameRate) fps")
        }
    }

     


    팀 차원의 성능 관리

    // 코드 리뷰 체크리스트 자동화
    struct SwiftUIPerformanceChecker {
        static func generateCodeReviewChecklist(for file: String) -> [String] {
            var checklist: [String] = []
            
            // 1. EquatableView 사용 여부
            if !file.contains("equatable()") && !file.contains("Equatable") {
                checklist.append("❓ EquatableView 적용을 고려해보세요")
            }
            
            // 2. ForEach identity 패턴
            if file.contains("id: \\.self") {
                checklist.append("⚠️ ForEach에서 id: \\.self 사용을 재검토해보세요")
            }
            
            // 3. @State 남용
            let stateCount = file.components(separatedBy: "@State").count - 1
            if stateCount > 5 {
                checklist.append("🤔 @State 변수가 \(stateCount)개입니다. 상태 분할을 고려해보세요")
            }
            
            return checklist
        }
    }
    
    // 성능 회귀 방지를 위한 CI/CD 통합
    class PerformanceRegressionTest {
        static func runPerformanceBenchmark() -> PerformanceResult {
            let testCases = [
                ("LargeListScrolling", measureListScrolling),
                ("ComplexViewRendering", measureComplexViewRendering),
                ("StateUpdatePerformance", measureStateUpdates)
            ]
            
            var results: [String: TimeInterval] = [:]
            
            for (name, test) in testCases {
                let start = CFAbsoluteTimeGetCurrent()
                test()
                let elapsed = CFAbsoluteTimeGetCurrent() - start
                results[name] = elapsed
                
                // 기준치를 초과하면 빌드 실패
                if elapsed > getPerformanceThreshold(for: name) {
                    fatalError("Performance regression detected in \(name)")
                }
            }
            
            return PerformanceResult(results: results)
        }
        
        private static func getPerformanceThreshold(for testName: String) -> TimeInterval {
            // 각 테스트별 성능 기준치 반환
            switch testName {
            case "LargeListScrolling": return 0.016 // 60fps 기준
            case "ComplexViewRendering": return 0.1
            case "StateUpdatePerformance": return 0.01
            default: return 1.0
            }
        }
    }

     


    Conclusion

    핵심이 되는 부분을 정리해볼까요?

    Identity가 모든 것의 시작

    • ForEach에서 안정적이고 유니크한 ID 사용하기
    • id: \.self는 신중하게, 중복 데이터가 없을 때만
    • Identifiable 프로토콜 적극 활용하기

    EquatableView로 diffing 제어

    • 복잡한 뷰는 커스텀 비교 로직 구현
    • @State, @ObservableObject는 별도 메커니즘임을 인지
    • 비교 비용 < body 계산 비용일 때만 사용

    뷰 복잡도 관리

    • 큰 body를 작은 diffable 조각들로 분할
    • computed property와 closure 사용 최소화
    • SwiftLint 룰로 복잡도 자동 체크
    가장 중요한 건 측정 → 분석 → 최적화 → 검증의 사이클을 반복하는 것입니다.

     

    무작정 모든 뷰에 .equatable()을 붙이는 게 아니라, 실제로 성능 병목이 되는 부분을 찾아서 집중적으로 개선하는 것이 효과적이라고 생각합니다.

     


    References

     

    Learn SwiftUI’s View struct: value semantics, diffing and dependency tracking – Malcolm Hall

    Learn SwiftUI’s View struct: value semantics, diffing and dependency tracking Published by malhal on 23rd March 202323rd March 2023 Learning SwiftUI can be tricky because it has a very different design to previous UI frameworks. Many make the mistake of

    www.malcolmhall.com

     

    Optimizing views in SwiftUI using EquatableView

    SwiftUI provides us a very fast and easy to use diffing algorithm, but as you might know, diffing is a linear operation. It means that diffing will be very fast for simple layouts and can take some time for a complicated layout.

    swiftwithmajid.com

     

    SwiftUI's diffing algorithm - Rens Breur

    If one thing sets SwiftUI apart from other declarative UI frameworks like React, it is its use of a static type system. The typing of views makes the diffing of views fast and unambiguous. How exactly does that work? And what more is involved in the diffin

    rensbr.eu

     

    Understanding and Improving SwiftUI Performance

    New techniques we’re using at Airbnb to improve and maintain performance of SwiftUI features at scale

    medium.com

     

Designed by Tistory.