[Project-Outline] 뷰 구조 개선

2024. 8. 18. 00:01· → Outline
목차
  1. 🧐 문제 상황
  2. 코드의 불필요한 중복, 로직의 분리 필요
  3. 💡 해결 과정

🧐 문제 상황

코드의 불필요한 중복, 로직의 분리 필요

기존의 RecordView만 보더라도 총 240줄의 긴 파일이었고, 당장 뷰와 로직의 분리가 되지 않아 유지보수, 가독성 부분에서 안좋은 부분이 많았습니다. 또한 필터링 부분이나 불필요한 print, 중복되는 언래핑등으로 코드의 개선이 필요했습니다. 또한 이러한 중복적인 계산로직으로 카드가 나타나는 UI를 표시하는 부분이 안보이기도 하는 오류도 발생했다.

 

RecordView만 봐도 알 수있듯이 시간에 쫓겨서 개발한 코드이기도 하고 이전에 작성한 코드이기도 하다보니 개선사항이 많이 보였다. 😎

// 불필요한 중복 계산, 로직과 뷰의 분리 문제
struct RecordView: View {
	// 너무 많은 속성 변수들
    @AppStorage("authState") var authState: AuthState = .logout
    @FetchRequest (entity: CoreRunningRecord.entity(), sortDescriptors: []) var runningRecord: FetchedResults<CoreRunningRecord>
    @State private var selectedIndex: Int = 0
    @State private var scrollOffset: CGFloat = 0
    @State private var filteredRecords: [CoreRunningRecord] = []
    @State private var gpsArtRecords: [CoreRunningRecord] = []
    @State private var freeRecords: [CoreRunningRecord] = []
    @State private var selectedSortOption: SortOption = .latest
    @State private var isDeleteData = false
    @State private var navigationTitle = "모든 아트"
    
    var body: some View {
        ZStack(alignment: .top) {
        
        	// ... 중략
            if authState == .login {
                ScrollView {
                    RecordHeader(scrollOffset: scrollOffset)
                    
                    if filteredRecords.isEmpty {
                        // ... 중략
                    } else {
                        LazyVStack(alignment: .leading) {
                            ScrollView(.horizontal, showsIndicators: false) {
                                LazyHStack(spacing: 16) {
                                    ForEach(filteredRecords.prefix(5), id: \.id) { record in
                                        if let courseName = record.courseData?.courseName,
                                           let coursePaths = record.courseData?.coursePaths,
                                           let startDate = record.healthData?.startDate,
                                           let score = record.courseData?.score {
                                            let data = pathToCoordinate(coursePaths)
                                            let cardType = getCardType(forScore: score)
                                            NavigationLink {
                                                RecordDetailView(isDeleteData: $isDeleteData, record: record, cardType: cardType)
                                            } label: {
                                                RecordCardView(size: .carousel, type: cardType, name: courseName, date: formatDate(startDate), coordinates: data!)
                                            }
                                            .padding(.bottom, 8)
                                        }
                                }
                            }
                            
                            VStack(alignment: .leading) {
                                HStack {
                                    // ... 중략
                                    NavigationLink {
                                        RecordGridView(title: "GPS 아트", records: gpsArtRecords)
                                    } label: { ... }
                                }
                                LazyHStack(spacing: 16) {
                                    ForEach(gpsArtRecords.prefix(3), id: \.id) { record in
                                        if let courseName = record.courseData?.courseName,
                                           let coursePaths = record.courseData?.coursePaths,
                                           let startDate = record.healthData?.startDate,
                                           let score = record.courseData?.score {
                                            let data = pathToCoordinate(coursePaths)
                                            let cardType = getCardType(forScore: score)
                                            NavigationLink {
                                                RecordDetailView(isDeleteData: $isDeleteData, record: record, cardType: cardType)
                                            } label: {
                                                RecordCardView(size: .list, type: cardType, name: courseName, date: formatDate(startDate), coordinates: data!)
                                            }
                                        }
                                    }
                                }
                            }
                            
                            VStack(alignment: .leading) {
                                HStack {
                                	// ... 중략
                                    NavigationLink {
                                        RecordGridView(title: "자유러닝", records: freeRecords)
                                    } label: { ... }
                                    
                                }
                                
                                LazyHStack(spacing: 16) {
                                    ForEach(freeRecords.prefix(3), id: \.id) { record in
                                        if let courseName = record.courseData?.courseName,
                                           let coursePaths = record.courseData?.coursePaths,
                                           let startDate = record.healthData?.startDate,
                                           let score = record.courseData?.score {
                                            let data = pathToCoordinate(coursePaths)
                                            let cardType = getCardType(forScore: score)
                                            NavigationLink {
                                                RecordDetailView(isDeleteData: $isDeleteData, record: record, cardType: cardType)
                                            } label: {
                                                RecordCardView(size: .list, type: cardType, name: courseName, date: formatDate(startDate), coordinates: data!)                                            }
                                        }
                                    }
                                    
                                    ForEach(0 ..< max(0, 3 - freeRecords.count), id: \.self) { _ in
                                        RecordEmptyCardView(size: .list)
                                    }
                                }
                            }
                        }
                    }
                }
            }
            
            // ... 중략
        }
        // 뷰와 로직의 분리 필요
        .onAppear {
            filteredRecords = Array(runningRecord)
                .sorted { ... }
            
            gpsArtRecords = filteredRecords
                .filter { ... }
                .sorted { ... }
            
            freeRecords = filteredRecords
                .filter { ... }
                .sorted { ... }
            
            print("Filtered Records Count: \(filteredRecords.count)")
            print("GPS Art Records Count: \(gpsArtRecords.count)")
            print("Free Records Count: \(freeRecords.count)")
        }
        .overlay {
            if isDeleteData { ... }
        }
    }
    
    func getCardType(forScore score: Int32) -> CardType { ... }
    func formatDate(_ date: Date) -> String { ... }
    func pathToCoordinate(_ paths: NSOrderedSet) -> [CLLocationCoordinate2D]? { ... }
}

💡 해결 과정

RecordViewModel을 만들어서 모든 변수를 옮기고, CoreRunningRecord를 RunningRecord로 바꿔주어 @Published var를 활용하여 뷰에 뿌려주었다.

class RecordViewModel: ObservableObject {
    @Published var recentRunningRecords: [RunningRecord] = []
    @Published var gpsArtRunningRecords: [RunningRecord] = []
    @Published var freeRunningRecords: [RunningRecord] = []
    
    private let userDataModel = UserDataModel()
    
    private let persistenceController = PersistenceController.shared
 
    func loadRunningRecords() {
        let fetchRequest: NSFetchRequest<CoreRunningRecord> = CoreRunningRecord.fetchRequest()
        
        do {
            let coreRunningRecords = try persistenceController.container.viewContext.fetch(fetchRequest)
            let runningRecords = coreRunningRecords.compactMap {
                userDataModel.convertToRunningRecord(coreRecord: $0)
            }
            
            recentRunningRecords = runningRecords
                .sorted(by: { $0.healthData.startDate > $1.healthData.startDate })
            
            gpsArtRunningRecords = runningRecords
                .filter { $0.runningType == .gpsArt }
                .sorted(by: { $0.courseData.score ?? -1 > $1.courseData.score ?? -1 })
            
            freeRunningRecords = runningRecords
                .filter { $0.runningType == .free }
                .sorted(by: { $0.healthData.startDate > $1.healthData.startDate })
            
        } catch {
            print("코어데이터에서 러닝기록을 가져오는데 실패했습니다: \(error)")
        }
    }
    
    func deleteCoreRunningRecord(_ id: String) { ... }
    
    func updateCoreRunningRecord(_ id: String, courseName: String) { ... }
    
    func saveShareData(_ runningRecord: RunningRecord) { ... }
    
    func getCardType(for score: Int?) -> CardType { ... }
    
    func sortingOptions(for title: String) -> [SortOption] { ... }
    
    func sortRecords(_ record1: RunningRecord, _ record2: RunningRecord) -> Bool { ... }    
}

결과적으로 확실히 뷰가 간결해 졌다. 240줄의 파일이 77줄로 줄었으며, 뷰의 역할도 분리해줬다.

struct RecordView: View {    
    @AppStorage("authState") var authState: AuthState = .logout
    @StateObject private var viewModel = RecordViewModel()
    
    var body: some View {
        ZStack(alignment: .top) {   
            if authState == .login {
                ScrollView {
                    Color.clear.frame(height: 0)
                        .onScrollViewOffsetChanged { offset in
                            viewModel.scrollOffset = offset
                        }
                    
                    RecordHeaderView(scrollOffset: viewModel.scrollOffset)
                    
                    if viewModel.recentRunningRecords.isEmpty {
                        RecordEmptyRunningView()
                    } else {
                        LazyVStack(alignment: .leading) {
                            ScrollView(.horizontal, showsIndicators: false) {
                                RecordListView(viewModel: viewModel, type: .main)
                            }
                            
                            VStack(alignment: .leading) {
                                RecordListHeaderView(viewModel: viewModel, type: .gpsArt)
                                RecordListView(viewModel: viewModel, type: .gpsArt)
                            }
                            
                            VStack(alignment: .leading) {
                                RecordListHeaderView(viewModel: viewModel, type: .free)
                                RecordListView(viewModel: viewModel, type: .free)
                            }
                        }
                    }
                }
                .overlay(alignment: .top) {
                    RecordInlineHeaderView(scrollOffset: viewModel.scrollOffset)
                }
            } else {
                RecordLookAroundView()
            }
        }
        .onAppear {
            viewModel.loadRunningRecords()
        }
        .overlay {
            if viewModel.isDeleteData {
                RunningPopup(text: "기록을 삭제했어요")
                    .frame(maxHeight: .infinity, alignment: .top)
            }
        }
    }
}

분리된 뷰의 역할들

'→ Outline' 카테고리의 다른 글

[Project-Outline] watchOS 1차 리팩토링  (0) 2024.08.10
[Project-Outline] Smooth Algorithm 개선  (0) 2024.08.04
[Project-Outline] MapSnapshot 적용  (0) 2024.08.04
  1. 🧐 문제 상황
  2. 코드의 불필요한 중복, 로직의 분리 필요
  3. 💡 해결 과정
'→ Outline' 카테고리의 다른 글
  • [Project-Outline] watchOS 1차 리팩토링
  • [Project-Outline] Smooth Algorithm 개선
  • [Project-Outline] MapSnapshot 적용
Swift librarian
Swift librarian
Swift librarian
Swift Library
Swift librarian
전체
오늘
어제
  • 분류 전체보기 (231)
    • 📺 Programming (5)
    • → Architecture (2)
    • → Design Pattern (0)
    • → Computer Science (15)
    • ⚙️ Algorithm (0)
    • → 알고리즘 관련 (22)
    • → Problems (104)
    • 🚀 Project (0)
    • → 알쏭달쏭 (0)
    • → Shook (2)
    • → Solver (8)
    • → Taster (7)
    • → Outline (4)
    • → Pointer (2)
    • → Guesser (3)
    • 🦜 Swift (2)
    • → Swift Archive (12)
    • → Swift Study (12)
    • → Xcode (6)
    • 🧰 Framework (0)
    • → Foundation (1)
    • → UIKit (2)
    • → SwiftUI (3)
    • → CoreData (2)
    • → MapKit (1)
    • → CoreHaptic (1)
    • → User Notification (1)
    • → StoreKit (2)
    • 🏛️ Library (0)
    • → TCA (0)
    • 🐈‍⬛ Git (8)
    • → Git의 원리 (2)
    • → Git 심화 (1)
    • 📦 Other (1)
    • 👦🏻 Log (0)

최근 글

hELLO · Designed By 정상우.v4.2.2
Swift librarian
[Project-Outline] 뷰 구조 개선
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.