SniffMEET. 닉네임 검증 개선 고민

작업 내역 닉네임 TextField의 검증 방식을 다시 정리했다. 현재는 UITextFieldDelegate로 길이 검증과 버튼 활성화를 처리하고 있었는데, 중복 체크까지 같은 흐름에 넣을 경우 네트워크 요청이 너무 자주 발생할 수 있어 보였다. 현재 구현 현재는 Delegate 기반으로 다음 두 가지를 처리하고 있었다. extension ProfileCreateViewController: UITextFieldDelegate { func textFieldDidChangeSelection(_ textField: UITextField) { guard let textCount = textField.text?.count else { return } submitButton.isEnabled = (textCount > 1 && textCount < 9 ) } func textField( _ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { guard let text = textField.text else { return false } let newLength = text.count + string.count - range.length let inputTextValid = newLength <= 15 return inputTextValid } } 길이 검증 자체는 단순했고, 현재 사용성도 나쁘지 않았다. 문제는 중복 체크처럼 네트워크 요청이 들어가는 검증까지 같은 방식으로 처리하기에는 부담이 커진다는 점이었다. ...

February 13, 2025

SniffMEET. 비동기 태스크와 액터 점검하기 (2)

메이트 요청 didTabAcceptButton과 saveMateInfo를 따라가 보니 유스케이스 실행을 위해 Task가 존재했고, Combine의 sink 클로저 안에서도 Task를 생성하고 있었다. Interactor에서 태스크를 묶으면 저장 완료를 알리는 메서드가 따로 필요해 보였다. Presenter 메서드는 async로 바꾸고, View의 sink에서는 Task를 통해 호출하도록 두었다. 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) } 산책 요청 및 응답 RespondWalkPresenter 산책 요청은 Task로 Interactor에 존재했고, 위치 변환은 Presenter에서 await로 처리했다. ...

January 8, 2025

SniffMEET. 비동기 태스크와 액터 점검하기 (1)

작업 내역 이번 작업에서는 Task, async/await, @MainActor가 섞여 있던 부분을 화면별로 점검했다. 앱 시작 흐름부터 프로필 등록, 메이트 목록, 산책 응답까지 비동기 경계를 다시 살펴보고, 각 화면에서 책임이 어디에 있어야 하는지 정리했다. 앱 시작 플로우 UIWindow의 루트 뷰 컨트롤러를 결정하는 흐름은 @MainActor로 두고, 그 안에서 세션 복원이 끝나기를 기다렸다. await로 대기하는 동안에는 메인 액터를 점유하지 않는다. displayInitialScreen이 MainActor 안에서 실행되는지 breakpoint로 확인했고, displayOnBoardingView는 그대로 MainActor에 남겨 두었다. // AppRouter.swift func displayInitialScreen() { Task { @MainActor in do { try await SupabaseAuthManager.shared.restoreSession() displayTabBar() } catch { displayOnBoardingView() } } } 프로필 입력 및 등록 프로필 입력 부분은 전달만 하는 역할이라 비동기 처리가 필요하지 않았다. ...

January 8, 2025

Combine. Combine in Practice - WWDC19

A unified, declarative API for processing values over time Combine에서는 값이 시간의 흐름에 따라 전달되는 과정을 Publisher로 표현하고, 여러 연산자를 연결해 데이터가 변환되는 흐름을 선언적으로 작성한다. 아래 예제에서는 Notification으로 전달된 Data를 MagicTrick으로 디코딩한다. let trickNamePublisher = NotificationCenter.default.publisher(for: .newTrickDownloaded) .map { notification in return notification.userInfo?["data"] as! Data } // Output: Data, Failure: Never .tryMap { data in let decoder = JSONDecoder() try decoder.decode(MagicTrick.self, from: data) } // Output: MagicTrick, Failure: Error decode 연산자를 사용하면 위 변환을 다음과 같이 줄여서 작성할 수 있다. let trickNamePublisher = NotificationCenter.default.publisher(for: .newTrickDownloaded) .map { notification in return notification.userInfo?["data"] as! Data } // Output: Data, Failure: Never .decode(MagicTrick.self, JSONDecoder()) // Output: MagicTrick, Failure: Error Error Handling 모든 Publisher는 자신이 발생시킬 수 있는 실패의 종류를 명확하게 정의한다. 실패가 발생하지 않거나 이미 처리된 경우에는 Never를 Failure 타입으로 사용한다. Combine은 실패를 감지하고 복구할 수 있는 다양한 연산자를 제공한다. Combine의 Publisher는 전달하는 값의 타입인 Output과 실패할 때 전달하는 오류의 타입인 Failure를 함께 가진다. 두 타입이 연산자를 거치며 어떻게 바뀌는지 확인하면 데이터 흐름과 오류 흐름을 함께 추적할 수 있다. ...

March 18, 2024

Combine. Introducing Combine - WWDC19

Combine Customize handling of asynchronous events by combining event-processing operators. 결합된 이벤트 처리 연산자를 이용하여 비동기 이벤트 처리를 커스터마이즈 하는 방법 Combine은 데이터 흐름을 간편하게 처리하고 비동기 이벤트를 관리하기 위한 프레임워크다. 다양한 소스에서 발생하는 이벤트를 같은 방식으로 다루고, 이벤트 사이의 상호작용을 연산자로 조합할 수 있다. 특징 Generic: 제네릭을 사용해 다양한 타입의 데이터 흐름을 표현한다. Type safe: Publisher의 출력 타입과 실패 타입을 컴파일 타임에 확인한다. Composition first: 작은 Publisher와 연산자를 조합해 더 큰 흐름을 만든다. Request driven: Subscriber가 필요한 만큼의 값을 요청하는 방식으로 흐름을 제어한다. 핵심 개념 Publisher 값과 에러가 어떻게 생성되는지를 정의한다. ...

March 17, 2024