전에 오픈마켓 프로젝트를 하면서 혹은 여러 메서드들을 사용하면서 completionHandler란 이름으로 closure를 참 많이도 사용 했지만 정작 내가 만들어 사용하려고 하면 그 메서드 내에서 어떻게 작동하는지를 몰라 사용을 하지 못했었다
(사용 하더라도 어떻게 작동되는지를 모르고 막 사용한듯)
그러다가 곰튀김님의 RxSwift 강의를 보던 중 영상 초반 부에 "RxSwift를 쓰지 않고 기존에는 이런 식으로 비동기 작업을 처리 했었죠...? (어쩌고)" 하는 부분이 있었는데 갑자기 깨달음을 얻어버려서 잠시 공부를 멈추고 내 프로젝트에 적용 및 기록을 남기려고 한다
기존 유저 검색시 서버로 부터 유저 정보를 받아오는 시간 동안 사용자는 어플이 멈춘건지, 뭐 정보를 받아오는데 오래 걸리는건지, 아니면 검색 버튼을 깜빡하고 안눌러서 검색 시작조차 되지 않은 건지 알 수 있는 방법이 없었다
그래서 그 과정에 Activity Indicator
를 보여줘서 검색 중이라는 것을 알리고 싶었다
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
myActivityIndicator.isHidden = false // 로딩 시작
self.view.endEditing(true)
guard let userName = searchBar.text else { return }
do {
let info = try CrawlManager.shared.searchUser(userName: userName)
if self.vcType == .searchCharacter {
moveToUserInfoVC(info: info)
} else {
showAlert(title: "\(info.basicInfo.name) LV.\(info.basicInfo.itemLevel)(\(info.basicInfo.class))", message: "대표 캐릭터를 설정 하시겠습니까?", userName: userName)
}
} catch {
switch error {
case CrawlError.inspection:
showInspectionAlert()
default:
showAlert(title: "", message: "검색하신 유저가 없습니다", userName: nil)
}
}
myActivityIndicator.isHidden = true // 로딩 끝
}
그래서 위와 같이 로딩을 알리는 코드를 작성해주었는데 결과는 처음과 같았다
그 이유를 생각해봤는데 로딩중 보여주기
, 유저 검색하기
, 로딩중 hidden시키기
이 세 가지의 기능들이 동기적으로 진행되고 있는데 로딩을 보여주긴 하나 유저를 검색하는 동안 뷰는 멈출 것이고 유저를 검색해 다음으로 넘어가면 로딩은 hidden될 것이기 때문에 결과적으로는 로딩중이라는 것이 보이지 않는 것이다(아닐 수도..!)
암튼 그래서 유저 검색하기
기능을 DispatchQueue를 이용해 비동기적으로 실행을 시키고 나머지 뷰에 표시하는 걸 main에서 실행시키면 될 것이라 생각하고 다시 작성을 해봤다
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
myActivityIndicator.isHidden = false
self.view.endEditing(true)
guard let userName = searchBar.text else { return }
DispatchQueue.global().async { // 비동기적으로 작동하도록
do {
let info = try CrawlManager.shared.searchUser(userName: userName)
DispatchQueue.main.async { //view 업데이트는 main에서
if self.vcType == .searchCharacter {
self.moveToUserInfoVC(info: info)
} else {
self.showAlert(title: "\(info.basicInfo.name) LV.\(info.basicInfo.itemLevel)(\(info.basicInfo.class))", message: "대표 캐릭터를 설정 하시겠습니까?", userName: userName)
}
self.myActivityIndicator.isHidden = true
}
} catch {
DispatchQueue.main.async { //view 업데이트는 main에서
switch error {
case CrawlError.inspection:
self.showInspectionAlert()
default:
self.showAlert(title: "", message: "검색하신 유저가 없습니다", userName: nil)
}
self.myActivityIndicator.isHidden = true
}
}
}
}
위와 같이 작성 했고 결과는
이렇게 로딩중인 것을 잘 확인 할 수 있었다...만!
코드가 너무 지저분하고 복잡해서 알아보기가 너무 힘들었으며 어디를 main에서 실행해야할지 찾기가 쉽지 않았다
일단 개선 코드를 작성하기 전에 closure
를 사용하는 방법부터 기록해보려고 한다
일단 testClosure
라는 메서드는 String -> Void 클로저
를 매개변수로 받는 메서드이고 그 클로저에게 "안녕" 이라는 String을 전달해준다
(클로저를 매개변수로 받는 메서드라는 말이 적절한지는 모르겠지만 어쨌든...?)
그래서 testClosure
를 호출하게 되면 클로저를 작성하고 작성되는 클로저는 string(여기서는 "안녕")을 받아 그걸로 뭔가를 하게 된다(여기서는 print를 함)
아무튼 앞으로 내가 할 일은 유저를 검색한 뒤 그 유저를 closure롤 넘겨주는 일을 해줄 것인데 이를 비동기적으로 처리해주기 위한 @escaping
키워드까지 알아볼 예정이다
이 어플에서 유저 검색은 100% 유저의 이름을 통해 검색 되기 때문에 String을 extension해서 구현했음
extension String {
func crawlUser(_ completion: @escaping (Result<UserInfo, Error>) -> Void) {
DispatchQueue.global().async {
do {
let info = try CrawlManager.shared.searchUser(userName: self)
DispatchQueue.main.async {
completion(.success(info))
}
} catch(let error) {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
}
위 @escaping 키워드를 제외하고 차근차근 보자면 CrawlManager.shared.searchUser(userName: self)
라는 메서드가 가져오는 data
를 전달하는 것이 주 목표 이다
근데 그 메서드는 에러를 던지는 함수이기 때문데 클로저에게 전달해줄 타입은 Result타입으로 성공시 UserInfo
타입을 실패시 Error
를 전달해주도록 했다
검색한 유저의 정보를 가져오는 기능은 비동기적으로 이루어져야 하기 때문에 DispatchQueue.global().async
내부에 작성을 하였고 가져온 정보(info
) 혹은 error
를 처리 하는 부분은 대부분 view와 관련될 내용이기도 할 뿐더러 그 결과를 처리하는 부분은 비동기적으로 진행될 필요가 없기에 main
에서 작동될 수 있도록 작성 하였다
@escaping
은?유저를 검색하는 코드는 서버에서 정보를 받아오는 시간이 있기 때문에 다른 스레드에서 비동기적으로 처리되게 하였다
그래서 그 값을 받아오는 동안은 그 뒤의 작업들이 진행될 것인데 (뒤의 작업이라고 하면 crawlUser
를 호출 하는 곳에서 crawlUser
를 제외한 다른 작업들)
그 작업들을 하다가도 유저 정보를 받아오는 것이 완료가 되었다면 그 정보를 가지고 Completion
이 실행 될 수 있도록 추적 해주는 역할을 한다
즉, "니가 요청한 data를 받아오면 원하는 작업 할 수 있도록 알아서 추적할게! 넌 다른거하삼~"
이런 느낌이랄까...
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
myActivityIndicator.isHidden = false //유저 검색 시작시 indicator 노출
self.view.endEditing(true)
guard let userName = searchBar.text else {
return
}
userName.crawlUser { info in
self.myActivityIndicator.isHidden = true
// crawlUser가 info를 받아오거나 error를 던질 때 indicator 숨기기
switch info {
case .success(let info):
self.showUser(info)
case .failure(let error):
self.handleError(error)
}
}
}
그래서 위와 같이 코드를 작성 해 userName
에서 바로 crawlUser
를 호출 할 수 있게 하였고 info를 받아오거나 error를 던질 때
indicator 숨기도록 작성 하였다
(성공 했을 때 info를 가지고 유저정보를 보여주는 기능
과 실패 했을 때 error를 통해 적절한 alert을 띄워주는 기능
을 각각의 메서드로 분리해주었다)
참고자료
https://www.youtube.com/watch?v=iHKBNYMWd5I&ab_channel=%EA%B3%B0%ED%8A%80%EA%B9%80