본문 바로가기
Project/SwiftUI 블록와이드

[SwiftUI Project] 예외처리: Json Data -> CoreData 백업하기

by iOS_woo 2022. 10. 14.

데이터를 받지 못했을 시 백업된 코인 리스트를 보여줍니다.

이번에는 서버로부터 데이터를 받지 못했을 때, StatusCode가 200이 아닐 때 사용자들에게 다른 화면을 보여줄 수 있도록 작업했습니다.

 

구현해야 하는 예외 처리 중에 첫번째는 "만약 인터넷은 연결되어 있는데 서버에 오류가 생겨 코인리스트를 받아오지 못했을 때" 백업된 코인리스트를 보여주는 것입니다. 

 

실제로 CoinGecko Status 사이트에서 상태 기록을 보면 종종 에러가 발생하는 것을 볼 수 있습니다. (빨간 블록이 오류가 발생한 날입니다.)

최근에도 개발 도중에 이틀 연속 서버 500 장애가 발생해 당혹스러웠는데요.

Coingecko Status

만약 이렇게 불안정한 서버로 서비스했을 때 예외 처리를 제대로 하지 못한다면 사용자의 신뢰가 하락하게 되겠지요.

사용자의 이탈이 발생할 수 있기 때문에 예외 처리는 무척이나 중요합니다!

 

이번에는 예외 처리 중에 첫번째로 Json Data로 받아온 코인리스트를 Core Data로 백업해놨다가 데이터가 없다면 백업된 리스트가 보일 수 있도록 하였습니다.

 

1. CoreData로 저장할 수 있는 Entity를 작성합니다. 

 

2. 코인리스트를 Core Data로 저장할 수 있는 코드를 작성해줍니다.

import Foundation
import CoreData

class CoinBackupDataService {
    
    @Published var backupCoins: [BackupCoinEntity] = []
    
    private let container: NSPersistentContainer
    private let containerName: String = "BackupCoinContainer"
    private let entityName: String = "BackupCoinEntity"
    
    init() {
        container = NSPersistentContainer(name: containerName)
        container.loadPersistentStores { (_, error) in
            if let error = error {
                print("Error loading Core Data! \(error)")
            }
            self.getBackup()
        }
    }
    
    // 중복으로 저장되는 일이 없도록 한번 체크해줍니다.
    func updateBackup(coins: [CoinModel]) {
        if backupCoins.isEmpty {
            add(coins: coins)
        } else {
            delete()
            add(coins: coins)
        }
    }
    
    // viewModel로 부터 리스트를 전달받고 CoreData에 저장 
    private func add(coins: [CoinModel]) {
        coins.forEach { (data) in
            let entity = BackupCoinEntity(context: container.viewContext)
            entity.id = data.id
            entity.symbol = data.symbol
            entity.name = data.name
            entity.image = data.image
            entity.rank = data.marketCapRank ?? 0
        }
        applyChanges()
    }
    
    // coinBackup 배열에 저장
    private func getBackup() {
        let request = NSFetchRequest<BackupCoinEntity>(entityName: entityName)
        do {
            backupCoins = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching Watchlist Entities. \(error)")
        }
    }
    
    // Save
    private func save() {
        do {
            try container.viewContext.save()
            print("Success saving to Cora Data.")
        } catch let error {
            print("Error saving to Core Data. \(error)")
        }
    }
    
    // 변경 적용
    private func applyChanges() {
        save()
        getBackup()
    }
    
    // Delete
    private func delete() {
        backupCoins.forEach { (coin) in
            container.viewContext.delete(coin)
        }
        applyChanges()
    }
}

 

3. Coingecko API를 통해 받아온 코인리스트를 ViewModel에서 다음의 함수를 이용해 전달해줍니다.

func updateBackup(coins: [CoinModel]) {
        if backupDataService.backupCoins.isEmpty {
            backupDataService.updateBackup(coins: coins)
        }
    }

함수가 불리는 타이밍은 코인리스트가 저장 된 직후에 같이 호출되도록 해줬습니다.

dataService.$allCoins
            .combineLatest($sortOption)
            .map(sortCoins)
            .sink { [weak self] (returnedCoins) in
                self?.allCoins = returnedCoins
                self?.updateBackup(coins: returnedCoins) <-- 백업!
                self?.configureTopMovingCoins()
                self?.configurelowMovingCoins()
                self?.loadWatchlist()
            }
            .store(in: &cancellables)

 

CoinGecko API로부터 받아온 데이터를 CoreData에 저장하는 작업은 모두 끝났습니다!

이제는 "A 데이터"가 없으면  "백업 데이터"를 볼 수 있도록  옵셔널 작업(?)을 해줘야 합니다. 

 

코인리스트 데이터를 사용하는 모든 파일과 네트워크 파일에서 대응 할 수 있도록 코드를 작성하느라 꽤 많은 시간이 걸렸는데요.

글에는 모든 코드를 가져올 수 없어서 어떤 작업이었는지 대표적인 것만 가져오도록 하겠습니다. 

 

1.  옵셔널이 아니었던 변수에 값이 없을 수 있다는 옵셔널 ? 을 붙여줍니다.

struct CoinRowView: View {
    
    var coin: CoinModel? <-- 코인리스트
    var backup: BackupCoinEntity? <-- 백업 코인리스트
    
    var body: some View {
        HStack(spacing: 0) {
            leftColumn
            Spacer()
            rightColmn
        }
        .padding()
        .contentShape(Rectangle())
    }
}

 

2. 만약 coin 이라는 변수가 nil 이라면 backup 변수를 사용하라고 옵셔널 처리를 해줍니다.

// 옵셔널 처리 예시
KFImage(URL(string: coin == nil ? backup?.image ?? "" : coin?.image ?? ""))
                .resizable()
                .scaledToFit()
                .frame(width: 32, height: 32)
                .cornerRadius(5)

// 작성하면서 터득한 것인데.. 이렇게 작성하면 더 간결해보였습니다.
KFImage(URL(string: (backup?.image ?? coin?.image) ?? ""))
                .resizable()
                .scaledToFit()
                .frame(width: 32, height: 32)
                .cornerRadius(5)

 

3. 다른 View에 데이터를 전달해주는 것도 if 문과 nil 을 사용하면 처리 가능합니다. 

// 옵셔널 처리 예시
// StatusCode와 코인리스트의 존재 여부에 따라서 백업을 보여줄지, 코인리스트를 보여줄 지 결정하는 코드입니다.
if viewModel.status != .status200 && viewModel.allCoins.isEmpty {
                    ForEach(viewModel.backupCoins) { backup in
                        NavigationLink(
                            destination: NavigationLazyView(DetailView(coin: nil, backup: backup)),
                            label: {
                                CoinRowView(coin: nil, backup: backup)
                            })
                            .buttonStyle(ListSelectionStyle())
                    }
                } else {
                    ForEach(viewModel.allCoins) { coin in
                        NavigationLink(
                            destination: NavigationLazyView(DetailView(coin: coin, backup: nil)),
                            label: {
                                CoinRowView(coin: coin, backup: nil)
                            })
                            .buttonStyle(ListSelectionStyle())
                    }
                }

 

4. ViewModel, DataService 에서 init은 다음과 같이 작성해주었습니다. 

// 옵셔널 처리 예시
init(coin: CoinModel?, backup: BackupCoinEntity?) {
        if coin == nil {
            self.backup = backup
        } else {
            self.coin = coin
        }
        getArticles()
    }

 

이런 식으로 전부 대응할 수 있도록 코드를 수정해주면 완료입니다. 

 

개인적으로 이번에 옵셔널 처리를 해주면서 약간 후회 되었던 지점도 있었는데.. 그것은 바로 하나하나 옵셔널 처리하는 작업이 상당히 번거롭다는 것입니다. 

이번에는 작업이 거의 끝날 무렵에 작업량이 상당하다는 것을 깨달아서 변경할 수 없었지만.. 

다음에 같은 작업을 해야한다면 같은 파일에서 옵셔널 처리를 해주는 것보다 새로운 백업 View를 만들어서 관리하는 것도 좋겠다는 생각이 들었습니다.

 

"A가 없다면 B를 보여줘!"

이러한 예외처리를 조금 더 효율적으로 할 수 있는 방법은 많은 프로젝트를 해보면서 찾아가야 할 것 같아요!

 

 

참고 유튜브: 

 

댓글