SHcommit / LearnMoreSwiftInUdemy

0 stars 0 forks source link

[Clone/Instagram] Notification 기능 개발 느낀점, 고민거리, Coordinator 패턴 적용 #15 #30

Open SHcommit opened 1 year ago

SHcommit commented 1 year ago

목표: 누군가 피드에서 팔로우나 커맨트, 좋아요 했을 때 Notificaiton Controller에서 알림을 뜨게 하는 것!

특정 사용자에게 알림을 업로드 해야한다.

A 계정으로 로그인 했을 때 B의 포스트에 하트 누르면 B의 notificaiton을 해서 알림 목록 리스트에 추가 해야한다.

B는 앱을 열고 알림창을 살펴봤을 때 A의 알림이 있어야 한다.

  1. 특정 사용자에게 알림 타입을 업로드 해야한다.

구현 영상

추가적으로,, 서버에 올릴 때 Timestamp에 seconds로 저장했는데 다시 시 분 초로 바꿔서 시간을 업데이트 해야한다.

그리고 사용자가 앱을 나가있을 때도 알림을 fcm푸시를 구현해봐야겠다.

새로 알게 된 개념

단순히 호출만하는게 아니라 인디케이터 뷰가 나타나면 실행중인 동안에는 다른 뷰들의 상호작용을 차단해야 좋다.

그래서 인디케이터 시작 할 때 view.isUserInteractionEnalbled = false

-> 인디케이터 끝 시 view.isUserInteractionEnabled = true 로 대처했다.

separator는 cell 과 cell사이의 선을 말함. separator inset 는 leadingAnchor에 대한 여백

notification #1

Data(contentsOf:)보다 URLSession와 같은 async network api를 사용하라고 한다.

URLSession.shared.dataTask(with: url) { (data, response, err) ... }.resume()

마주한 오류

modern concurrency + combine을 사용할 때 주의사항..

ViewController의 state에 따른 Input을 ViewModel의 transform(_:)-> Output으로 로직 처리 후 다시 뷰 컨트롤러으 State를 업데이트 할 수 있도록 갱신해 주는 코드에서 refresh라는 이벤트가 발생되면 vm의 input으로 들어간다. 그리고 로직을 처리한다.

여기오류1

이때 기존 저장된 posts 배열에 담겨있는 포스트 정보를 모두 삭제한다. 그리고 fetchPosts를 통해 새로 갱신된 포스트 정보들을 받아온다. 그 후 .reloadData로 반환한다.

얼핏 보면 정상적인 코드 같지만... fetchPosts를 보면

여기에 문제가 있음 Task

Task를 통해 async함수인 apiClient.postCase.fetchPosts()를 비동기적으로 다운받는다. fetchPosts() 함수는 종료된다. 그리고 .reloadData state로 반환되어 ViewController의 State에서 데이터를 리 로드하라고 곧바로 전달한다. 하지만 async 함수는 await를 통해 여러번 실행, 중지 작업을 통해 포스트를 받아옴으로 새로 데이터를 갱신하기 위해 테이블 뷰.reloadData를 실행해도 async apiClient.postCase.fetchPosts()는 실행이 완료되지 않는다. 이 점을 조심해야 한다.

바인딩 중첩.

cell안에서 바인딩을 할 경우 바인딩이 중첩된다. prepareforReuse를 통해 재사용 큐에 저장했다가 특정 cell이 화면에 노출된다면 다시 꺼내 쓰는데 꺼낼때마다 바인딩 된다.

noti에러3 81e029b10819107)

그럼

NotificationCellViewModel의 transform(with:)가 재사용으로 꺼내질 때마다 바인딩이 되서 누적 바인드 된 만큼의 같은 value를 emit한다.

그래서 cell의 경우 prepareForReuse()에서 subscription들을 전부 cancel한 후에 다시 바인딩을 해주었다.

subscriptions을 holding한다고 해도 dismiss가 되는 경우가 아니면 중첩해서 쌓일 수 있어서 임의적으로 cancel과 재 할당 등을 통해 관리하는게 중요하단느 것을 알게 되었다.

최소한으로 바인딩 되는 경로를 줄였음에도 그래도 두 번씩 실행이 된다. 중복 호출해주는 구간이 없어서 그런데 이전에 있던 cell의 위치와 재사용큐에서 바로 꺼내오기 때문에 transform(with:)가 두 번 호출되는지 모르겠다. 정말 최소한의 호출인 것 같다. 여기서 operator를 사용하 단 한번만 사용하기로 했다. 나중에 문제 발생시 first()를 삭제해야겠다.


https://dev-with-precious-dreams.tistory.com/168


passthroughSubject를 통해 delegate를 만들었는데 한번의 action event임에도 불구하고 두개씩 published되는 이유 NotificationController.swift 관련

결론적으로 두 번 send -> sink된 것이기 때문이다...

저번에도 한번 컴바인으로 delegate 쓸 때 호출 가능한 경우를 최소한으로 줄였는데도 불구하고 두 번씩 호출 됐다. 계속해서 건드려봐도 안되서 어쩔 수 없이 first() operator를 사용해서 넘겼는데 이번에도 또 같은 상황이 발생됬다. 고치기 위해 안간힘을 썼는데도 계속 두 개씩 호출됬다.

상황 // Date: 1.13

  1. cell의 object 터치 -> addTarget 메소드 한번 호출. 여기서 send로 currentValueSubject에게 데이터 전달하는데 두 번 전달하게 된다.

에6

일단 cell안에 버튼과 프로필 이미지 뷰, 포스트 이미지 뷰 세 개가 있고 그 중 두 개의 액션 메서드다.

중요한 건 버튼, 프로필 이미지 한 번 클릭하면 분명 액션 메서드는 한번 동작한다. 그런데 subject의 sink를 두 번 받는다. 확인해본 결과 send를 두 번 한다. 바인딩이 두 번 됬다는 경우다.

에7

이건 delegate를 받는쪽

cell에도 vm을 정의했다. 추후 Advanced architecure로 VM의 예를 정확히 봐야겠다.. ( Cell 또한 view인데 viewModel을 사용해도 괜찮은지.. 궁금하다)

원인을 찾지 못해 iOS개발 단톡방에 도움을 요청했는데 다행히 한분이 답해 주셨다.. "재사용 큐에서 꺼냈을때도 subscriptions 초기화를 했는지??!!?"

에9

물론 했었다... 그럼에도 두 번씩 호출됬다. 결론적으로 어쨌든 두번씩 sink를 한 것은 바인딩을 두 번 했다는 의미로 결론을 내렸다. cell<-> cellVM , ViewController<->vm 코드 여기저기서 subscriptions를 유지하느라 바쁘다...

NotificationsController에도 vm과의 바인딩, NotificationCell 도 cellVM과의 바인딩, 게다가 cell과 notificationController의 델리게이트 간 바인딩 즉 subscriptions가 두 개 정도 있었는데 이중에서 delegate바인딩에 관한 subscriptions를 notificationController의 subscriptions 변수에서 찾기 힘들었다. 왜냐하면 Set으로 저장했기 때문이다.

상황 설명을 글로 남기기 애매한데 결과적으로 내가 잘못한 점은 단톡방에서 개발자 분이 해준 말이 맞다. 재사용큐에서 꺼내올 때 cell의 cellVM은 초기화를 했지만 NotificationController의 subscriptions는 초기화를 하지 않았다. 결론적으로 재사용 큐에서 꺼낼 때 초기화를 완전히 하지 못한 것이 된다.(cell의 subscriptions는 초기화 했었는데 델리게이트 관련 subscription을 NotificationCeneter에서 해주지 못한 내 잘못..ㅠ)

재사용큐 -> setupNotificationCellDelegate(_:)는 매번 sink를 통해 바인딩 되는데 이때 subscriptions를 초기화 해주지 않은 것이다.

에10

바인딩을 이곳 저곳에서 하다 보니까 prepare를 통해 cell은 진작에 subscriptions 초기화 했지만 delegate관련은 해주지 못했던 것이다. ㅋㅋ,, 근데 심지어 cancellables는 Set에 저장시켜서 delegate관련 publihser의 cancellable는 찾기 힘들었다.

에11

그래서 대안책으로 프로퍼티 하나 추가했다. 그리고 변수 명을 분명하게 했다. 그렇지 않으면 죄다 subsciriptions여서 정말 했갈린다. 좋은 경험을 한 것 같다.

에12

그리고 재사용큐에서 setupNotificationCellDelegate함수가 호출될 때마다 delegateSubscription을 새로운 send로부터 holding하는 것으로 바꿔서 새로 delegate가 호출되면 다시 delegateSubscription이 새로운 거로 덮어지도록 함으로 에러를 고쳤다.

에13

그리고 다시 화면 나타날때, reloadTableView로 테이블 뷰 데이터들 다시 갱신될 때도 해주었다 거의 모든 상황에 대비를 했다고 할 수 있다!! 후..

고민거리..

초기화 관련..

클로는 capture 성격이다. 특히 lazy var 를 남용할 경우 좋지 않지만 viewController시점에선 view가 load되기 이전에 addTarget을 통해 메서드를 등록할 경우 해당 메서드가 적용이 안되서 lazy var 키워들르 사용한다.

반면 함수는 호출 되어야 사용된다. 그리고 종료시점이 분명하다(async를 쓰지않는한)

좀 더 연구해봐야한다.

swift 공식문서에는 클로저로 변수 초기화가 없다. 다 선언 후 init메서드 안에서 초기화를 한다. 하지만 유명한 책에서도 클로저를 통해 초기화를 사용한다.

초기화

원래의 경우 클래스의 상단부에 변수 선언 + 초기화, init메서드 안 초기화. 클래스 상단부에 선언하자마자 바로 함수를 사용해서 초기화를 함으로 클래스 상단부를 간략하게 해서 변수들을 보기 쉽게 하려고 했다.그런데 함수를 통해 init() 메서드가 시작이 안된, 초기화가 되지 않은 변수를 초기화 하기 위해서는 조건이 있다.

전역 함수인가? or 해당 클래스 내부의 함수인가? 이때 해당 클래스 내부의 함수인 경우 init()메서드를 통해 클래스가 초기화 되기 이전 이라면 클래스 내 구현했던 함수들은 사용하지 못한다.(클래스가 초기화 되지 않았기 때문,.,.)

왜?

A 클래스 내부의 변수들을 초기화 하기 위해 A 클래스 내부에 특정 프로퍼티에 대한 초기화 함수를 구현했지만 이 초기화 전용 함수들은 A 객체가 메모리에 할당 되어야만 사용할 수 있기 때문이다.

그래서 난 static 키워드를 통해서 함수를 선언했다. Static을 사용한다면 A 프로퍼티의 초기화가 없어도 사용할 수있기 때문이다. Static키워드를 사용하면 object's 메모리에 곧바로 할당되고 A 인스턴스 없이도 바로 이용할 수 있게 만들어주기 때문이다.

Static 키워드는 네트워크 파싱에 필요한 함수 등을 클래스 안에 구조화 해서 선언할 때 인스턴스를 생성하지 않고도 바로 사용할 수있도록 Static 키워드를 선언해서 사용했다. (Like singleton pattren) ? 연속적으로 인스턴스를 생성하게 되면 그만 큼 같은 프로퍼티, 함수들이 메모리에 쌓이는데 static을 사용하면 인스턴스를 생성하지 않고 static으로 지정된 함수들만 메모리에 allocate되기에 메모리 릭을 줄일 수있지 않을까..생각한다.

초기화2

그때문에 이렇게 사용했다. ) 위 그림에서 변수들을 초기화하기 위해 선언된 initProfileImageView()... 등의 함수들 또한 static으로 구현됬다. 이 이미지 뷰, username, fullname label등을 갖는 UIView를 여러개 선언한다고 가정할 때 static 키워드로 함수를 만들면 (어차피 초기화 때 한번 쓰이는 함수들,,) 메모리에 연속적으로 함수까지 allocate되지 않고 단 한번 할당되기 때문이다.

클로저를 통해 변수를 초기화 할 때는 선언한 { } 형태의 클로저를 "( )"를 통해 클로저 실행 시켜야한다. 그리고 init()메서드가 실행되기 이전 return을 통해 특정 타입의 object를 정하는 클로저와 ()는 init메서드가 호출되기 전 시점에 실행되는 것이다. 근데 클로저는 일급 객체 함수이다. 어떤 개체와도 상호작용이 가능하다. return을 통해 반환된 값의 타입 추론을 통해 변수를 초기화 한다.

클로저는 특정 범위 내 변수를 바인딩하기 위해 사용할 수 있고 capture의 특징이 있다. 하지만 캡쳐는 외부의 변수에 값을 사용할 때만 캡쳐를 한다. ARC 는 reference type일 때 작동하는데 중요한 것은 클로저는 reference type이다.

일급 객체를 지원하는 Swift 스타일이 클래스 내 변수 초기화 시점에도 선호할까? 초기화 공식문서에는 클로저를 통해 초기화를 하는 구문이 없다.. .... 내 스타일은 변수들을 확인하고 관련 로직을 구현하기 위해 변수들을 한 눈에 보기 쉽게 해야한다. 그렇기에 클로저를 통한 초기화를 할 땐 클로저의 내부에 특정 프로퍼티를 초기화 하기 위한 기능들이 길게 늘어질 수 있단점이고 프로퍼티를 알기 상대적으로 어려울 수 있다. 이 부분만 따로 뺀다면 들쑥 날쑥해질 것이다. 그럼에도 선언과 동시에 초기화를 하는 클로저는 매력적이다.

그런데 내가 했던 static을 쓰는 방법은 좋은 방법일까 생각을 해봤다. 일단 다른 클래스에도 같은 이름을 사용한 static 클래스가 있다면? 물론 해당 static 함수가 이름이 같아도 클래스 안이기 때문에 식별이 가능하다. 하지만 특정 클래스를 사용하지 않아도 static func 가 메모리에 할당되기 때문에 좋지 않다고 판단했고 혼란스러웠다... 물론 static의 장점은 그럼에도 한번 메모리에 로드 된 이상 해당 클래스를 4개 만든다고 했을때도 static type은 중첩으로 메모리에 할당되지 않는다는 점이다.

그래도 계속해서 찾아봤는데 좋은 방안을 쉽게 찾을 수 없었다,, 계속해서 찾아보다가 클로저를 통한 lazy var는 thread unsafety하다는 것을 알게 되었다. 그럼에도 UIButton을 초기화 할 때 클로저를 통한 초기화를 하게 된다면, init()메서드 이전에 초기화를 한다는 말이다. 아직 self 키워드를 사용할 수 없다. init() 호출 시점 전 이니까,, 그렇다는 것은 func, @objc action method도 사용이 당연히 불가능하다. 그럼 클로저를 통한 변수 선언과 초기화 구문에서 클로저 안에 버튼에 addTarget을 통해 클래스 내에 있는 액션 메소드를 지정하면 init()시점이 아니기에 메모리에 클래스의 func, @objc func 가 할당되지 않았음으로 액션메소드가 등록되지 않아 lazy를 선언함으로 init()이후에 @objc func를 addTarget으로 등록하면서 초기화를 한다.

이런 lazy를 쓰면 좋지 않다니?! 그래서 계속해서 찾아보았다. ㅠ; 스토리보드 사용할 때 UI object를 클래스에 드래그하면 아울렛 변수가 선언되는데 강제 옵셔널 타입이다. weak var가 아닌 이상,, 그리고 어떤 글을 봤는데 UIViewController에서 사용되는 UI프로퍼티들은 거의 사용된다. 그래서 "!"를 쓰는데 이는 init()이후에 초기화가 됨으로 lazy를 쓰지 않아도 위와같은 lazy var 클로저 구문 or 함수를 사용하지 않아도 된다고 한다. 그래서 일단 요 방식으로 선택했다.

초기화9

변수들을 바로 확인 할 수있어 좋다. init메서드 안에서 함수를 통해 깔끔하게 초기화 할 수 있따.

일단 let, var, 클로저, ARC, retain 등 초기화에 관한 공부를 더 한 후 정리 해야겠다...


커스텀 만들기전 발단

lazy var 변수를 선언할 때 선언된 변수의 기능을 지정하는 함수다. 근데 followButton을 너무 계속 써서 클로저를 통해 캡쳐되는 기능을 활용하면 좋지 않을까 생각했고

커스텀 후

UtilsUI 안에 setupLayout이라는 커스텀 함수를 통해 클로저를 구현했다. 쉽게 $0으로 초기화를 했다

letusHyun commented 1 year ago

감사합니다. 많이 배워갑니다 !