⭐️앨런님 강의를 듣고 정리한 글입니다⭐️
💥디스패치큐(GCD) 사용시 주의해야할 사항
❓반드시 메인큐에서 처리해야하는 작업
- UI관련 처리는 반드시 메인큐에서 처리해야한다
DispatchQueue.global().async{
//코드 1
//코드 2
DispatchQueue.main.async{
//다운로드 한 이미지 표시 코드
self.imageView.image = image // UI 관련 작업들은 메인큐에서 진행
}
}
//URLSession은 내부적으로 비동기 처리가 되어있음
URLSession.shared.dataTask(with: url){
//코드 1
//코드 2
DispatchQueue.main.async{
//다운로드 한 이미지 표시 코드
self.imageView.image = image // UI 관련 작업들은 메인큐에서 진행
}
}.resume()
❓컴플리션핸들러의 존재이유 - 올바른 콜백함수의 사용 ⭐️⭐️⭐️
iOS에서 비동기(Async) 처리 방식은 "해당 작업을 기다리지 않고 다음 작업을 진행"하는 방식이다.
이러한 방식은 작업을 분산 처리하여 성능을 높인다는 장점이 있다. 하지만 특정 작업의 함수 결과물을 의존/사용하는 다른 작업이 존재할 경우 에러가 발생할 수 있다.
위 그림처럼 서로 다른 쓰레드에서 task1과 task2가 동시에 진행 하려 하면 에러가 발생합니다.
(task2의 작업 진행은 task1의 결과물에 의존하기 때문)
❗️비동기로 작동하는 함수의 작업 결과를 의존/사용하는 다른 함수가 존재할 때는? ⭐️
- 비동기로 작동하는 함수의 작업 결과를 의존하는 다른 함수가 존재할 경우 특정 함수의 작업 결과를 return하는 방식으로 설계해선 안된다.
- 비동기와 관련된 함수들은 모두 컴플리션핸들러를 가지고있다.
- 에러없는 작동을 위해 return하지 않고 (return형) -> (?)형태의 클로저를 호출할 수 있는 형태로 설계해야한다.
비동기 작업의 작업이 끝나는 시점을 파악하여 해당 작업의 결과를 클로저로 전달해야한다 !!
❌잘못된 함수의 설계
func getImages(with urlString: String) -> UIImage? {
let url = URL(string: urlString)!
var photoImage: UIImage? = nil
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let imageData = data else { return }
// 데이터를 UIImage 타입으로 변형
photoImage = UIImage(data: imageData)
}.resume()
return photoImage // 항상 nil 이 나옴
}
✔️ 이미지를 가져와서 리턴하는 함수
dataTask함수는 내부에 비동기처리가 되어있어서 비동기적으로 작동하는 함수이다.
비동기함수이므로 return photoImage는 dataTask함수의 종료까지 기다려주지 않고 return하게된다.
데이터를 불러오기전에 이미 return하게되므로 항상 nil을 리턴하게된다.
전체적인 함수의 실행은 dataTask(비동기함수)를 제외한 나머지의 코드는 동기적으로 실행된다.
또한 dataTask내부의 코드는 동기적으로 실행된다.
⭕️올바른 함수 설계
func getImages(with urlString: String, completionHandler: @escaping (UIImage?) -> Void){
let url = URL(string: urlString)!
var photoImage: UIImage? = nil
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let imageData = data else { return }
// 데이터를 UIImage 타입으로 변형
photoImage = UIImage(data: imageData)
completionHandler(photoImage)
}.resume()
}
파라미터인 completionHandler는 UIImage를 받아서 Void를 리턴하는 함수이다.
전체적인 코드를 봤을 때 dataTask함수 자체는 비동기적으로 실행된다. getImages 함수는 dataTask의 종료를 기다리지 않는다.
⭐️⭐️⭐️
하지만 getImages함수 내부의 dataTask함수의 내부는 동기적으로 실행된다. 클로저로 묶인 하나의 비동기 함수는 그 자체를 한개의 작업으로 처리한다. 함수 자체는 비동기이지만 내부의 작업은 동기적으로 처리된다.
그렇다면 photoImage = UIImage(data: imageData)가 처리가 되어야 completionHandler(photoImage)가 실행될것이다!!
위의 함수를 활용한 네트워크 처리 이후에 UI를 변경하는 코드
properlyGetImages(with: "이미지링크") { (image) in
// 처리 관련 코드 넣는 곳
DispatchQueue.main.async {
// UI관련작업의 처리는 여기서
}
}
스위프트 5.5에서의 비동기함수의 리턴이 가능한 Async/await의 도입되었다...
다른 글에서 정리!
❓weak, strong 캡처를 주의해야한다.
- 클로저로 작업을 할 때 객체에 대한 capture현상을 주의해야한다.
- 대부분의 경우 캡처리스트 안에서 weak self로 선언하여 약한참조를 하게끔 하는 것을 권장한다.
- 클로저 내부에서 특정 인스턴스를 캡쳐할 때 weak self를 사용한다.
대표적인 [weak self] 사용 예 ⭐️
1. 클로저 내부에서 self를 참조할 때
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
self.doSomething()
}
2. 비동기 처리
비동기 처리 클로저를 사용하여 비동기 처리를 하는 경우, 클로저 내부에서 self에 대한 강한 참조가 발생할 수 있습니다. 이 경우, 비동기 처리가 완료되기 전까지 self가 메모리에서 해제되지 않으므로, 메모리 누수가 발생합니다.
class ViewController: UIViewController {
func loadData() {
SomeAPI.getData { response in
// self에 대한 강한 참조 발생
self.updateUI(response)
}
}
}
위 코드에서는 클로저 내부에서 weak self를 사용하여 self를 강한 참조 대신 약한 참조로 선언합니다. 그리고 guard let 구문을 사용하여 self가 nil인 경우 클로저를 빠져나오도록 합니다. 이렇게 하면, 메모리 누수를 방지할 수 있습니다.
2. 델리게이트 패턴
델리게이트 패턴(delegate pattern)은 객체지향 디자인 패턴 중 하나로, 객체 간의 상호작용을 구현하는 데 사용됩니다. 델리게이트 패턴을 사용하는 경우, 델리게이트 객체가 자신을 참조하고 있는 객체를 참조하게 됩니다. 이 경우, 델리게이트 객체와 자신을 참조하고 있는 객체 간에 서로 강한 참조가 발생할 수 있으므로, weak self를 사용하여 메모리 누수를 방지해야 합니다.
protocol SomeDelegate: AnyObject {
func didReceiveData(_ data: String)
}
class SomeClass {
weak var delegate: SomeDelegate?
func loadData() {
SomeAPI.getData { [weak self] response in
guard let self = self else { return }
self.delegate?.didReceiveData(response)
}
}
}
❓동기함수를 비동기함수로 변형하는 방법
변형하는 이유
동기함수를 비동기적으로 동작하는 함수로 변형하는 함수를 생성하여 만들면 버벅임 등과 같은 현상을 에방할 수 있다.
1. Responsiveness(반응성) 향상
- 동기함수는 함수가 실행 중인 동안에는 해당 스레드에서 다른 작업을 수행할 수 없기 때문에, 긴 작업을 실행하는 경우에는 UI가 멈추는 등의 반응성 문제가 발생할 수 있습니다. 이러한 반응성 문제를 해결하기 위해서는 긴 작업을 비동기적으로 실행하고, 작업이 완료되면 콜백 함수를 호출하여 UI를 업데이트하는 방법이 필요합니다.
2. 성능향상
- 비동기 함수는 여러 개의 작업을 동시에 실행할 수 있기 때문에, 동기 함수보다 더 높은 성능을 발휘할 수 있습니다. 예를 들어, 네트워크 요청을 여러 개 동시에 보내는 경우에는 비동기 함수를 사용하여 병렬로 요청을 처리하면, 요청 처리 속도가 더욱 빨라질 수 있습니다. 따라서, Swift에서는 비동기 함수를 사용하여 긴 작업을 실행하고, UI를 멈추지 않도록 하며, 높은 성능을 발휘할 수 있도록 개발하는 것이 좋습니다.
동기함수
func myTask(programmingLanguage: String) -> String{
print("수학 숙제 시작")
sleep(2)
print("수학 숙제 종료")
print("\(programmingLanguage) 코딩 공부 시작")
sleep(2)
print("코딩 공부 종료")
print("영어 숙제 시작")
sleep(2)
print("영어 숙제 종료")
return "공부 종료"
}
myTask(programmingLanguage: "Swift")
동기함수를 비동기함수로 변형
func myTask(programmingLanguage: String) -> String{
print("수학 숙제 시작")
sleep(2)
print("수학 숙제 종료")
print("\(programmingLanguage) 코딩 공부 시작")
sleep(2)
print("코딩 공부 종료")
print("영어 숙제 시작")
sleep(2)
print("영어 숙제 종료")
return "공부 종료"
}
func asyncMyTask(programmingLanguage: String, completionHandler: @escaping (String) -> Void){
DispatchQueue.global().async {
let function = myTask(programmingLanguage: programmingLanguage)
completionHandler(function)
}
}
asyncMyTask(programmingLanguage: "Swift") { XXX in
print(XXX)
}
마지막 비동기 처리 한번 더 ❗️❗️❗️❗️❗️
print("출력 - 1")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let safeData = data else {
return
}
print(String(decoding: safeData, as: UTF8.self))
}.resume()
print("출력 - 2")
/*
출력 - 1
출력 - 2
{"boxOfficeResult":{"boxofficeType":"일별 박스오피스","showRange":"20210201~20210201",
"dailyBoxOfficeList":[{"rnum":"1","rank":"1","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20207443",
"movieNm":"소울","openDt":"2021-01-20","salesAmt
*/