포스트

10주차 과제 (9)

우선 과제제출은 끝났다.

하지만 Combine이라는 새로운 녀석을 쓰다보니 예외처리하는 부분이 상당히 빡세다.

우선 시급한 예외처리

  1. 서치바에 내용이 지워졌을때 빈배열 만들기 & page init

  2. 현재 최근 본 리스트에 coredata에 이상하게 입력이 되는 문제

1. 첫번째 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Publishers.CombineLatest(input.searchPublisher, input.numberPublisher)
            .map { [unowned self] (value, page) in
                if value.isEmpty {
                    currentPage = 1
                    document = []
                }
                return (value, currentPage)
            }
            .eraseToAnyPublisher()
            .sink { [weak self] (value, page) in
                guard !value.isEmpty else { return } // value가 빈 문자열인 경우 fetchTotalRequest 호출하지 않음
                self?.fetchTotalRequest(queryValue: value, page: page)
            }
            .store(in: &cancellables)

value가 empty일때 즉 값이 없을때 currentPage를 1로, 그리고 document도 빈배열로 초기화를 해준다.

작동은 하지만 다시 겁색을 하게 되면 무한스크롤을 해서 페이지가 4까지 증가했다면, 다시 글을 지우고 검색을 다시하면

api호출을 4번 하는걸로 확인이 된다.

즉 searchVM에서 손을 봐야한다는 말이된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func fetchTotalRequest(queryValue: String, page: Int) {
        let urlString = "https://dapi.kakao.com/v3/search/book?target=title"
        let headers = ["Authorization" : "KakaoAK \(Secret.apikey)"]
        
        var urlComponent = URLComponents(string: urlString)
        urlComponent?.queryItems?.append(URLQueryItem(name: "query", value: queryValue))
        urlComponent?.queryItems?.append(URLQueryItem(name: "page", value: page.stringValue))
        
        guard let url = urlComponent?.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        
        let session = URLSession(configuration: .default)
        session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: BookModel.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
            .replaceError(with: totalDocumnet)
            .sink(receiveValue: { [weak self] model in
                if model.meta.isEnd == false {
                    print("document Count is \(model.documents.count)")
                    self?.document.append(contentsOf: model.documents)
                } else {
                    return
                }
            })
            .store(in: &cancellables)
    }
1
2
3
4
document Count is 10
document Count is 10
document Count is 10
document Count is 10

이렇게 같은걸 4번 출력을 한다. 그래서 같은내용의 셀이 반복이 되었던것.

내선에서는 안될것같아 튜터님을 찾아갔다.

1
2
3
4
5
6
7
8
9
10
11
12
@objc func loadData() {
        searchVM.currentPage += 1
        searchVM.numberSubject.send(searchVM.currentPage)
        //searchVM.transform(input: SearchVM.Input(searchPublisher: searchView.valuePublisher, numberPublisher: searchVM.valuePublisher))
        searchVM.$document
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
            .sink { [weak self] _ in
                self?.resultView.tableView.reloadData()
            }.store(in: &cancellables)
        print(searchVM.currentPage)
    }

바로 주석친 저부분이 문제가 되었던것.

어차피 subject에서 데이터를 전달하기에 그냥 1회 구독에서 계속 값만 갱신을 하면 되었는데 아무 생각없이 함수처럼 한번 더 사용을 해야되나 라는 생각으로 transform을 그대로 사용함으로써 무한스크롤 할때마다 구독도 계속 증가를 했기에, 초기화를 하고 검색을 다시하면 증가한 구독횟수만큼 호출하는것이다.

1
2
3
4
5
6
7
8
9
10
receive value: (("해리", 1))
receive value: (("해리", 2))
receive value: (("해리", 3))
receive value: (("해리", 4))
receive value: (("해리", 5))
receive value: (("해리", 6))
receive value: (("해리", 7))
receive value: (("해리", 8))
receive value: (("해리", 9))
receive value: (("해리", 10))

호출이 잘된다.

즉 loadData에서는 page값만 보내주면 되었다. 함수처럼 생각해서 다시 재호출을 해버리면 재호출 한만큼 Publisher와 Subscriber 사이의 구독 횟수같이 증가한다는걸 잊지말자.

2. 두번째 문제

이건 어느순간 갑자기 발생하기 시작했다.

정확하게 어느부분이 문제인지 파악이 필요한 상태.

1. 내방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        
        // DetailVC에 전달
        searchVM.$document
            .map{ document in
                return document[indexPath.row]
            }
            .eraseToAnyPublisher()
            .receive(on: DispatchQueue.main)
            .sink { document in
                let imageURL = URL(string: document.thumbnail)
                detailVC.titleView.titleLabel.text = document.title
                detailVC.titleView.authorLabel.text = document.authors.joined()
                detailVC.imageView.imageView.kf.setImage(with: imageURL)
                detailVC.imageView.priceLabel.text = document.price.stringValue
                detailVC.bodyView.bodyLabel.text = document.contents
                detailVC.wishSubject.send(document)
            }.store(in: &cancellables)
        
        // CoreData에 등록
        searchVM.$document
            .map{  document in
                if !document.isEmpty {
                    return document[indexPath.row]
                } else {
                    return document[0]
                }
            }
            .eraseToAnyPublisher()
            .sink(receiveValue: { [weak self] document in
                self?.recentVM.saveDocumentToCoredata(data: document)
            })
            .cancel() // modified
        
        detailVC.modalPresentationStyle = .fullScreen
        present(detailVC, animated: true)
    }

뭔가 계속해서 메모리에 남은건가? 라는 생각이 들었고, 이전에도 컬렉션뷰에서 로딩했을때 담기 버튼 할때 구독을 끊음으로써 해결이 되었던걸 생각하고

여기도 끊어버렸다.

문제해결 그래도 덕분에 alert 작동하는걸 확인했다.

2. 튜터님 방식

튜터님께 이부분을 말씀드리니 DetailVC에서 강한 순환참조가 발생한다고 하신다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        
        // DetailVC에 전달
        searchVM.$document
            .map{ document in
                return document[indexPath.row]
            }
            .eraseToAnyPublisher()
            .receive(on: DispatchQueue.main)
            .sink { [weak detailVC] document in // modified
                let imageURL = URL(string: document.thumbnail)
                detailVC?.titleView.titleLabel.text = document.title
                detailVC?.titleView.authorLabel.text = document.authors.joined()
                detailVC?.imageView.imageView.kf.setImage(with: imageURL)
                detailVC?.imageView.priceLabel.text = document.price.stringValue
                detailVC?.bodyView.bodyLabel.text = document.contents
                detailVC?.wishSubject.send(document)
            }.store(in: &detailVC.cancellables) // modified
        
        // CoreData에 등록
        searchVM.$document
            .map{  document in
                if !document.isEmpty {
                    return document[indexPath.row]
                } else {
                    return document[0]
                }
            }
            .eraseToAnyPublisher()
            .sink(receiveValue: { [weak self] document in
                self?.recentVM.saveDocumentToCoredata(data: document)
            }).cancel() // modified
        
        detailVC.modalPresentationStyle = .fullScreen
        present(detailVC, animated: true)
    }

위에 코드도 적어놨지만, 현재 detailVC에 강한 순환 참조를 피하기 위해

[weak detailVC]를 하셨다. 항상 클로저 안에서 [weak self]만 쓰다보니 저렇개 쓸 생각을 하질 못했다.

첫번째 사진 셀을 클릭하고 DetailVC로 들어간 상태 무난해보인다.

CleanShot 2024-05-10 at 15 04 51@2x

두번째 사진 닫기 버튼을 클릭 한상태

CleanShot 2024-05-10 at 15 06 17@2x

detailVC가 남아있다.

weak으로 약한 참조를 한 상태의 사진 DetailVC로 들어간 상태

CleanShot 2024-05-10 at 15 07 42@2x

닫기 버튼을 클릭 한상태

CleanShot 2024-05-10 at 15 08 42@2x

DetailVC가 보이지 않는다.

그리고 detailvc의 cancellables에 저장을 해둔다.

그러면 deinit 하면서 해당 내용은 사라진다.

이렇게 강한 순환 참조로 인해 발생하던 문제를 해결하게되었다.

그리고 wishSubject가 CurrentValueSubject 값을 들고있기에 최근 본 데이터에 같이 누적되는것으로 판단하여 cancel로 바로 끊어버렸다.

후기

이번에는 처음에 시작할때부터 Combine + MVVM 을 사용하는데 의미를 두고 시작했다.

MVVM은 솔직히 크게 문제가 없었는데, Combine 사용에 있어서 많은 부족함을 느꼈다.

사실 부족함 느끼는게 정상이긴 하다 4~5일 정도 공부하고 바로 적용을 해봤는데, 하면서 느낀건, Data 전달이 뭔가 명확하다는 것이고,

Publisher - Subscriber 와의 관계가 얼마나 중요한지 새삼 느끼게 된다.

이번에 하는 예외 처리 역시 이미 Subsciption이 형성이 되어있기에 Data만 계속해서 send를 해주어도 API통신이 되면서 해당 값을 가져오게 되는데, 기존의 방식에 익숙해져서 일까 함수를 호출하면서 생긴 문제였다.

Combine 사용하면서 과제나 팀프로젝트에서도 단 한번도 사용하지 않은 GPT를 두번이나 사용했다. 그래도 그걸가지고 왜 이렇게 사용을 했는지 내 나름대로 자료를 검색하면서 그 의도를 생각해보니 괜찮았다.

물론 GPT 사용은 이번 Combine에서 끝.

그래도 이번 프로젝트에서 나름 재미있는 경험을 한것같아 만족한다.

다행히도 튜터님에게 과제를 진행하면서 Insight가 필요할때마다 여쭤 봤는데, Combine 이정도로 사용한것만으로도 대단하다고 하시긴 해서, 4~5일간의 공부와 프로젝트기간 약 6일정도를 합치면 10~11일 동안 내가 무모한 시도를 한건 아니었다는 생각이 든다.

원래 이렇게 개인프로젝트하면서 후기를 잘 쓰지 않는데, Combine 공부할거 앞으로 산더미겠지만, 그래도 약간의 자신감은 갖고 간다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.