비동기 태스크 - Team-HGD/SniffMEET GitHub Wiki
-
UIWindow 루트 뷰 컨트롤러 결정 - @MainActor
-
세션 복원 작업 - Background thread
displayInitialScreen - MainActor 속 함수에 breakpoint 걸어 확인
// AppRouter func displayInitialScreen() { Task { @MainActor in do { try await SupabaseAuthManager.shared.restoreSession() displayTabBar() } catch { displayOnBoardingView() } } }
-
displayOnBoardingView - MainActor 그대로 유지
-
-
프로필 입력 부분 - 전달만 하는 역할이라 비동기 x
-
프로필 등록 부분 (Interactor)
-
이미지 가져와 placeholer 교체 - UI에 해당하기 때문에 - @MainActor loadImage 부분 클로저 형태로 진행되니 async await 사용해 통일성 있게 개선해도 될 듯
// ProfileCreateViewController if let itemProvider = itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) { itemProvider.loadObject(ofClass: UIImage.self) { image, error in guard let selectedImage = image as? UIImage else { return } Task { @MainActor [weak self] in self?.profileImageView.image = selectedImage } } }
-
등록
- 4개의 태스크가 전부 하나의 스레드에서 적용
- 딜레이 발생 (멀티스레드 x)
- 내용 간 순서 유지 할 필요 있음
- 태스크 조각내어도 되지 않을까? - 태스크 그룹 만들어 2개 로 분리
- 등록 다음 화면이 보여지는 부분을 2개 중 어느 스레드에서 진행할지?
-
두 스레드 모두 실행되고 나서 saveUserInfo 호출
-
// ProfileCreateInteractor func signInWithProfileData(dogInfo: UserInfo, imageData: (png: Data?, jpg: Data?)) { Task { do { // Task 1 await SupabaseAuthManager.shared.signInAnonymously() try saveUserInfoUseCase.execute(dog: UserInfo( name: dogInfo.name, age: dogInfo.age, sex: dogInfo.sex, sexUponIntake: dogInfo.sexUponIntake, size: dogInfo.size, keywords: dogInfo.keywords, nickname: dogInfo.nickname, profileImage: imageData.png) ) /// /// Task 2 var fileName: String? = nil if let jpgData = imageData.jpg { fileName = try await saveProfileImageUseCase.execute( imageData: jpgData ) } guard let userID = SessionManager.shared.session?.user?.userID else { return } await saveUserInfoRemoteUseCase.execute( info: UserInfoDTO( id: userID, dogName: dogInfo.name, age: dogInfo.age, sex: dogInfo.sex, sexUponIntake: dogInfo.sexUponIntake, size: dogInfo.size, keywords: dogInfo.keywords, nickname: dogInfo.nickname, profileImageURL: fileName ) ) /// presenter?.didSaveUserInfo() } catch { presenter?.didFailToSaveUserInfo(error: error) } } }
- 4개의 태스크가 전부 하나의 스레드에서 적용
-
-
홈
- loadInfo, saveDeviceToken - 유지
- profileCardView에서 이미지 로딩 - 현재 로컬에서 로드하는 부분이라 task가 따로 없음
- 로컬에서 가져와도 비동기 처리 하는것이 좋아 보임
// LoadUserInfoUseCase // 현재는 비동기 처리하지 않음. func execute() throws -> UserInfo { var userInfo = try dataLoadable.loadData(forKey: Environment.UserDefaultsKey.dogInfo, type: UserInfo.self) userInfo.profileImage = try imageManageable.image(forKey: Environment.FileManagerKey.profileImage) return userInfo }
*수파베이스 세션매니저에 userID 바로 확인가능하도록 추가 수정해도 좋을 듯
guard let userID = SessionManager.shared.session?.user?.userID else {
return
}
-
정보 수정
- 프로필 등록 완료와 거의 동일한 방식으로 구현되어있어 유사한 방식으로 개선 가능
-
메이트목록
- requestMateList - 현재 MainActor
- alert.show를 위해 UI와 연동 필요한 이유일 수도 ?
- 확인 결과 MainActor가 없어도 잘 동작함
- requestProfileImage도 동일하게 MainActor 없어도 동작
- output.mate 부분에서 reloadData가 존재하는데
- 그럼 여기서 MainActor 적용해도될듯?
또는 아예 MainActor 없어도 되려나? → 스레드 문제 발생
// MateListInteractor // View에서 이미 Main Thread로 전환하고 있기 때문에 @MainActor가 필요하지 않음 func requestMateList(userID: UUID) { Task { @MainActor in let mateList = await requestMateListUseCase.execute() presenter?.didFetchMateList(mateList: mateList) } } func requestProfileImage(id: UUID, imageName: String?) { Task { @MainActor in let imageData = try await requestProfileImageUseCase.execute(fileName: imageName ?? "") presenter?.didFetchProfileImage(id: id, imageData: imageData) } }
- requestMateList - 현재 MainActor
-
메이트 목록 - 이미지 fetch
-
테이블 뷰 각 셀이 로드될 때 각각 요청하고 있어 이미지 로드 시간차 발생 (이미지 요청 → 셀구성 반복)
- 요청을 전부 보내고 로드를 한번에 하는 방식으로 개선
- 이미지를 요청하는 역할 수행 타입을 따로 둬야하나?
- 결론 : 한번에 요청해두고 셀 알아서 그리고 이미지 응답이 오면 셀 리로드 발생(이미 컴바인 존재)
// MateListViewController func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: Identifier.mateCellID, for: indexPath ) guard let mate = presenter?.output.mates.value[indexPath.row] else { return cell } var content = cell.defaultContentConfiguration() content.image = .app if let imageData = imageDataSource[mate.userID] { var profileImage = UIImage(data: imageData) profileImage = profileImage?.clipToSquareWithBackgroundColor( with: ItemSize.profileImageSize.width) content.image = profileImage } else { presenter?.didTableViewCellLoad( mateID: mate.userID, imageName: mate.profileImageURLString ) } content.imageProperties.maximumSize = ItemSize.profileImageSize content.imageProperties.cornerRadius = ItemSize.profileImageCornerRadius content.text = presenter?.output.mates.value[indexPath.row].name cell.contentConfiguration = content if let mate = presenter?.output.mates.value[indexPath.row] { cell.accessoryView = createAccessoryButton(mate: mate) } cell.selectionStyle = .none return cell }
-
-
메이트 요청
- didTabAcceptButton - saveMateInfo - 유스케이스 실행을 위해 Task 존재
- Combine 내부에 Task 존재.. 허거덩스럽다 허거덩화이팅!
Interactor에서 태스크 묶으면 Save 되었음을 알리는 메서드가 필요해보임- Presenter에서 태스크 묶어서 실행되도록 수정해 View에서 Task 제거
// View acceptButton.publisher(event: .touchUpInside) .sink { [weak self] in Task { await self?.presenter?.didTapAcceptButton(id: self?.profile.id ?? DogProfileDTO.example.id) self?.presenter?.closeTheView() } } .store(in: &cancellables) // RequestMatePresenter func closeTheView() { if let view { router?.dismissView(view: view) } } func didTapAcceptButton(id: UUID) async { SNMLogger.info("id: \(id)") await interactor?.saveMateInfo(id: id) }
- didTabAcceptButton - saveMateInfo - 유스케이스 실행을 위해 Task 존재
-
산책 응답
-
RespondWalkPresenter
- 산책 요청은 Task로 Interactor에 존재
- 위치 변환은 Presenter에서 await
-
Interactor에서 태스크 제거하고 async 사용하는것으로 수정 필요 - convertLactaionToText
func convertLocationToText(latitude: Double, longtitude: Double) async { Task { let locationText: String? = await convertLocationToTextUseCase.execute( latitude: latitude, longtitude: longtitude ) presenter?.didConvertLocationToText(with: locationText) } }
-
didFetchUserInfo - walkRequest 생성 부분은 밖으로 옮겨도 가능 didFetch~ 부분이 메인이 아닌 곳에서 실행되는데, showRequestDetail은 UI 작업이라 메인에서 돌아가야 하기 때문에 @MainActor 붙었다. → 하지만 파악 힘든 부분 있어 개선 필요해보임
func didFetchUserInfo(senderInfo: UserInfoDTO) { Task { @MainActor [weak self] in guard let self else { return } let walkRequest = WalkRequestDetail(mate: senderInfo.toEntity(), address: Address(longtitude: self.noti.longtitude, latitude: self.noti.latitude), message: self.noti.message) self.view?.showRequestDetail(request: walkRequest) } }
-
RespondWalkInteractor - 유지 (순서 중요)
-
respondWalkRequest - walkNoti 까지는 밖으로 분리 가능
func respondWalkRequest(isAccepted: Bool, receivedNoti: WalkNoti) { let walkNotiCategory: WalkNotiCategory = isAccepted ? .walkAccepted : .walkDeclined Task { do { guard let date = receivedNoti.createdAt?.convertDateToISO8601String(), let userID = SessionManager.shared.session?.user?.userID else { return } let userInfo = try loadUserUseCase.execute() let walkNoti = WalkNotiDTO(id: UUID(), createdAt: date, message: receivedNoti.message, latitude: receivedNoti.latitude, longtitude: receivedNoti.longtitude, senderId: userID, receiverId: receivedNoti.senderId, senderName: userInfo.name, category: walkNotiCategory) // 여기까지 분리 try await respondWalkRequestUseCase.execute(requestID: receivedNoti.id, walkNoti: walkNoti) } catch { presenter?.didFailToSendWalkRequest(error: error) } } presenter?.didSendWalkRespond() }
-
Rounter - 유지!
-
이후 따른 함수의 Task 부분 수정 필요 → Interactor에서는 Task 분리
func convertLocationToText(latitude: Double, longtitude: Double) async { Task { let locationText: String? = await convertLocationToTextUseCase.execute( latitude: latitude, longtitude: longtitude ) presenter?.didConvertLocationToText(with: locationText) } } func fetchProfileImage(urlString: String) { Task { [weak self] in let imageData = try await self?.requestProfileImageUseCase.execute(fileName: urlString) self?.presenter?.didFetchProfileImage(with: imageData) } }
-
-
-
산책 요청
-
전체적인 유지
-
View
-
textViewDidBeginEditing
- 산책 보내기 요청시 텍스트뷰 누르면 키보드가 올라가는데, 이때 스크롤을 사용자에게 맞게 변경하기 위한 부분으로, 키보드가 올라가는 것 파악을 위해 딜레이 주기 위해 Task.sleep 사용
- 비동기 / 동기 적인 것보다는 sleep을 위한 Task 이다.
- 그렇다면 sleep이 main에서 동작하는 것인가?
- breakpoint 확인 결과 Main!! 아니었다..??
- scrollRectToVisible이 UIScroll 하위라 Main에서 동작되도록 되어있다. sleep이 메인에서 발생하는 것은 아니지만, scroll~으로 인해 메인스레드로 넘어가진다.
- 그렇다면 sleep이 main에서 동작하는 것인가?
- 진원님의 Combine 사용 방식을 이용해 개선..?? → 가능할지도 ! trytry~ 고진원~~ㄴ
func textViewDidBeginEditing(_ textView: UITextView) { Task {[weak self] in try? await Task.sleep(nanoseconds: 10_000_000) // 50ms (0.05초) self?.scrollView.scrollRectToVisible(textView.frame, animated: true) } if textView.text == Context.messagePlaceholder { textView.text = nil textView.textColor = .black textViewEdited = true } }
-
submitButton 바인딩 부분 - 이벤트 자체가 메인으로 볼 수 있으니 debounce - RunLoop.main
- 테스트 필요한 부분!!
submitButton.publisher(event: .touchUpInside) .debounce(for: .seconds(EventConstant.debounceInterval), scheduler: RunLoop.main) .sink { [weak self] _ in guard let message = self?.messageTextView.text, let latitude = self?.address?.latitude, let longtitude = self?.address?.longtitude, let location = self?.address?.location else { return } self?.presenter?.requestWalk(message: message, latitude: latitude, longtitude: longtitude, location: location) } .store(in: &cancellables)
-
-
Presenter - 없다~~
-
Interactor
- requestProfileImage - MainActor 제거하고 async로 변경. Task는 Presenter 딴으로 이동
func requestProfileImage(imageName: String?) { Task { @MainActor in let fileName = mate.profileImageURLString ?? "" let imageData = try await requestProfileImageUseCase.execute(fileName: fileName) presenter?.didFetchProfileImage(imageData: imageData) } }
-
-
산책 장소 선택 뷰
- View, Presenter - 전체적으로 동일한 개선 사항들만 존재
-
산책 답변에 대한 뷰
- Presenter - didFetchUserInfo - @MainActor 유지
- Interactor - 유지!!!
- convertLocationToText 여기에는 async 빠져있음 - 응답과 동일하게 개선
-
Supabase - 전체적으로 do-catch 확인 필요 - 확인 완료~문제업스!
-
로컬 네트워크
-
MPCManager
- MainActor 제거 테스트 필요
- receive 부분에도 동일하게 MainActor 제거 테스트
- 이때는 바인딩에서 RunLoop 추가해줘야함
if let tokenData = receivedData.token { Task { @MainActor in receivedTokenPublisher.send(tokenData) } } else if let profile = receivedData.profile { Task { @MainActor in receivedDataPublisher.send(profile) } } else if let message = receivedData.transitionMessage { Task { @MainActor in receivedViewTransitionPublisher.send(message) } }
-
NIManager
- RunLoop.main을 써서 받아야 할지 같이 생각해보기
- 거리 방향 조건 만족시 - MainActor 꼭 필요할까?
-
-
Cache
- FileManager 부분에서 처리 필요해 보인다. → 동일 주소에 동일하게 접근하는 경우 방지 위해
- actor 사용해 개선 가능해보임
- 또는 queue
- FileManager 부분에서 처리 필요해 보인다. → 동일 주소에 동일하게 접근하는 경우 방지 위해
-
유스케이스
- CreateAccount
- 순차적으로 진행되는 부분이 do-catch로 잔뜩 존재
- 태스크 그룹으로 나눠 개선하면 좋을 듯
- 순차적으로 진행되는 부분이 do-catch로 잔뜩 존재
// CreateAccountUseCase struct CreateAccountUseCaseImpl: CreateAccountUseCase { // RLS 정책은 ID 기반으로 인증이 됩니다. 따라서 info에 id 정보가 필요합니다. func execute(info: UserInfoDTO) async { let encoder = JSONEncoder() do { let userData = try encoder.encode(info) try await SupabaseDatabaseManager.shared.insertData( into: Environment.SupabaseTableName.userInfo, with: userData ) } catch { SNMLogger.error("\(error.localizedDescription)") } do { let mateListData = try encoder.encode(MateListInsertDTO(id: info.id)) try await SupabaseDatabaseManager.shared.insertData( into: Environment.SupabaseTableName.matelist, with: mateListData ) } catch { SNMLogger.error("mate list insert error: \(error.localizedDescription)") } do { let notiListData = try encoder.encode(WalkNotiListInsertDTO(id: info.id)) try await SupabaseDatabaseManager.shared.insertData( into: Environment.SupabaseTableName.notificationList, with: notiListData ) } catch { SNMLogger.error("notifiaction list insert error: \(error.localizedDescription)") } } }
- CreateAccount
- RespondWalkRequestUseCase
- 태스크 지우기
func execute(requestID: UUID, walkNoti: WalkNotiDTO) async throws {
guard let requestData = try? encoder.encode(walkNoti) else { return }
let request = try PushNotificationRequest.sendWalkRespond(data: requestData).urlRequest()
let _ = try await session.data(for: request)
// MARK: - walk-request 테이블 업데이트
var tableData: WalkRequestUpdateDTO?
switch walkNoti.category {
case .walkAccepted:
tableData = WalkRequestUpdateDTO(state: .accepted)
case .walkDeclined:
tableData = WalkRequestUpdateDTO(state: .declined)
default:
break
}
guard let tableData else { return }
let data = try JSONEncoder().encode(tableData)
Task {
try await remoteDatabaseManager.updateData(
into: Environment.SupabaseTableName.walkRequest,
at: requestID,
with: data)
}
}
동일하게 개선 필요
- 태스크 어디서 묶음?
- Presenter: 여기서 조건 분기 일어나는 방향으로 (throw 처리)
- Router를 MainActor로 지정 → 추후 수정 여부 결정
- 전체적인 수정이 필요해짐 (await)
- 바인딩에서 receive RunLoop.main이 필요없는 부분도 있어보임
- MainActor 사용하는 것과 동일하기에 확인 필요해보임
- UI 작업이 아닐수도 있음