샘성의 iOS 개발 일지

[KakaoMap 클론] 2. Kakao API로 장소 검색 결과 받기 본문

iOS/UiKit

[KakaoMap 클론] 2. Kakao API로 장소 검색 결과 받기

SamusesApple 2023. 5. 25. 11:51
728x90

  카카오맵 검색창에 '카페'라고 검색하면 사용자 위치 근처의 카페들에 대한 검색 결과를 보여준다.

  이를 구현하기 위해 키워드로 장소 검색하기' 를 사용할 것이다.

 

 

 

배경: 카카오 REST API KEY 발급받은 상태 + Alamofire pod install 된 상태

 

 

1. Kakao API - 키워드로 장소 검색하기

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

 

HTTP 요청 보내는 법은 간단하다. 

GET 메서드에 원하는 요청의 정보를 Parameter에 담아 Host에 요청을 보내면 된다.

GET /v2/local/search/keyword.${FORMAT} HTTP/1.1
Host: dapi.kakao.com
Authorization: KakaoAK ${REST_API_KEY}

 

 

 

 

 

   우선, 필자는 '키워드로 검색' 하되 위치 근처에 있는 검색 결과를 받고싶기에, '위치의 좌표(x, y)'도 같이 줄 것이다.

따라서 요청 보낼 파라미터는 query, x, y 3개가 될 것이다.

 

  그렇다면 요청 형식은 하단처럼 될 것이다.

 

1. 호스트 정보

2. GET메서드 + 파라미터

 

파라미터는 ? 이후에 들어가며, 각각의 파라미터를 &로 분리해야한다. (띄어쓰기처럼 생각하면 편함)

https://dapi.kakao.com/v2/local/search/keyword.json?query=검색키워드&x=위도&y=경도

 

 

 

 

2. Postman로 테스트 요청 보내기

  응답 결과가 어떻게 나오는지 미리 받고, JSON을 파싱할 모델을 만들 것이기에 Postman을 통해 미리 요청 테스트를 하면 편하다.

 

Download Postman | Get Started for Free

Try Postman for free! Join 25 million developers who rely on Postman, the collaboration platform for API development. Create better APIs—faster.

www.postman.com

 

 

 

 

  이 때, Header에 각자 받은 Kakao REST API Key 꼭 입력해야한다.

입력할 때 주의점은 'AK 각자받은키' 형식으로 넣어야한다는 것. (AK와 각자받은키 사이에 띄어쓰기 1번 해줘야함)

 

 

 

 

 

   입력이 끝났다면, Send를 누른다.

그렇다면 하단처럼 JSON 형태의 데이터를 받을 수 있다.

 

 

 

 

 

 

3. 원하는 데이터 형태 만들기

 

Instantly parse JSON in any language | quicktype

 

app.quicktype.io

 

 

이제 이거를 다 복사해서 (command + C) 상단의 사이트에 붙여넣는다. (하단 사진 참고)

 

 

  오른쪽에 원하는 언어 (Swift) 선택, 데이터가 없는 경우를 방지하여 최하단의 옵셔널을 선택(이건 선택사항)해주면 된다. 

왼쪽처럼 Swift 구조체 형식으로 데이터가 정리된 것을 확인할 수 있다.

 

 

 

  이제 실제로 데이터를 요청해서 원하는 형태로 담기 위해(Parsing 하기 위해) 필요한 '원하는 형태'를 만들 것이다.

따라서 상단의 정리된 Swift타입 구조체를 복사해서 Xcode에 붙여넣는다.

 

 

 

  그리고 필요한 데이터만 남겨놓고 지운다.

 

 

 

  지울 때 유의할 점이 짝꿍도 같이 지워야한다.

예를 들어, Meta에 isEnd를 지우고 싶다면 하단 CodingKeys에 있는 isEnd도 같이 지워야한다. (하단 사진 참고)

 

 

 

 

4. 실제 요청 보내서 화면에 띄우기 (feat. 소스코드)

  이제 데이터를 걸러받을 깔떼기를 만들었으니, 실제 요청을 보내면 된다.

보다 편리하게 Http 통신을 하기 위해 필자는 Alamofire를 사용했다.

 * 헤더 상수 *
    private let headers : HTTPHeaders = [
        "Authorization": "KakaoAK 개인kakaoAPI키값"
    ]
* 파라미터 *    
    // 요청할 때 사용할 파라미터 함수
    private func keywordParameters(query: String, lon: String, lat: String, page: Int) -> [String: Any] {
        [
            "query": query,
            "x": lon,
            "y": lat,
            "page": page
        ]
    }
 * 요청 코드 *
 
    /// 키워드로 검색하기 (상호명, 주소, 건물명 등을 '키워드', '위도 경도'로 검색)
    func searchKeyword(with keyword: String, lon: String, lat: String, page: Int, completion: @escaping (KeywordResult) -> Void) {
        let url = "https://dapi.kakao.com/v2/local/search/keyword.json"

        AF.request(url,
                   method: .get,
                   parameters: keywordParameters(query: keyword,  // 위에서 만든 파라미터 함수의 리턴값 사용
                                                 lon: lon,
                                                 lat: lat,
                                                 page: page),
                   encoding: URLEncoding.default,
                   headers: headers) // 위에서 만든 헤더 상수 넣기
        .validate(statusCode: 200..<600) // 위치 정보가 잘못된 경우를 대비하여 범위를 600미만까지 설정
        .responseDecodable(of: KeywordResult.self) { response in
            let result = response.result
            
		// 응답결과는 Result 타입으로 되어있으므로 성공의 경우와 실패의 경우 switch문으로 분기처리
            switch result {
            case .success(let searchResult):
            // 디버깅 코드
                print("keyword : \(keyword)")
                print("lon : \(lon)")
                print("lat: \(lat)")
                // 성공할 경우, completionBlock에 검색 결과 데이터 넘겨주기
                completion(searchResult)
            case .failure(let error):
            // 실패한 경우 error 프린트
                print(error)
            }
        }
    }

 

 

 

 

* 전체 요청 코드 * 

import Foundation
import Alamofire

class HttpClient {
    
    static let shared = HttpClient()
    
    private init() { }
    private let headers : HTTPHeaders = [
        "Authorization": "KakaoAK 개인kakaoAPI키값"
    ]
    
    // MARK: - Functions
    
    private func keywordParameters(query: String, lon: String, lat: String, page: Int) -> [String: Any] {
        [
            "query": query,
            "x": lon,
            "y": lat,
            "page": page
        ]
    }
    
    /// 키워드로 검색하기 (상호명 등을 검색)
    func searchKeyword(with keyword: String, lon: String, lat: String, page: Int, completion: @escaping (KeywordResult) -> Void) {
        let url = "https://dapi.kakao.com/v2/local/search/keyword.json"

        AF.request(url,
                   method: .get,
                   parameters: keywordParameters(query: keyword,
                                                 lon: lon,
                                                 lat: lat,
                                                 page: page),
                   encoding: URLEncoding.default,
                   headers: headers)
        .validate(statusCode: 200..<600)
        .responseDecodable(of: KeywordResult.self) { response in
            let result = response.result
            switch result {
            case .success(let searchResult):
                print("keyword : \(keyword)")
                print("lon : \(lon)")
                print("lat: \(lat)")
                completion(searchResult)
            case .failure(let error):
                print(error)
            }
        }
    }

 

  completion에 담은 결과값을 자유롭게 원하는 형태로 만들어 사용하면 된다.

 

 

 

  필자는 MVVM 패턴으로 만들고 있고 controller가 비대해지는 것을 방지하기 위해

ViewModel에서 CoreLocation으로부터 받은 현재 좌표를 사용하여 입력된 키워드로 목록을 받도록 구현했다. (하단 코드 참고)

* View Model 코드 *
// 현재 위치가 담겨있는 변수 longtitude, latitude가 초기화 되어있는 상태

    func getKeywordSearchResult(with keyword: String, completion: @escaping([KeywordDocument]) -> Void) {
        guard let lon = longitude,
              let lat = latitude else {
            print(#function)
            return
        }
        showProgressHUD()
        HttpClient.shared.searchKeyword(with: keyword,
                                        lon: lon,
                                        lat: lat,
                                        page: searchPage) { [weak self] result in
            guard let keywordResultArray = result.documents,
                  let totalPage = result.meta?.pageableCount,
                      totalPage > 1 else {
                print("SearchVM - 결과 없음")
                self?.dismissProgressHUD()
                return
            }
            
            // 검색 히스토리 배열에 추가하기
            let newHistory = SearchHistory(type: UIImage(systemName: "magnifyingglass")!, searchText: keyword)
            
            if (self?.searchHistories) != nil {
                print("SearchVM - newHistory KEYWORD : \(keyword)")
                self?.searchHistories?.insert(newHistory, at: 0)
                self?.dismissProgressHUD()
                completion(keywordResultArray)
            } else {
                print("SearchVM - newHistory KEYWORD 로 배열 초기화")
                self?.searchHistories = [newHistory]
                self?.dismissProgressHUD()
                completion(keywordResultArray)
            }
        }
    }

 

 

 

 

그리고 검색 버튼이 눌리면, 검색 결과를 검색 결과를 띄울 SearchResultVC에 넘겨주었다. 

SearchVC - 검색하기 버튼 눌리면 실행되는 함수

*SearchResultVC 생성자 코드

SearchResultVC - 생성자에서 검색 결과 넘겨받아서 viewModel 초기화하기

 

 

  마지막으로 넘겨받은 Result 데이터로 tableView cell의 UI를 세팅했다.

SearchResultVC - TableView

 

 

 

 

 

결과

 

장소 이미지 띄우는 것과 별점 기능은 아직 구현하지 않았기에....

 

일단 키워드로 검색하기 끝!

728x90