Modal의 onAppear에서 데이터를 fetching하는 API 호출하면 안되는 이유

SteadySlower·2022년 9월 2일
0

3시간…😭

오늘 단어장을 마감하는 기능을 구현하기 위해 Modal View를 하나 만들고 있었습니다. 해당 View에서는 기존의 단어장 중에 하나를 골라서 마감하려는 단어장에서 아직 학습이 완료되지 않은 단어들을 그 단어장으로 이동시키는 작업을 할 생각이었습니다.

그러기 위해서는 모든 단어장을 호출하는 API를 사용해서 이동시킬 단어장의 목록을 서버에서 가져와야 했습니다. 이 과정에서 단어장의 데이터가 가져와지지 않아서 🐶 고생을 했던 3시간을 이 포스팅에 적어보도록 하겠습니다.

API를 onAppear에서 사용하려고 했던 이유

예전에 단어을 쭉 보여주는 StudyView를 만들 때는 단어들을 fetch 해올 때 onAppear에서 API를 호출하도록 했습니다. 이유는 해당 View가 NavigationLink로 감싸져있기 때문입니다.

NavigationLink {
    StudyView(wordBook: viewModel.wordBook)
} label: {
    HStack {
        Text(viewModel.wordBook.title)
        Spacer()
    }
    .padding(12)
}

어떤 View가 NavigationView안에 있는 경우 해당 NavigationView가 init이 될 때 이 View도 init이 되게 됩니다.

따라서 해당 View가 실제로 화면이 나타나기 전에는 호출할 필요가 없는 API가 호출되는 것이죠

🤔 Modal이라서 그런가?

onAppear에서 API 호출하면 버그

위에 View에서는 문제 없이 작동을 했습니다. 하지만 지금 만들고 있는 WordCloseView에서는 onAppear에서 API를 호출했더니 데이터를 불러오지 못했습니다.

정확한 정황을 설명하자면 아래와 같습니다. onAppear에서 호출하는 함수는 다음과 같습니다. 서버에서 books를 받아와서 viewModel의 wordsBooks 변수에 할당하는 것이죠.

func getWordBooks() {
    WordService.getWordBooks { [weak self] books, error in
        if let error = error {
            print(error)
            return
        }
        
        guard let books = books else {
            print("Debug: No wordbook Found")
            return
        }
        
				print("completionHandler 안에서 self: \(self)")
        self?.wordBooks = books
    }
}

하지만 Modal이 뜨고 나서 wordBooks은 항상 초기값인 빈배열이었습니다.

completion 안에서 self가 nil?

일단 completionHandler에서 self를 출력해보았습니다. 놀랍게도 self가 nil로 출력되었습니다. 즉 viewModel이 API를 호출하고 나서 deinit이 되어 버린 것이죠.

View, ViewModel의 init과 onAppear의 실행시점

그렇다면 onAppear이 출력되는 시점과 View가 init, viewModel이 init, deinit되는 시점에 각각 프린트를 해보았더니 아래와 같은 결과가 나왔습니다. (at은 viewModel이 init된 시점의 TimeInterval 값입니다.)

결과를 분석해보면 SwiftUI는 Modal을 띄울 때 총 3번의 View와 ViewModel을 init합니다. 각각 1, 2, 3번으로 번호를 붙이겠습니다. 그리고 화면을 그리고 나면 처음 2번에 init한 ViewModel (그리고 아마 View도) 즉 1번, 2번을 deinit을 시켜버리죠.

하지만 onAppear는 처음에 생성된 View에서만 딱 1번 실행됩니다. 이 때 API 호출이 1번 실행됩니다. 네트워크에서 데이터를 받아서 completionHandler를 실행할 때는 이미 클로저가 캡쳐한 self, 즉 API를 호출한 viewModel은 deinit이 된 이후입니다.

결국…

의도인지 의도되지 않은 것인지는 모르겠지만 일단 내부적으로 저렇게 실행이 되므로 onAppear에서 API를 호출해서는 데이터를 받아올 수 없습니다. 결국 제 선택은 View의 initializer에서 API를 호출하는 것입니다. 호출 결과는 아래와 같습니다.

예상한대로 API가 3번 호출되고 마지막 View에서 호출한 API만 제대로 ViewModel에 데이터를 전달합니다.

뭐 일단은 어쩔 수 없는 선택이었지만 해당 네트워크 작업이 굉장히 무거운 것이라면 반드시 첫 2개를 취소해주거나 아니면 더 적절한 API 호출 위치를 찾아야할 것 같습니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글