→ Solver

[Project-Solver] Data race 오류 기록

Swift librarian 2024. 7. 7. 14:32

문제 상황

앱에서 fetch를 연속적으로 하게 되면 위와 같이 data race가 발생하는 것을 확인했습니다.

 

해결 과정

문제 파악

간단한 Top100Store 부터 살펴보니 fetch() 함수 부분에서 top100 = fetchedTop100fetch 마다 호출이 되기 때문에 발생하는 문제였습니다. 비동기적으로 top100 이 여러 번 업데이트되면서 동시성 문제가 발생하기 때문입니다.

@Observable
final class Top100Store {
    let useCase = FetchUseCase()
    
    var top100: Top100Entity?
    
    @ObservationIgnored
    @AppStorage("userId") var userId = ""
    
    init() { 
        fetch()
    }
    
    init(top100: Top100Entity) {
        self.top100 = top100
    }
    
    func fetch() {
        Task {
            do {
                let fetchedTop100 = try await useCase.fetchTop100(userId: userId)
                top100 = fetchedTop100 // 🐛 문제 발생 부분
            } catch {
                print("Error to fetch top100")
            }
        }
    }
}

문제 해결

1. isFetching 변수 추가 : 중복 fetch 호출을 방지하기 위해 isFetching 변수를 추가했습니다.

  • fetch 함수가 여러 번 호출되는 것을 방지하기 위해, isFetching 변수를 도입했습니다.
  • fetch 함수 시작 시, isFetching을 true로 설정하고, 작업이 끝나면 false로 설정합니다.

 

2. 메인 스레드에서 상태 업데이트: 비동기 작업이 완료된 후, DispatchQueue.main.async를 사용해 메인 스레드에서 상태를 업데이트하도록 했습니다.

  • 비동기 작업이 완료된 후, DispatchQueue.main.async를 사용해 top100을 메인 스레드에서 업데이트합니다. 이렇게 하면 UI 관련 업데이트가 메인 스레드에서 안전하게 이루어질 수 있습니다.

 

수정 코드

import SwiftUI

final class Top100Store: ObservableObject {
    private let useCase = FetchUseCase()
    
    @Published var top100: Top100Entity?
    @Published var isFetching = false
    
    init() {
        fetch()
    }
    
    init(top100: Top100Entity) {
        self.top100 = top100
    }
    
    func fetch() {
        guard !isFetching else { return }
        guard let userId = UserDefaults.standard.string(forKey: "userId") else {
            print("Failed to retrieve userId from UserDefaults")
            return
        }
        
        isFetching = true
        
        Task {
            defer {
                DispatchQueue.main.async {
                    self.isFetching = false
                }
            }
            do {
                let fetchedTop100 = try await useCase.fetchTop100(userId: userId)
                DispatchQueue.main.async {
                    self.top100 = fetchedTop100
                }
            } catch {
                print("Error to fetch top100")
            }
        }
    }
}