SwiftUI : Background Threads / Queues
이 게시글을 보시기 전에 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
}
자 fetchData
에 downloadData()
를 성공적으로 호출했고 이제 downloadData
는 이 클래스 내에서만 사용할 것이기 때문에 private
를 넣어줘서 클래스 내에서만 액세스 할 수 있도록 해줍니다.
private func downLoadData() -> [String] {
var data: [String] = []
for i in 0..<100 {
data.append("\(i)")
print(data)
}
return data
}
그리고 Body
의 Text
에 onTapGeuture
을 사용하여 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탭을 보면 지금은 Thread1과 Thread4가 존재합니다. 여기서 Thread1이 Main Thread입니다. 작성하는 모든 코드가 Thread1에서 실행됩니다. 만약 시뮬레이터에서 텍스트를 탭 해보면 스파이크가 생기고 스크롤을 하면 다시 한번 Main Thread의 변화를 확인할 수 있습니다.
사용자가 앱을 스크롤했을 때 갑자기 속도가 늦어지거나 렉이 발생하면 아마 Main Thread에서 너무 많은 프로세스가 진행 중이라는 것을 알 수 있겠네요.
물론 지금은 간단하게 1
에서 100
까지의 숫자만 불러왔기 때문에 문제가 되지 않지만, 만약 인터넷에서 무언가를 다운로드해 오거나 무거운 작업을 한다면 문제가 발생할 수 있습니다. 앞서 말했지만 이것을 해결하기 위해서는 background Thread로 이동시켜야 합니다.
바로 한번 해보겠습니다.
다시 ContentView
로 돌아가서 우리가 해줄 것은 class
의 downloadData
를 background Thread로 옮겨줄 것입니다. 이것을 옮기는 방법은 아주 간단합니다. 바로 dispatchQueue
를 이용하는 거죠.
func fetchData() {
DispatchQueue.global(qos: .background).async {
let newData = downLoadData()
dataArray = newData
}
}
이렇게 dispatchQueue
를 적용시키면 한 가지 오류를 확인할 수 있습니다.
downLoadData
에 self
를 붙여줘야 합니다. 왜냐하면 DownloadData
는 실제로 ViewModel
클래스에 있기 때문이죠.
func fetchData() {
DispatchQueue.global(qos: .background).async {
let newData = self.downLoadData()
self.dataArray = newData
}
}
이제 시뮬레이터를 다시 실행해보고 CPU
의 Thread
를 확인해볼 차례입니다.
이렇게 Main Thread에서 Background Thread로 일부 작업을 오버로드 하였습니다.
여기서 중요한 점은 바로,
UI를 이동할 때(스크롤할 때)는 여전히 Main Thread에 있습니다. (매우 중요)
- UI에 영향을 미치는 모든 작업은 Main Thread에서 수행
다시 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 2는 main Thread이기 때문에 true
로 나와야 정상입니다.
한번 시뮬레이터를 실행해서 확인해볼게요.
읽어주셔서 감사합니다🤟