비동기 태스크 - Team-HGD/SniffMEET GitHub Wiki

  1. UIWindow 루트 뷰 컨트롤러 결정 - @MainActor

    1. 세션 복원 작업 - Background thread

      displayInitialScreen - MainActor 속 함수에 breakpoint 걸어 확인

      // AppRouter
      
      func displayInitialScreen() {
              Task { @MainActor in
                  do {
                      try await SupabaseAuthManager.shared.restoreSession()
                      displayTabBar()
                  } catch {
                      displayOnBoardingView()
                  }
              }
          }
      
    2. displayOnBoardingView - MainActor 그대로 유지

  2. 프로필 입력 부분 - 전달만 하는 역할이라 비동기 x

  3. 프로필 등록 부분 (Interactor)

    1. 이미지 가져와 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
                      }
                  }
              }
      
    2. 등록

      1. 4개의 태스크가 전부 하나의 스레드에서 적용
        1. 딜레이 발생 (멀티스레드 x)
        2. 내용 간 순서 유지 할 필요 있음
      2. 태스크 조각내어도 되지 않을까? - 태스크 그룹 만들어 2개 로 분리
      3. 등록 다음 화면이 보여지는 부분을 2개 중 어느 스레드에서 진행할지?
        1. 두 스레드 모두 실행되고 나서 saveUserInfo 호출

          image

      // 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)
                  }
              }
          }
      
    1. loadInfo, saveDeviceToken - 유지
    2. profileCardView에서 이미지 로딩 - 현재 로컬에서 로드하는 부분이라 task가 따로 없음
      1. 로컬에서 가져와도 비동기 처리 하는것이 좋아 보임
    // 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 
}
  1. 정보 수정

    1. 프로필 등록 완료와 거의 동일한 방식으로 구현되어있어 유사한 방식으로 개선 가능
  2. 메이트목록

    1. requestMateList - 현재 MainActor
      1. alert.show를 위해 UI와 연동 필요한 이유일 수도 ?
      2. 확인 결과 MainActor가 없어도 잘 동작함
        1. requestProfileImage도 동일하게 MainActor 없어도 동작
      3. 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)
        }
    }
    
  3. 메이트 목록 - 이미지 fetch

    1. 테이블 뷰 각 셀이 로드될 때 각각 요청하고 있어 이미지 로드 시간차 발생 (이미지 요청 → 셀구성 반복)

      1. 요청을 전부 보내고 로드를 한번에 하는 방식으로 개선
      2. 이미지를 요청하는 역할 수행 타입을 따로 둬야하나?
      3. 결론 : 한번에 요청해두고 셀 알아서 그리고 이미지 응답이 오면 셀 리로드 발생(이미 컴바인 존재)
      // 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
      }
      
  4. 메이트 요청

    1. didTabAcceptButton - saveMateInfo - 유스케이스 실행을 위해 Task 존재
      1. Combine 내부에 Task 존재.. 허거덩스럽다 허거덩화이팅!
      2. Interactor에서 태스크 묶으면 Save 되었음을 알리는 메서드가 필요해보임
      3. 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)
    }
    
  5. 산책 응답

    1. RespondWalkPresenter

      1. 산책 요청은 Task로 Interactor에 존재
      2. 위치 변환은 Presenter에서 await
    2. 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)
          }
      }
      
    3. 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)
          }
      }
      
    4. RespondWalkInteractor - 유지 (순서 중요)

      1. 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()
            }
        
      2. Rounter - 유지!

      3. 이후 따른 함수의 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)
            }
        }
        
  6. 산책 요청

    1. 전체적인 유지

    2. View

      1. textViewDidBeginEditing

        1. 산책 보내기 요청시 텍스트뷰 누르면 키보드가 올라가는데, 이때 스크롤을 사용자에게 맞게 변경하기 위한 부분으로, 키보드가 올라가는 것 파악을 위해 딜레이 주기 위해 Task.sleep 사용
        2. 비동기 / 동기 적인 것보다는 sleep을 위한 Task 이다.
          1. 그렇다면 sleep이 main에서 동작하는 것인가?
            1. breakpoint 확인 결과 Main!! 아니었다..??
            2. scrollRectToVisible이 UIScroll 하위라 Main에서 동작되도록 되어있다. sleep이 메인에서 발생하는 것은 아니지만, scroll~으로 인해 메인스레드로 넘어가진다.
        3. 진원님의 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
            }
        }
        
      2. submitButton 바인딩 부분 - 이벤트 자체가 메인으로 볼 수 있으니 debounce - RunLoop.main

        1. 테스트 필요한 부분!!
      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)
      
    3. Presenter - 없다~~

    4. Interactor

      1. 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)
          }
      }
      
  7. 산책 장소 선택 뷰

    1. View, Presenter - 전체적으로 동일한 개선 사항들만 존재
  8. 산책 답변에 대한 뷰

    1. Presenter - didFetchUserInfo - @MainActor 유지
    2. Interactor - 유지!!!
      1. convertLocationToText 여기에는 async 빠져있음 - 응답과 동일하게 개선
  9. Supabase - 전체적으로 do-catch 확인 필요 - 확인 완료~문제업스!

  10. 로컬 네트워크

    1. MPCManager

      1. MainActor 제거 테스트 필요
      2. receive 부분에도 동일하게 MainActor 제거 테스트
        1. 이때는 바인딩에서 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)
          }
      }
      
    2. NIManager

      1. RunLoop.main을 써서 받아야 할지 같이 생각해보기
      2. 거리 방향 조건 만족시 - MainActor 꼭 필요할까?
  11. Cache

    1. FileManager 부분에서 처리 필요해 보인다. → 동일 주소에 동일하게 접근하는 경우 방지 위해
      1. actor 사용해 개선 가능해보임
      2. 또는 queue
  12. 유스케이스

    1. CreateAccount
      1. 순차적으로 진행되는 부분이 do-catch로 잔뜩 존재
        1. 태스크 그룹으로 나눠 개선하면 좋을 듯
    // 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)")
            }
        }
    }
    
  • 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 작업이 아닐수도 있음