App Store 검색 예제 구현중 발생한 메모리 누수 이슈
iTunes Search API를 활용하기
오늘은 애플의 iTunes Search API를 활용하여 기본 앱인 앱스토어의 검색 기능을 구현하던 중 메모리 누수와 앱 정지 이슈를 경험하게 되어 작성하게 되었습니다.
예제는 RxSwift를 기반으로 코드를 작성하였고 RxSwift를 학습하면서 예제를 구현하고있어 문제를 파악하고 해결하는데 시간이 꽤 걸렸던 것 같습니다.
원하는 기능 (실제 앱 스토어 기능 구현)
- 초기 화면 - 검색결과가 없는 (당연하게도) 상태
- 단어 검색 중 - 검색 중에도 검색 결과는 나타나지 않음
- 검색 버튼 클릭 - 검색 창의 단어로 API를 요청하여 검색 결과를 받아 테이블 뷰에 보여짐
- 취소 버튼 클릭 - 모든 결과와 검색 창의 단어도 사라지며 초기 상태로 돌아옴
- 검색 단어 수정 - 검색한 단어에서 한 글자라도 수정이 일어나면 검색 결과 삭제
위 스크린샷은 실제 앱스토어 캡쳐 화면입니다!
구현 초기 화면
가장 먼저 검색 창을 만들었고 검색 창에 원하는 키워드를 입력하면 애플의 iTunes Search API를 이용하여 검색결과를 가져오는 작업을 수행했습니다.
검색어를 입력하고 검색 버튼을 클릭하여 결과를 가져오는 부분에서 메모리 누수와 앱 정지의 문제가 일어나게 되는데
위 스크릿샷과 같이 아무 문제가 없어 보이지만?
위와 같이 CPU는 계속 돌아가게되고 가만히 냅두고 있으면 메모리가 미친듯이 쌓여가게 된다…!
제가 처음으로 생각한 문제의 원인은 텍스트의 변화를 감지할 때와 검색 버튼을 눌렀을 때의 충돌이라고 생각했습니다.
아래의 코드를 같이 보면
import Foundation
import RxSwift
import RxCocoa
class SearchViewModel {
struct Input {
let searchButtonClicked: ControlEvent<Void>
let cancelButtonClicked: ControlEvent<Void>
let searchText: ControlProperty<String>
}
struct Output {
let result: PublishRelay<[AppInfo]>
}
let disposeBag = DisposeBag()
func transform(input: Input) -> Output {
let result = PublishRelay<[AppInfo]>()
input.searchText
.distinctUntilChanged()
.map { _ in
return result
}
.flatMap { $0 }
.observe(on: MainScheduler.instance)
.withUnretained(self)
.bind { owner, value in
result.accept(value)
}
.disposed(by: disposeBag)
}
}
transform 메서드 내의 input.searchText는 검색 TextField에 입력되어 있는 String 값을 반환하고 있으며 distinctUntilChanged() 메서드를 사용하여 텍스트의 변화가 있을 때에만 아래의 코드를 수행되게 작성해주었습니다.
저는 String 값이 필요한게 아닌 검색어의 값이 변하는 타이밍을 원하는 것이기에 map을 사용하여 result로 반환해주었죠.
추가로 PublishRelay<[AppInfo]> 값을 flatMap을 사용하여 result에 값을 넘겨줄 수 있도록 [AppInfo]로 반환했고 마지막으로 results에 값을 accep로 전달하였습니다.
넘겨줘야하는 값의 타입도 잘맞고 제대로 넘겨줬다고 생각했으나 위의 코드는 result에 result를 다시 불러와 가공하는 작업을 거쳐 다시 자신에게 넣어주고 있는 코드라고 볼 수 있습니다..
결국 제가 생각했던 텍스트 변화 감지와 검색 버튼을 클릭의 충돌이 문제는 아니었고 빈 배열을 result에게 넣어주고 싶었던 작업이 결국 자기 자신을 가공하여 전달하는 부분이 되어버린 작업이 오류였고 여기서 메모리 누수가 일어나게 되는 것이었습니다.
(문제를 해결하고 보니 그냥 이해를 제대로하지 못한 상태에서 코드를 작성했던 것 ㅎㅎ,,,!)
문제를 해결한 코드는 너무 간단하게도 result에 빈 배열을 넣어주면 되는 아주아주 간단한 문제였죠 하하,,,
input.searchText
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.withUnretained(self)
.bind { _ in
result.accept([])
}
.disposed(by: disposeBag)
결론
- 텍스트가 변경되는 시점을 알고 싶었고 변경되는 시점은 String 값을 반환해주고 있다.
- 하지만 텍스트가 변경되는 시점만 알고싶을뿐 String 값은 필요가 없기에 값을 _ 로 처리하고 result.accept([]) 로 코드를 작성하였다.
RxSwift를 처음 접하기에 학습하는 과정에서 코드에 결함이 많을 수 있습니다..! 오늘도 읽어주셔서 감사합니다~