https://apis.map.kakao.com
카카오에서는 위에 링크를 들어가면 확인할 수 있듯이 개발자들이 카카오맵을 사용할 수 있도록 API를 제공합니다.
KakaoMapsSDK 프로젝트를 시작하기 위해서는 다음과 같은 방법들을 거쳐야합니다.
프로젝트 시작
KakaoMapsSDK를 Swift Package Manager(SPM) 또는 Cocoapods 중 원하는 방법을 사용하여 프로젝트에 적용합니다.
저는 간단하게 SPM를 사용하여 프로젝트에 적용시켜보겠습니다.
https://github.com/kakao-mapsSDK/KakaoMapsSDK-SPM.git 링크를 위에 SearchBar에 입력하여 SPM을 프로젝트에 추가해줍니다.
이후 패키지가 추가되고 나면 아래 코드처럼 KakaoMapsSDK를 import 할 수 있게 됩니다.
import KakaoMapsSDK
...
사용 등록
KAKAO_APP_KEY 발급하기
KakaoMapsSDK 설치한 후, KakaoMapsSDK를 사용할 KAKAO_APP_KEY를 등록해야 KakaoMapsSDK를 사용할 수 있습니다. 아래는 KAKAO_APP_KEY를 발급받고 앱에 등록하는 절차입니다.
API Key를 발급 받는 첫 번째 단계는 애플리케이션을 추가하는것 입니다.
위에 단계를 거쳐 애플리케이션을 생성하였다면 다음과 같이 API를 요청할 때 사용되는 Key를 발급받았음을 확인할 수 있습니다.
이후 iOS 플랫폼을 등록해야합니다.
네이티브 앱 키 발급 및 앱에 추가
1. 앱을 등록하는 절차를 마쳤으면 발급 받은 네이티브 앱 키를 앱에 추가합니다.
2. SDKInitializer.InitSDK() 함수를 사용하여 발급받은 KAKAO_APP_KEY를 앱에 추가합니다.
import SwiftUI
import KakaoMapsSDK
@main
struct DiseaseTrackerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
SDKInitializer.InitSDK("YOUR_KAKAO_APP_KEY")
}
}
}
}
주의할점
KAKAO_APP_KEY는 앱의 보안을 위해 외부에 노출되지 않도록 주의해야 합니다. 따라서, KAKAO_APP_KEY는 소스코드에 직접 입력하지 않고, 별도의 파일에 저장하여 사용하는 것이 좋습니다. SDKInitializer.InitSDK()는 엔진 시작전에 호출되어야 합니다.
인증절차
KakaoMapsSDK를 사용하기 위해서는 인증 절차를 거쳐야 합니다. 인증은 Kako Developers에 등록한 앱키를 기반으로 이루어집니다. 엔진을 시작하면 자동으로 인증 절차가 진행됩니다. 아래 그림은 인증 처리 과정을 도식화한 그림으로 카카오에서 제공합니다.
인증 처리 과정은 내부적으로 비동기적으로 진행되어 성공할 경우 계속해서 API를 사용할 수 있습니다. 만약 인증에 실패하게 되면 엔진이 강제로 정지되고 API를 사용할 수 없게 됩니다. 인증 결과에 따라 MapControllerDelegate의 delegate 함수가 호출되며, 성공시에는 별다른 작업이 필요없지만 실패했을 경우에는 API를 사용할 수 없게 되므로 이에 대한 적절한 추가 처리 작업이 필요할 수 있습니다. 인증이 실패할 경우에는 인증 실패에 대한 에러코드가 함께 전달됩니다.
주의
인증실패시 prepareEngine 함수를 다시 호출하여 인증을 다시 시도할 수 있습니다. 이 때, 앱 키 오류등의 이유로 재시도해도 실패할 수 밖에 없는 경우 실패 –> 재시도 –> 실패 –> 재시도… 와 같은 무한루프에 빠지지 않도록 주의해야 합니다.
override func viewDidLoad() {
super.viewDidLoad()
mapContainer = self.view as? KMViewContainer
//KMController 생성.
mapController = KMController(viewContainer: mapContainer!)!
mapController!.delegate = self
mapController?.prepareEngine() //엔진 prepare
}
override func viewWillAppear(_ animated: Bool) {
if _auth {
if mapController?.isEngineActive == false {
mapController?.activateEngine()
}
}
}
// MapControllerDelegate. 인증에 성공했을 경우 호출.
func authenticationSucceeded() {
print("성공")
_auth = true
}
// MapControllerDelegate. 인증에 실패했을 경우 호출.
// errorCode : 에러 코드
// desc : 에러 디스크립션
func authenticationFailed(_ errorCode: Int, desc: String) {
print("error code: \(errorCode)")
print("\(desc)")
// 추가 실패 처리 작업
mapController?.prepareEngine() //인증 재시도
}
인증 실패 시의 에러코드는 아래와 같습니다.
Error CodeDescription
특별히 499 에러의 경우 인증서버와의 통신에서 문제가 생겨서 발생하는데, 네트워크 상태가 다시 정상으로 돌아왔다고 판단하여 재시도 할 수 있습니다. 이 때에는 KMController의 authenticate 함수를 직접 호출하여 인증을 다시 시도할 수 있습니다.
View Controls
KakaoMapsSDK의 뷰와 라이브사이클 컨트롤
View와 Controller
KakaoMapsSDK는 엔진부와 앱 인터페이스 부분으로 나누어집니다.
엔진은 KMViewContainer 에 내장되어 지도가 그려지는 뷰를 제공하는 역할
KMController은 엔진-앱 인터페이스 연결 및 엔진의 상태 관리 담당하는 역할
KakaoMapsSDK를 사용하기 위해서는 이 두 클래스를 사용해야 합니다.
KMViewContainer
KakaoMapsSDK에서는 UIView를 상속한 View 클래스로 KMViewContainer 클래스를 제공합니다. 지도 표시 및 API 사용을 위해서는 ViewController에 KMViewContainer 클래스를 추가해야합니다. KMViewContainer는 그려질 ViewBase의 영역을 제공하며, ViewBase는 뷰 안에 그려지는 모든 렌더링 컨텐츠와 이에 대한 컨트롤 및 이벤트를 제공합니다. 여러개의 뷰를 생성하는 경우에는 개별 뷰는 모두 각각 KMViewContainer안에서 고유의 영역을 가지게 되며, API내부의 렌더링 및 컨트롤의 처리 단위가 됩니다.
API에는 ViewBase 서브클래스로 KakaoMap 클래스를 제공합니다. KakaoMap은 지도, 스카이뷰 및 3D스카이뷰를 그릴 수 있습니다. 그리고자 하는 지도 종류에 따라 적절한 viewInfo를 이용하여 ViewBase를 생성할 수 있고, 필요에 따라 이미 생성된 ViewBase의 viewInfo를 교체하여 다른 종류의 지도로 전환할 수도 있습니다.
KMController
KMController는 KakaoMapsSDK를 사용하기 위한 인증 절차 및 내부 엔진부분과 앱 인터페이스부분과의 연결, API의 상태등을 관리합니다. KMController로 엔진의 초기화 및 Start와 Stop, 그리고 렌더링의 Status를 컨트롤 할 수 있습니다. 사용자가 원하는 시점에서 Engine의 start및 stop, 렌더링 시작/중지등을 KMController를 통해 직접 명시적으로 컨트롤 할 수 있습니다.
Life Cycle
사용자는 KMController의 인터페이스를 통해 엔진의 상태를 관리해야 합니다. 아래 그림은 KakaoMapsSDK의 Life Cycle 및 상태 전이 과정을 나타낸 것입니다. 노란색 사각형은 각각 엔진의 상태를 의미하며, 녹색 사각형은 delegate로 사용자에게 전달되는 부분을 의미합니다. 빨간색 화살표는 사용자의 함수 호출에 의한 상태 전이를 나타내며, 검은색 화살표는 내부적으로 상태 전이에 따라 자동적으로 진행되는 부가 과정을 나타냅니다.
prepareEngine을 호출하면 엔진 준비 작업이 진행되고 완료되면 엔진 대기 상태가 됩니다. 이때 SDK가 인증되지 않은 상태면 백그라운드에서 인증을 진행합니다. 인증이 실패하면 엔진이 강제로 종료되어 초기 상태로 돌아가고 authenticationFailed delegate가 호출됩니다.
activateEngine를 호출하면 활성화 상태가 되고 시스템 Vsync에 엔진 렌더링 함수를 등록합니다. 이 상태에서는 지속적으로 시스템으로부터 VSync 호출을 받아 변경된 그릴 내용이 있을 때 렌더링을 수행합니다. 추가된 ViewBase가 없어 그릴 것이 없는 경우 addViews delegate를 호출하여 ViewBase를 추가하라는 메시지를 전달합니다. Delegate 함수에서 addView함수를 호출하여 사용할 ViewBase를 추가할 수 있습니다. KMController의 addView함수의 실제 동작은 비동기로 렌더링 과정의 준비작업 내에서 수행되고 결과는 delegate를 통해 전달됩니다.
pauseEngine을 호출하면 렌더링을 멈추고 비활성화 상태가 됩니다. 엔진의 반복적인 렌더링 작업은 중단되지만 뷰에 그려진 내용은 유지됩니다. 앱이 백그라운드로 전환되어야 할 경우처럼 렌더링을 수행하지 않아야 할 때 pauseEngine을 호출하여 엔진을 비활성화 시켜야 합니다. activateEngine을 호출하여 다시 활성화상태로 돌아갈 수 있습니다.
resetEngine을 호출하면 엔진이 정지되고, 모든 렌더링에 사용했던 resource를 release 한 후 엔진 초기 상태로 돌아갑니다. 이때 기존에 추가했던 ViewBase및 SDK를 통해 생성했던 POI, GUI등의 객체들도 모두 SDK에서 삭제됩니다. 다시 prepareEngine을 호출하기 전까지 엔진을 사용할 수 없습니다.
주의
addView함수의 처리는 비동기로 렌더링 과정에서 수행됩니다. 엔진이 지도 렌더링 상태가 아닐 경우 addView를 호출하여도 실제 수행은 되지 않습니다. 경우에 따라 화면에 보이기 전에 지도에 대한 초기 설정을 수행하기를 원할 수 있습니다.
이런 경우 뷰가 보이기 전에 (ex. viewWillAppear 가 호출되는 시점) activateEngine을 호출하고 addViews delegate를 통해 ViewBase를 추가할 수 있습니다. 초기 세팅 작업이 많을 경우 일시적으로 뷰를 안보이게 하는 등의 처리가 필요할 수도 있습니다.
주의
SDK는 KMViewContainer의 사이즈가 변할 경우 delegate로 이를 전달하지만, 앱 초기화 흐름상 addView 함수를 호출하는 시점에서 KMViewContainer의 사이즈를 모르거나 addView가 완료되기 전에 layout이 완료되어 이를 전달받지 못할 가능성이 있습니다.
이런 경우 addViewSucceeded delegate에서 사이즈를 다시 지정해 주는 것이 필요할 수 있습니다.
KakaoMapsSDK 120Hz로 사용하기
방법은 다음과 같습니다. ProMotion Display를 지원하여 아래와 같이 KMController의 proMotionSupport를 true로 설정하면 사용할 수 있습니다. 별도로 설정하지 않았을 경우에는 기본값 false가 설정됩니다.
override func viewDidLoad() {
mapController = KMController(viewContainer: mapContainer!)!
mapController?.proMotionSupport = true;
}
iPhone13, iPhone13 Pro Max의 경우 아래의 Key를 Info.plist에 추가해야 프로모션이 지원됩니다.
또한, KMViewContainer.proMotionDisplay를 통해서 현재 단말기의 ProMotion Display를 지원 여부를 알 수 있습니다.
override func viewDidLoad() {
mapContainer = self.view as? KMViewContainer
mapController = KMController(viewContainer: mapContainer!)!
// 프로모션 지원 여부 확인
if mapContainer.proMotionDisplay == true {
// 프로모션 Display 사용 설정
mapController?.proMotionSupport = true;
}
}
지도 구성
본격적으로 지도를 그리기 전에 지도가 어떻게 구성되어 있는지를 확인하겠습니다.
KakaoMap은 크게 baseMap, overlay, poi, shape(polygon, polyline 등), gui 로 구성되어 있습니다. 그 중에 baseMap과 overlay는 지도의 배경을 구성하는 요소입니다.
BaseMap 및 overlay의 종류
BaseMap 종류
Overlay 종류
BaseMap 및 overlay 사용 방법
ViewInfo
ViewInfo란 KakaoMapsSDK가 사용가능한 데이터 중에 어떤 데이터를 가져와서, 어떻게 그릴지에 대한 부분을 정의하고 있는 데이터입니다. ViewInfo를 통해서 정의되는 항목에는 좌표계, baseMap 및 사용 가능한 오버레이, 지도 최소/최대 레벨, POI 등이 포함됩니다. KakaoMapsSDK 서버에는 각각의 app 마다 사용하는 데이터의 종류와 이들을 통해 구성된 viewInfo 가 정의되어 있습니다.
KakaoMapsSDK는 지도를 그릴때 어떤 viewInfo를 사용할지가 필요하므로 사용자는 사용할 viewInfo의 이름을 지정해야 합니다. 이를 위해, ViewInfo 생성시 사용할 appName과 viewInfoName을 지정하게 됩니다. AppName은 기본값으로 “openmap” 을 사용하게 되고, “openmap” 의 내용과 다른 요구사항이 있어 커스터마이징이 필요한 경우에는 별도로 문의하여 협의를 통해 app을 추가할 수 있습니다. 즉, 서버를 통해 별도로 지정되지 않은 앱은 appName으로 “openmap”, viewInfoName으로 “map"을 사용하면 됩니다. BaseMap 변경은 KakaoMap.changeViewInfo API를 통해 변경할 수 있습니다.
BaseMap의 변경
BaseMap은 viewInfo에 정의되어 있습니다. 그러므로 표시할 baseMap을 변경하려면 사용하는 viewInfo를 변경해주어야 합니다. 아래 예시는 changeViewInfo 함수를 통해서 viewInfo를 변경하는 코드입니다.
/// 세가지 종류의 base map을 제공한다. 각 base map으로 변경은 KakaoMap 객체의 viewInfoName을 통해 변경할 수 있다.
///
/// - kakao_map : 일반 2d 지도
///
/// - skyview: 스카이뷰
///
/// - cadastral_map : 지적편집도
let mapView: KakaoMap = mapController?.getView("mapview") as! KakaoMap
mapView.changeViewInfo(appName: "openmap", viewInfoName: "skyview")
BaseMap 변경 콜백
KakaoMap.changeViewInfo 함수를 호출하여 베이스맵을 변경했을 때, 성공/실패 여부를 콜백으로 전달받을 수 있습니다. KakaoMapEventDelegate의 viewInfo 변경 성공/실패 프로토콜을 구현하여 변경에 성공 혹은 실패한 viewInfo 이름을 받아올 수 있습니다.
// KakaoMap.changeViewInfo의 성공 및 실패 여부를 콜백으로 전달받을 수 있다.
extension ChangeBaseMapSample: KakaoMapEventDelegate {
// changeViewInfo 성공시 호출
func onViewInfoChanged(kakaoMap: KakaoMap, viewInfoName: String) {
print("\(kakaoMap.viewName()) success to change viewInfo \(viewInfoName)")
}
// changeViewInfo 실패 호출
func onViewInfoChangeFailure(kakaoMap: KakaoMap, viewInfoName: String) {
print("\(kakaoMap.viewName()) fail to change viewInfo \(viewInfoName)")
}
}
오버레이 표시하기
오버레이는 KakaoMap.showOverlay API를 호출하여 표시할 수 있습니다. 표시하고자 하는 Overlay의 이름을 파라미터로 전달하여 오버레이를 ON/OFF 할 수 있습니다.
mapView.showOverlay("traffic_info")
예를 들어, 스카이뷰를 baseMap으로 표시하면서 하이브리드를 함께 표시하려면 하이브리드 오버레이를 켜주면 됩니다.
오버레이 숨기기
오버레이는 KakaoMap.hideOverlay API를 호출하여 숨길 수 있습니다. 숨기고자 하는 오버레이의 이름을 파라미터로 전달하여 오버레이를 끕니다.
mapView.hideOverlay("traffic_info")
BaseMap 및 overlay 사용권한
App 에 따라 사용할 수 있는 baseMap 및 overlay 의 종류가 정해져 있습니다. 아래는 app 에 상관없이 일반적으로 사용 가능 여부를 표로 나타낸 것입니다.
지도 그리기
드디어 지도를 그리는 차례가 왔습니다. SwiftUI 관점에서 정리하겠습니다.
KakaoMapsSDK에서 제공하고 있는 지도를 표시하는 KMViewContainer는 UIKit을 기반으로 만들어졌기 때문에 SwiftUI에서 사용하기 위해서는 UIViewRepresentable로 Wrapping해야 사용할 수 있습니다. 또한 SwiftUI View의 라이프 사이클에 따라서 KMViewContainer를 컨트롤 하기 위해 KMControllerDelegate를 Coordinator로 구현할 수 있다고 합니다.
아래 코드는 카카오에서 제공하고 있는 UIViewRepresentable을 이용하여 KMViewContainer를 Wrapping하고, Coordinator를 KMControllerDelegate로 구현한 예제입니다.
struct ContentView: View {
@State var draw: Bool = false //뷰의 appear 상태를 전달하기 위한 변수.
var body: some View {
KakaoMapView(draw: $draw).onAppear(perform: {
self.draw = true
}).onDisappear(perform: {
self.draw = false
}).frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct KakaoMapView: UIViewRepresentable {
@Binding var draw: Bool
/// UIView를 상속한 KMViewContainer를 생성한다.
/// 뷰 생성과 함께 KMControllerDelegate를 구현한 Coordinator를 생성하고, 엔진을 생성 및 초기화한다.
func makeUIView(context: Self.Context) -> KMViewContainer {
let view: KMViewContainer = KMViewContainer()
view.sizeToFit()
context.coordinator.createController(view)
context.coordinator.controller?.prepareEngine()
return view
}
/// Updates the presented `UIView` (and coordinator) to the latest
/// configuration.
/// draw가 true로 설정되면 엔진을 시작하고 렌더링을 시작한다.
/// draw가 false로 설정되면 렌더링을 멈추고 엔진을 stop한다.
func updateUIView(_ uiView: KMViewContainer, context: Self.Context) {
if draw {
context.coordinator.controller?.activateEngine()
}
else {
context.coordinator.controller?.resetEngine()
}
}
/// Coordinator 생성
func makeCoordinator() -> KakaoMapCoordinator {
return KakaoMapCoordinator()
}
/// Cleans up the presented `UIView` (and coordinator) in
/// anticipation of their removal.
static func dismantleUIView(_ uiView: KMViewContainer, coordinator: KakaoMapCoordinator) {
}
/// Coordinator 구현. KMControllerDelegate를 adopt한다.
class KakaoMapCoordinator: NSObject, KMControllerDelegate {
override init() {
first = true
super.init()
}
// KMController 객체 생성 및 event delegate 지정
func createController(_ view: KMViewContainer) {
controller = KMController(viewContainer: view)
controller?.delegate = self
}
// KMControllerDelegate Protocol method구현
/// 엔진 생성 및 초기화 이후, 렌더링 준비가 완료되면 아래 addViews를 호출한다.
/// 원하는 뷰를 생성한다.
func addViews() {
let defaultPosition: MapPoint = MapPoint(x: 14135167.020272, y: 4518393.389136)
let mapviewInfo: MapviewInfo = MapviewInfo(viewName: "mapview", viewInfoName: "map", defaultPosition: defaultPosition)
controller?.addView(mapviewInfo)
}
//addView 성공 이벤트 delegate. 추가적으로 수행할 작업을 진행한다.
func addViewSucceeded(_ viewName: String, viewInfoName: String) {
print("OK") //추가 성공. 성공시 추가적으로 수행할 작업을 진행한다.
}
//addView 실패 이벤트 delegate. 실패에 대한 오류 처리를 진행한다.
func addViewFailed(_ viewName: String, viewInfoName: String) {
print("Failed")
}
/// KMViewContainer 리사이징 될 때 호출.
func containerDidResized(_ size: CGSize) {
let mapView: KakaoMap? = controller?.getView("mapview") as? KakaoMap
mapView?.viewRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
if first {
let cameraUpdate: CameraUpdate = CameraUpdate.make(target: MapPoint(x: 14135167.020272, y: 4518393.389136), zoomLevel: 10, mapView: mapView!)
mapView?.moveCamera(cameraUpdate)
first = false
}
}
var controller: KMController?
var first: Bool
}
}
Fatal Error (중요)
코드를 적용시키는 도중 아래와 같은 에러를 확인하게 되었습니다. 카카오에서 KMController는 만들었으면서 KMControllerDelegate를 만들어놓지 않았다는 것을 확인할 수 있었으며 조금 더 찾아본 결과 카카오에서는 그다지 SwiftUI를 완벽히 지원하지 않아 기다려야할 것 같습니다.
'SwiftUI' 카테고리의 다른 글
커스텀 원형 테두리 만들기 (1) | 2024.05.01 |
---|---|
Cocoapods (0) | 2024.05.01 |
Protocols 프로토콜 (0) | 2024.04.27 |
UIViewControllerRepresentable (0) | 2024.04.27 |
PreferenceKey (0) | 2024.04.25 |