SWIFTUI/Others

SwiftUI : Background Threads / Queues

서근 2021. 6. 7. 16:34
반응형

이 게시글을 보시기 전에 DispatchQueue에 대에 먼저 보시고 오시는 것을 추천드립니다.

Thread

기본적으로 앱에는 여러 Thread가 있습니다. 이 Thread는 작업을 수행할 수 있는 프로세스를 수행할 수 있죠. 앱에 작성하는 모든 코드는 main Thread 즉, Thread1에 선언됩니다.

 

특별히 지정하지 않은 이상은 Main Thread에서 작업이 진행되는 것이죠.

 

하지만 코드를 작성하면 작성할수록 코드가 길어지고 Main Thread에서 많은 작업을 수행하게 되고 앱의 속도가 느려지고 렉을 유발할 수 있습니다.

 

그래서 Download와 같이 background에서 발생하는 무거운 작업을 Main Thread가 아닌 Background Thread에서 실행하는 것이 가장 좋습니다. 

Thread에서 가장 중요한 것은 UI를 업데이트하는 모든 것을 기억해야 한다는 점입니다. 즉, 실제 화면을 업데이트하려면 반드시 Main Thread에서 수행해야 합니다.

Thread의 업데이트 과정

Main Thread에서 시작 👉🏻  앱 실행 👉🏻  Background Thread로 이동 👉🏻  작업을 수행 👉🏻  수행한 작업을 업데이트하기 전에 Main Thread로 이동 👉🏻  수행 작업 업데이트

 

직접 화면을 구성해 가면서 자세히 알아보도록 하겠습니다.

 

새로운 SwiftUI 프로젝트를 하나 만들어 ViewModel class를 하나 생성해줍니다.

import SwiftUI

class backgroundThreadViewModel: ObservableObject {
    
}

struct backgroundThread: View {
    @StateObject var bvm = backgroundThreadViewModel()
    
    var body: some View {
        Text("Hello, World!")
    }
}

그리고 Body에 간단한 View를 생성해줄게요.

import SwiftUI

class backgroundThreadViewModel: ObservableObject {
    @Published var dataArray: [String] = []
}

struct backgroundThread: View {
    @StateObject var bvm = backgroundThreadViewModel()
    
    var body: some View {
        ScrollView{
            LazyVStack(spacing: 10) {
                Text("Background Thread")
                    .font(Font.title2.bold())
                
                ForEach(bvm.dataArray, id: \.self) { data in
                    Text(data)
                        .font(.headline)
                        .foregroundColor(Color("Peach"))
                }
            }
        }
    }
}

이제 ViewModel에서 데이터를 로드하는 함수를 하나 만들어 줍니다. 이름은 func fetchData라고 정해주겠습니다.

 

fetchData함수를 일단 생성해 놨고 다음으로는 downLoadData함수를 만들어 주려고 하는데, 이 함수는 일부 데이터를 반환해 줄 것이기 때문에 [String]을 반환하고 이 함수에는 실제로 인터넷에서 데이터를 다운로드하는 함수입니다. 하지만 우리는 지금 인터넷에서 다운로드할 필요가 없기 때문에 fake Data를 하나 만들어 주겠습니다.

    func downLoadData() -> [String] {
        var data: [String] = []
        
        for i in 0..<100 {
            data.append("\(i)")
            print(data)
        }
        return data
    }

100개의 숫자를 화면에 만들어 줬습니다. 이제 이것을 fetchData에 호출해줍니다.

    func fetchData() {
        let newData = downLoadData()
        dataArray = newData
    }

fetchDatadownloadData()를 성공적으로 호출했고 이제 downloadData는 이 클래스 내에서만 사용할 것이기 때문에 private를 넣어줘서 클래스 내에서만 액세스 할 수 있도록 해줍니다.

    private func downLoadData() -> [String] {
        var data: [String] = []
        
        for i in 0..<100 {
            data.append("\(i)")
            print(data)
        }
        return data
    }

그리고 BodyTextonTapGeuture을 사용하여 fetchData를 호출합니다.

    var body: some View {
        ScrollView{
            LazyVStack(spacing: 10) {
                Text("Background Thread")
                    .font(Font.title2.bold())
                    .onTapGesture {
                        bvm.fetchData()
                    }

캔버스를 실행해서 Background Thread텍스트를 클릭해볼까요?

성공적으로 100개의 아이템이 생성되었습니다. 이제 Thread가 어떻게 실행되고 있는지 확인해 볼 차례입니다. 이것을 확인해보기 위해서는 시뮬레이터를 실행해야 합니다. 시뮬레이터를 수정하고 아래 아이콘을 클릭해주세요.

Percentage Used화면을 보면 지금은 0%로 나와 있지만 복잡하고 큰 데이터를 가지고 있는 앱을 구축하려면 반드시 이 CPU를 확인해야 합니다. 아래 막대가 빨간색이면 CPU가 높은지 확인해야 하죠. 만약 CPU가 높다면 이것을 해결할 수 있는 방법이 바로 Background  Thread 또는 여러 Thread를 사용하는 것입니다. 

아래 Threads탭을 보면 지금은 Thread1Thread4가 존재합니다. 여기서 Thread1Main Thread입니다. 작성하는 모든 코드가 Thread1에서 실행됩니다. 만약 시뮬레이터에서 텍스트를 탭 해보면 스파이크가 생기고 스크롤을 하면 다시 한번 Main Thread의 변화를 확인할 수 있습니다.

사용자가 앱을 스크롤했을 때 갑자기 속도가 늦어지거나 렉이 발생하면 아마 Main Thread에서 너무 많은 프로세스가 진행 중이라는 것을 알 수 있겠네요.

 

물론 지금은 간단하게 1에서 100까지의 숫자만 불러왔기 때문에 문제가 되지 않지만, 만약 인터넷에서 무언가를 다운로드해 오거나 무거운 작업을 한다면 문제가 발생할 수 있습니다. 앞서 말했지만 이것을 해결하기 위해서는 background Thread로 이동시켜야 합니다.

 

바로 한번 해보겠습니다.

 

다시 ContentView로 돌아가서 우리가 해줄 것은 classdownloadDatabackground Thread로 옮겨줄 것입니다. 이것을 옮기는 방법은 아주 간단합니다. 바로 dispatchQueue를 이용하는 거죠. 

 

 

    func fetchData() {
        
        DispatchQueue.global(qos: .background).async {
          
        let newData = downLoadData()
        dataArray = newData
            
        }
    }

이렇게 dispatchQueue를 적용시키면 한 가지 오류를 확인할 수 있습니다.

downLoadDataself를 붙여줘야 합니다. 왜냐하면 DownloadData는 실제로 ViewModel 클래스에 있기 때문이죠.

    func fetchData() {
        
        DispatchQueue.global(qos: .background).async {
          
            let newData = self.downLoadData()
            self.dataArray = newData
            
        }
    }

이제 시뮬레이터를 다시 실행해보고 CPUThread를 확인해볼 차례입니다.

이렇게 Main Thread에서 Background Thread로 일부 작업을 오버로드 하였습니다.

 

여기서 중요한 점은 바로, 

UI를 이동할 때(스크롤할 때)는 여전히 Main Thread에 있습니다. (매우 중요)

 - UI에 영향을 미치는 모든 작업은 Main Thread에서 수행

스크롤만 했을때에는 Thread1만 스파이크가 튄다.
텍스트를 클릭해 ontapgesture을 수행하면 Thread14의 스파이크가 튄다.

다시 fetchData() 함수로 돌아가서 우리는 Background Thread에서 데이터를 다운로드했고, 데이터를 처리하고 데이터를 이동하고 난 후  데이터베이스로 돌아갈 때 UI를 업데이트하는 작업을 수행하기 때문에 Data Array를 업데이트하면 UI에 View를 업데이트할 것이므로 Data Array를 업데이트할 때마다 background Thread가 아닌 Main Thread에서 수행돼야 합니다.

    func fetchData() {
        
        DispatchQueue.global(qos: .background).async {
          
            let newData = self.downLoadData()
            
            DispatchQueue.main.async {
                self.dataArray = newData
            }
        }
    }

마지막으로 알아볼 것은 앱에서 간단하게 이를 확인할 수 있는 방법입니다. 즉, 지금 실행되는 코드가 어느 Thread에 위치해 있는지 알 수 있는 방법이죠.

            Thread.isMainThread
            Thread.current

이 두 개의 코드는 Bool 타입이기 때문에 만약 Mian Thread에 있으면 true로 나오고 그게 아니라면 false를 프린트해주겠죠? 만약 Main Thread가 아니라면 어떤 Thread에 있는지 확인해야 하기 때문에 current도 같이 호출해줘야 합니다. 호출은 아래와 같이 작성합니다.

    func fetchData() {
        
        DispatchQueue.global(qos: .background).async {
          
            let newData = self.downLoadData()
            print("Thread 확인 1 : \(Thread.isMainThread)")
            print("Thread 확인 1 : \(Thread.current)")
            
            DispatchQueue.main.async {
                self.dataArray = newData
                print("Thread 확인 2 : \(Thread.isMainThread)")
                print("Thread 확인 2 : \(Thread.current)")
            }
        }
    }

코드를 보면 Thread 1 qos:. background 이기 때문에 mian Thread가 아니죠? 그럼 false가 돼야 하고,

Thread 2main Thread이기 때문에 true로 나와야 정상입니다. 

 

한번 시뮬레이터를 실행해서 확인해볼게요.

 

읽어주셔서 감사합니다🤟