Firebase-Firestore에서 검색하는 법

SteadySlower·2022년 9월 9일
0
post-custom-banner

예전에 제가 다른 프로젝트들을 했을 때는 디바이스에 직접 저장하는 CoreData를 사용하거나 MySQL를 활용한 웹서버를 이용해서 데이터를 저장했었는데요. 둘 다 SQL 계열의 DB라서 많이 익숙했었습니다. 하지만 현재 프로젝트에서는 Firebase에서 제공하는 Firestore를 활용하고 있습니다. 이 DB는 NoSQL이라서 조금씩 배워가면서 하고 있습니다.

이번 포스팅에는 제가 단어들이 저장된 DB에서 특정 문자열을 포함한 단어를 fetch 해오는 기능을 만들고자 합니다. 2가지 방법으로 구현하고 제가 최종적으로 어떤 방법을 선택했는지 소개하겠습니다.

방법 1) 전부 다 읽어오고 filter 하기

첫 번째 방법은 약간 무식(?) 방법입니다. DB 그냥 단어를 전부 다 가져옵니다. 그리고 나서 filter를 통해서 해당 문자열을 포함한 단어를 찾는 방법입니다. 코드는 아래와 같습니다.

// Examples를 검색하는 함수
static func fetchExamples(_ query: String, completionHandler: @escaping ([WordExample]?, Error?) -> Void) {
    Constants.Collections.examples
        .getDocuments { snapshot, error in
            if let error = error {
                completionHandler(nil, error)
            }
            guard let documents = snapshot?.documents else { return }
            let examples = documents
                    .compactMap { try? $0.data(as: WordExample.self) }
                    .filter { $0.meaningText.contains(query) }
            completionHandler(examples, nil)
        }
}

방법 2) String의 비교연산자 활용하기

두 번째는 DB에서 필터링을 하도록 하는 것입니다. 아쉽게도 Firestore는 특정 문자열을 포함하는 데이터만 fetch 해 올 수 있는 메소드는 제공하지 않는 것 같습니다. 하지만 약간의 우회적인 방법으로 100% 1번과 동일한 기능은 아니지만 비슷한 기능을 구현할 수 있습니다.

String을 “<, >, =” 등이 비교연산자로 비교하면 아스키코드를 기준으로 비교합니다. 따라서 query ~ query + “힣" 사이의 모든 문자열을 반환하게 하면 검색기능을 구현할 수 있습니다. 마지막 범위에 “힣"을 붙이는 이유는 이 Character가 가장 큰 아스키 코드를 가지기 때문입니다. 코드는 아래와 같습니다.

// Examples를 검색하는 함수
static func fetchExamples(_ query: String, completionHandler: @escaping ([WordExample]?, Error?) -> Void) {
    Constants.Collections.examples
        .whereField("meaningText", isGreaterThanOrEqualTo: query)
		//👉 원하는 문자열 보다 크고
        .whereField("meaningText", isLessThan: query + "힣")
		//👉 원하는 문자열 + "힣" 보다는 작거나 같은 문자열
        .getDocuments { snapshot, error in
            if let error = error {
                completionHandler(nil, error)
            }
            guard let documents = snapshot?.documents else { return }
            let examples = documents
                    .compactMap { try? $0.data(as: WordExample.self) }
                    .sorted(by: { $0.used > $1.used })
            completionHandler(examples, nil)
        }
}

방법 2의 장점

이렇게 하면 DB에서 원하는 데이터만 filter해서 보내주므로 네트워크 비용이 절약되고 앱에서 filter를 할 필요도 없으므로 성능도 개선됩니다.

방법 2의 단점

하지만 이 방법은 정확히 이야기하면 query를 포함한 모든 문자열을 반환하게 하는 방법이 아닙니다. query로 시작하는 문자열을 반환하도록 하는 방법입니다. 즉 만약에 query가 “사과"라면 “사과 나무”라는 문자열은 반환하기만 “햇사과"라는 문자열은 반환하지 않습니다.

저의 선택은….

저는 방법 2를 택했습니다. 이 API는 자주 호출되는 API입니다. 따라서 매번 모든 단어를 다 불러오는 것은 곤란합니다. 물론 단어들을 한번만 불러오고 메모리에 저장해두고 사용하는 방법도 있겠지만 단어를 불러온 이후 추가한 단어는 검색할 수 없다는 점에서 한계가 있습니다.

따라서 비록 검색 기능에 한계가 있는 방법이지만 자주 호출해도 부담이 없는 2번 방법을 선택하는 것이 적절하다고 생각했습니다.

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

0개의 댓글