본문 바로가기

UIKIT

검색한 결과에서 중복된 데이터 통합하기

  • 1페이지 당 10개의 데이터를 가져옵니다. 
  • 가져온 데이터를 카테고리별로 구분합니다. 
  • 카테고리 별 표시할 수 있는 데이터 수는 최대 3개입니다. 
  • 이때, 페이지 당 10개의 데이터 중에서 카테고리 별로 구분했을 때 1개 또는 2개 일 경우는 그 다음 페이지로 넘어갑니다. 
  • 넘어간 다음 페이지에서 카테고리 별로 데이터를 구분하여 누적하여 관리합니다. 
  • 이때, 페이지 내 또는 페이지 간의 중복된 데이터를 통합할 필요가 있습니다. (통합 또는 관리)
  • 페이지 단위로 검색 결과를 가져오면서 중복된 데이터를 제거하고, 각 contentTypeId별로 최소 3개의 항목을 확보하는 방식으로 구현해보겠습니다. 

 

주요 구조

  • uniqueContentIds (Set): 중복된 데이터를 체크하기 위해 contentId만 저장하는 Set입니다. 이를 통해 이미 추가된 contentId를 추적할 수 있습니다.
  • spotResultsByContentTypeId (Dictionary): contentTypeId를 키로 하고, 각 contentTypeId에 해당하는 중복되지 않은 AttractionItem 배열을 값으로 저장하는 딕셔너리입니다.

 

 

함수 설명

1. searchForKeyword 함수

이 함수는 네트워크 요청을 통해 키워드 검색 결과를 가져옵니다. 데이터가 성공적으로 가져와지면 중복을 검사하고 카테고리별로 정리하는 filterAndCategorizeByContentTypeId 함수를 호출합니다.

func searchForKeyword(with keyword: String, page: Int, completion: @escaping (Int) -> Void) {
    NetworkManager.shared.searchKeywordList(pageNo: String(page), keyword: keyword) { [weak self] results in
        switch results {
        case .success(let items):
            let searchList = items.response.body.items.item
            let totalCount = items.response.body.totalCount
            self?.filterAndCategorizeByContentTypeId(searchList)  // 중복 필터링 및 카테고리화
            completion(totalCount)
            
            DispatchQueue.main.async {
                self?.spotResultsTableView.isHidden = searchList.isEmpty
                self?.spotResultsTableView.reloadData()
            }
            
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}

 

 

  • 입력값:
    • keyword: 검색할 키워드.
    • page: 요청할 페이지 번호.
    • completion: 검색 완료 후, 총 결과 수(totalCount)를 반환하는 클로저.
  • 작업 흐름:
    1. NetworkManager의 searchKeywordList 함수를 호출하여, 주어진 keyword와 page에 맞는 검색 결과를 가져옵니다.
    2. 요청이 성공하면 items 객체에서 searchList와 totalCount를 추출합니다.
    3. filterAndCategorizeByContentTypeId 함수를 호출하여 중복을 제거한 후, 각 contentTypeId별로 정리된 데이터를 spotResultsByContentTypeId에 저장합니다.
    4. completion 클로저를 호출하여 totalCount를 반환합니다.
    5. UI 업데이트: 메인 스레드에서 테이블뷰를 업데이트합니다.

 

 

2. fetchForKeyword 함수

이 함수는 검색 결과가 최소 3개 확보될 때까지 페이지를 순차적으로 가져오는 역할을 합니다. 초기화와 함께 첫 페이지를 요청하고, 각 contentTypeId별로 필요한 데이터를 확보하기 위해 ensureMinimumItems 함수를 호출합니다.

// 중복을 확인하기 위한 Set (fetchForKeyword 외부에 선언)
var uniqueContentIds = Set<String>()

func fetchForKeyword(with keyword: String) {
    // 검색 시작 시 중복 확인 Set을 초기화
    uniqueContentIds.removeAll()

    searchForKeyword(with: keyword, page: 1) { [weak self] totalCount in
        guard let self = self else { return }
        
        let itemsPerPage = 10
        let totalPages = (totalCount / itemsPerPage) + (totalCount % itemsPerPage > 0 ? 1 : 0)

        var allContentTypesSatisfied = false

        // 각 contentTypeId별 최소 3개 확보
        for contentTypeId in self.spotResultsByContentTypeId.keys {
            if (self.spotResultsByContentTypeId[contentTypeId]?.count ?? 0) < 3 {
                allContentTypesSatisfied = true
                self.ensureMinimumItems(for: contentTypeId, keyword: keyword, currentPage: 1, totalPages: totalPages)
            }
        }
        
        if allContentTypesSatisfied {
            DispatchQueue.main.async {
                self.spotResultsTableView.isHidden = self.spotResultsByContentTypeId.isEmpty
                self.spotResultsTableView.reloadData()
            }
        }
    }
}

 

 

 

  • 작업 흐름:
    1. 중복 확인 Set 초기화: uniqueContentIds.removeAll()을 통해 검색 시작 전에 중복을 확인할 Set을 초기화합니다.
    2. 첫 번째 페이지 데이터를 가져오기 위해 searchForKeyword를 호출하고, 검색 결과의 총 개수를 확인합니다.
    3. 최소 확보 조건 검토: 각 contentTypeId별로 최소 3개의 데이터가 확보되었는지 확인하고, 부족한 경우 ensureMinimumItems를 호출하여 다음 페이지 데이터를 가져옵니다.
    4. UI 업데이트: 모든 contentTypeId가 조건을 충족하면 테이블뷰를 리로드합니다.

 

3. filterAndCategorizeByContentTypeId 함수

이 함수는 중복되지 않은 contentId만 추가하도록 데이터를 필터링하여 spotResultsByContentTypeId에 저장합니다.

func filterAndCategorizeByContentTypeId(_ items: [AttractionItem]) {
    for item in items {
        let contentId = item.contentId
        let contentTypeId = item.contentTypeId
        
        // 중복 검사: Set에 contentId가 없는 경우에만 추가
        if uniqueContentIds.insert(contentId).inserted {
            // 중복이 없으므로 딕셔너리에 추가
            spotResultsByContentTypeId[contentTypeId, default: []].append(item)
        }
    }
}

 

  • 작업 흐름:
    1. uniqueContentIds에 각 contentId를 삽입하여 중복 여부를 확인합니다.
    2. 중복이 아닌 경우에만 spotResultsByContentTypeId에 contentTypeId별로 데이터를 추가합니다.

 

4. ensureMinimumItems 함수

이 함수는 각 contentTypeId에 대해 최소 3개의 항목이 확보될 때까지 다음 페이지 데이터를 요청합니다. 데이터가 충분히 확보되면 UI를 업데이트합니다.

 

func ensureMinimumItems(for contentTypeId: String, keyword: String, currentPage: Int, totalPages: Int) {
    let itemCount = spotResultsByContentTypeId[contentTypeId]?.count ?? 0

    if itemCount < 3, currentPage < totalPages {
        let nextPage = currentPage + 1
        searchForKeyword(with: keyword, page: nextPage) { [weak self] _ in
            self?.ensureMinimumItems(for: contentTypeId, keyword: keyword, currentPage: nextPage, totalPages: totalPages)
        }
    } else {
        DispatchQueue.main.async {
            self.spotResultsTableView.reloadData()
        }
    }
}

 

  • 작업 흐름:
    1. 데이터 개수 확인: spotResultsByContentTypeId에서 해당 contentTypeId의 데이터 개수를 확인합니다.
    2. 추가 요청 조건: 데이터가 3개 미만이고, 총 페이지 수에 도달하지 않았을 때 nextPage를 요청합니다.
    3. 재귀적으로 ensureMinimumItems를 호출하여 데이터를 확보합니다.

이 코드를 통해 각 contentTypeId별로 중복 없는 데이터를 3개 이상 확보할 때까지 페이지를 순차적으로 요청하며, 중복된 데이터를 제외하고 딕셔너리에 추가합니다.

 


 

코드를 설계할 때에는 문제를 명확히 이해하고, 이를 해결할 수 있는 구조와 흐름을 먼저 구상하는 것이 중요합니다.

 

1. 요구사항 분석과 목표 설정

먼저 코드가 해결해야 할 문제를 명확히 정의하고, 필요한 목표를 설정합니다.

예제 목표:

  • 검색 키워드를 통해 데이터를 가져온다.
  • contentTypeId별로 최소 3개의 결과를 확보하고, 중복된 contentId는 허용하지 않는다.
  • 테이블뷰에 contentTypeId별로 데이터를 표시한다.

팁: 문제의 전체 구조를 작은 단계로 쪼개어 설계하며, 각각의 역할과 목적을 정의합니다.

 

2. 데이터 구조 설계

데이터가 어떻게 저장되고 전달될지를 설계합니다. 여기서는 검색 결과를 저장하는 딕셔너리와 중복 확인을 위한 집합(Set)이 필요했습니다.

  • spotResultsByContentTypeId: contentTypeId별로 데이터를 그룹화하여 저장할 딕셔너리 ([String: [AttractionItem]]).
  • uniqueContentIds: 중복된 contentId를 방지하기 위해 contentId를 추적하는 Set (Set<String>).

팁: 데이터를 다룰 때, 저장해야 할 정보와 필요 시 확인할 조건들을 미리 정리합니다. 이러한 데이터 구조는 이후의 코드 흐름과 로직을 간단하게 만듭니다.

 

3. 함수 분리 및 역할 설정

각 함수는 하나의 명확한 역할을 담당하도록 설계합니다. 위의 코드를 예시로 다음과 같이 각 함수가 담당할 역할을 정의할 수 있습니다.

  • searchForKeyword: 네트워크 요청을 통해 데이터를 가져오는 역할
  • fetchForKeyword: 최소 데이터 개수 확보를 위해 재귀적으로 데이터를 요청하는 역할
  • filterAndCategorizeByContentTypeId: 데이터를 중복을 확인하고 카테고리별로 정리하는 역할
  • ensureMinimumItems: 각 contentTypeId별로 데이터가 충분한지 확인하고, 부족할 경우 추가로 데이터를 요청하는 역할

팁: 각 함수의 역할을 작고 명확하게 정의하고, 한 가지 책임만 가지도록 설계합니다. 이를 통해 코드의 유지보수와 테스트가 쉬워집니다.

 

 

4. 중복 처리 로직과 조건 설계

중복 처리는 이 코드의 중요한 요구사항입니다. 여기서는 Set을 이용하여 중복을 방지하면서 데이터를 추가하는 방식을 선택했습니다.

 

func filterAndCategorizeByContentTypeId(_ items: [AttractionItem]) {
    for item in items {
        let contentId = item.contentId
        let contentTypeId = item.contentTypeId
        
        // 중복 검사: Set에 contentId가 없는 경우에만 추가
        if uniqueContentIds.insert(contentId).inserted {
            // 중복이 없으므로 딕셔너리에 추가
            spotResultsByContentTypeId[contentTypeId, default: []].append(item)
        }
    }
}

 

팁: 조건문을 최대한 간결하게 유지하며, 한 줄에 여러 조건이 포함되지 않도록 설계합니다. 각 조건은 논리적으로 설명 가능하고 코드가 실행될 때의 흐름이 예측 가능해야 합니다.

 

5. 재귀 호출과 반복 조건 관리

재귀 호출(또는 반복)을 통해 조건을 충족할 때까지 데이터를 가져오는 경우, 종료 조건 반복 조건을 명확히 설계합니다. 종료 조건이 명확해야 무한 루프를 방지할 수 있습니다.

 

func ensureMinimumItems(for contentTypeId: String, keyword: String, currentPage: Int, totalPages: Int) {
    let itemCount = spotResultsByContentTypeId[contentTypeId]?.count ?? 0

    if itemCount < 3, currentPage < totalPages {
        let nextPage = currentPage + 1
        searchForKeyword(with: keyword, page: nextPage) { [weak self] _ in
            self?.ensureMinimumItems(for: contentTypeId, keyword: keyword, currentPage: nextPage, totalPages: totalPages)
        }
    }
}

 

팁: 재귀 호출을 사용할 때는 항상 종료 조건을 포함하고, 재귀가 끝난 후의 작업을 잘 정의합니다. 필요한 경우 디버깅을 통해 종료 조건이 잘 작동하는지 확인합니다.

 

 

6. UI 업데이트와 동기화 관리

최종적으로 데이터를 가져오고 나면, UI를 업데이트합니다. 이때 UI 변경은 메인 스레드에서 이루어져야 합니다.

DispatchQueue.main.async {
    self.spotResultsTableView.isHidden = self.spotResultsByContentTypeId.isEmpty
    self.spotResultsTableView.reloadData()
}

 

 

팁: 네트워크 요청이나 비동기 작업에서 데이터를 가져올 때는 DispatchQueue.main.async를 활용하여 UI 업데이트가 메인 스레드에서 실행되도록 관리합니다.

설계 순서 요약

  1. 요구사항 분석: 해결해야 할 문제와 목표를 명확히 정의합니다.
  2. 데이터 구조 설계: 데이터를 효율적으로 저장할 구조를 결정합니다.
  3. 함수 역할 분리: 각 함수가 하나의 역할만 담당하도록 설계합니다.
  4. 조건 및 중복 처리 설계: 중복 확인 및 추가 조건을 명확히 정의하여 효율적으로 처리합니다.
  5. 반복 조건 관리: 재귀나 반복의 종료 조건을 명확히 하고, 필요한 경우 재귀 호출을 활용합니다.
  6. UI 업데이트와 동기화 관리: 최종적으로 UI 업데이트가 필요한 경우 메인 스레드에서 이루어지도록 관리합니다.

이 순서를 참고하여 각 요구사항에 맞게 코드를 세분화하고, 설계의 방향을 잡는 것이 효과적인 코드 작성을 위한 핵심입니다.