본문 바로가기
스파르타코딩 클럽/팀프로젝트

팀프로젝트2 [공유 킥보드 앱 만들기(4) - 네이버 커스텀 마커, 버튼 이벤트]

by UDDT 2025. 5. 1.



 팀 프로젝트 4일차

    지도에서 킥보드를 선택할 수 있게 하려면, 지도 위에 킥보드가 올라가 있어야 한다.

  네이버 Map 문서를 보니, 마커라는 기능이 있어 이를 활용해보고자 한다.

 

 네이버 마커

    https://navermaps.github.io/ios-map-sdk/guide-ko/5-2.html

 

마커 · NAVER Map iOS SDK

No results matching ""

navermaps.github.io

 

   마커는 지도상의 한 지점을 나타내는 오버레이로, 우리가 네이버 맵을 사용하면서 자주 보던 UI다.

  마커는 지도상 좌표에 아이콘 형태로 표시되는데, 무언가 검색했을 때 뜨던 이 아이콘이 바로 마커이다.

이 녹색 원이 마커

// 네이버에서 제공하는 예제
let marker = NMFMarker()
marker.position = NMGLatLng(lat: 37.5670135, lng: 126.9783740)
marker.mapView = mapView

 

   마커는 위의 코드처럼 비교적 간단하게 맵에 띄울 수 있다(lat, lng은 좌표)

 

 마커 커스텀하기

    마커에는 iconImage라는 속성이 있고, 이를 활용하면 마커를 커스텀할 수 있다.

 아이콘을 지정하기에 앞서 먼저 NMGOverlayImage 객체를 만들어야 한다.

 (NMGOverlayImage : 오버레이에서 사용할 수 있는 비트맵 이미지를 나타내는 클래스)

// 내가 작성한 코드
marker.iconImage = NMFOverlayImage(name: "kickBoard")
marker.position = NMGLatLng(lat: 37.5557, lng: 126.9708)

 

  위의 코드처럼 이미지를 넣어주고, 좌표를 정해주면 해당 좌표에 마커가 찍히게 된다.

 

   iconTIntColor로 마커의 색상을 변경해줄 수도 있고, 크기의 변경도 가능하다

//네이버에서 제공하는 예제
marker.iconImage = NMF_MARKER_IMAGE_BLACK
marker.iconTintColor = UIColor.red
marker.width = 25
marker.height = 40

 

 마커에 이벤트 적용하기

    https://navermaps.github.io/ios-map-sdk/guide-ko/5-1.html

 

오버레이 공통 · NAVER Map iOS SDK

오버레이 공통 오버레이는 지리적 정보를 시각적으로 나타내는 요소로, 개발자가 지도 위에 자유롭게 배치할 수 있습니다. 네이버 지도 SDK는 마커, 정보 창, 셰이프 등 다양한 유형의 오버레이

navermaps.github.io

 

    네이버는 기본적으로, 오버레이의 이벤트를 도와주는 overlay handler를 제공한다.

overlay.touchHandler = { (overlay: NMFOverlay) -> Bool in
    print("오버레이 터치됨")
    return true
}

    overlay에 touchHandler 속성을 지정하면, 터치 이벤트가 발생했을 때 클로저 블럭이 실행된다.

 

    그런데 여기서 궁금한게 하나 생긴다.

   왜 overlay를 받아서 Bool Type을 리턴할까?

 

   기본적으로 네이버 지도는 수많은 요소들이 겹쳐있는 View다

  요소들은 대부분 터치 이벤트를 소화할 수 있게 되어 있는데

  그렇다면, 누가 이 이벤트를 처리할지 결정해야 한다

 

  예를 들어보면,
  1. 마커 위에 유저가 손가락으로 터치
  2. 1번 상황에서 마커가 터치 이벤트를 받음
  3. 마커의 touchHandler 클로저가 실행됨
  4. touchHandler 클로저가 true를 리턴
      - "나 마커인데, 이벤트 여기서 처리했다~"
      - 다른 오버레이가 이 이벤트를 더이상 처리하지 않게 됨
  5. touchHandler 클로저가 false를 리턴
     - "나 마커인데, 이벤트 처리 안할게"
     - 다른 오버레이가 이 이벤트를 처리할 수 있게 됨

 

// true 리턴 시
marker.touchHandler = { overlay in
    // 마커 클릭 시 원하는 동작 수행
    print("마커 클릭됨")

    return true  // 여기서 이벤트를 끝냄 (마커가 터치 이벤트를 전부 처리한 것)
}

// false 리턴 시
marker.touchHandler = { overlay in
    print("터치 이벤트를 실행하지 않음")

    return false  // 터치 이벤트가 지도나 다른 오버레이로 계속 전달됨
}

 

 userInfo

    네이버 맵은 오버레이에 추가적인 정보를 지정할 수 있도록 userInfo 속성을 제공하는데,

  userInfo는 기본적으로 딕셔너리 타입이며, 터치 이벤트 리스너와 결합해 사용할 수 있다.

    touchHandler에서 userInfo에 저장된 정보를 확인해서 어떤 오버레이가 탭 되었는지를 식별할 수 있어

  여러 오버레이가 터치 핸들러를 공유하도록 구현할 수 있다.

// 네이버 제공 예제
let handler: overlayTouchHandler = { (overlay) -> Bool in
    print("마커 \(overlay.userInfo["tag"] ?? "tag") 터치됨")
    return false
}

marker1.userInfo = ["tag": 1]
marker2.userInfo = ["tag": 2]

marker1.touchHandler = handler
marker2.touchHandler = handler

 

   나는 위의 정보들을 사용하여 아래와 같은 코드를 작성했다.

    private func setMarker() {
        let markers: [NMFMarker] = [
            NMFMarker(position: NMGLatLng(lat: 37.5557, lng: 126.9708)),
            NMFMarker(position: NMGLatLng(lat: 37.5560, lng: 126.9720)),
            NMFMarker(position: NMGLatLng(lat: 37.5570, lng: 126.9700)),
            NMFMarker(position: NMGLatLng(lat: 37.5550, lng: 126.9750)),
            NMFMarker(position: NMGLatLng(lat: 37.5530, lng: 126.9686))
        ]

        // 네이버에서 제공하는 Overlay 핸들러(네이버 지도 마커의 터치 이벤트를 처리하는 클로저)
        let handler: (NMFOverlay) -> Bool = { [weak self] overlay in
            guard let marker = overlay as? NMFMarker else { return false }

            // 유저 Info 딕셔너리를 디코딩해서 쓰는 방법 ??? enum이나 어딘가에 담아서 써야 함
            let isSelected = marker.userInfo["selected"] as? Bool ?? false
            if isSelected {
                marker.iconImage = NMFOverlayImage(name: "kickBoard")
                marker.userInfo["selected"] = false
            } else {
                marker.iconImage = NMFOverlayImage(name: "seletedKickBoard")
                marker.userInfo["selected"] = true
                self?.rentButton.isHidden = false
            }
            
            return true
        }

        for marker in markers {
            marker.mapView = myView.mapView
            marker.iconImage = NMFOverlayImage(name: "kickBoard")
            marker.userInfo["selected"] = false
            marker.touchHandler = handler
        }
    }

 

   상세히 보자면, 

// 희망하는 위도와 경도에 배열로 마커를 만들어주었다
        let markers: [NMFMarker] = [
            NMFMarker(position: NMGLatLng(lat: 37.5557, lng: 126.9708)),
            NMFMarker(position: NMGLatLng(lat: 37.5560, lng: 126.9720)),
            NMFMarker(position: NMGLatLng(lat: 37.5570, lng: 126.9700)),
            NMFMarker(position: NMGLatLng(lat: 37.5550, lng: 126.9750)),
            NMFMarker(position: NMGLatLng(lat: 37.5530, lng: 126.9686))
        ]
        /* 
        네이버에서 제공하는 Overlay 핸들러(네이버 지도 마커의 터치 이벤트를 처리하는 클로저)
        핸들러는 NMFOverlay를 받아서 Bool Type을 retur함
        */
        let handler: (NMFOverlay) -> Bool = { [weak self] overlay in
            // overlay는 다양하므로 NMFMarKer로 다운캐스팅. 만약 캐스팅이 안되면 false 리턴
            guard let marker = overlay as? NMFMarker else { return false }

            // isSelected라는 상수에 marker.userInfo["seleted"]라는 키가 있는지 확인.
            // 값이 없으면 기본적으로 false. 값이 있으면 Bool로 타입 캐스팅을 함
            let isSelected = marker.userInfo["selected"] as? Bool ?? false
            
            // isSelected가 true면
            if isSelected {
                // marker의 iconImage를 기본 이미지로 세팅하고, false로 변경
                marker.iconImage = NMFOverlayImage(name: "kickBoard")
                marker.userInfo["selected"] = false
                // 버튼을 감추는 것으로 설정
                self?.rentButton.isHidden = true
              // 만약 isSelected가 false면
            } else {
                // marker의 iconImage를 선택 이미지로 변경하고
                marker.iconImage = NMFOverlayImage(name: "seletedKickBoard")
                marker.userInfo["selected"] = true
                // 버튼을 보여주는 것으로 설정
                self?.rentButton.isHidden = false
            }
            
            return true
        }
        // markers 배열에서 marker를 하나씩 순회하면서
        for marker in markers {
            // marker를 mapView에 반영해주고
            marker.mapView = myView.mapView
            // iconImage를 설정해주고 
            marker.iconImage = NMFOverlayImage(name: "kickBoard")
            // 기본값을 false로 지정함
            marker.userInfo["selected"] = false
            // marker에 handler를 달아줌
            marker.touchHandler = handler
        }

 

  위의 코드는 userInfo["selected"]가 직접 입력하게 되어 있어서 안전한 코드가 아니다.

 따라서 휴먼 에러를 줄이기 위해 해당 Key값을 enum Type으로 변경하였다

enum MarkerUserInfo: String {
    case seleted = "seleted"
}
    private func setMarker() {
        let markers: [NMFMarker] = [
            NMFMarker(position: NMGLatLng(lat: 37.5557, lng: 126.9708)),
            NMFMarker(position: NMGLatLng(lat: 37.5560, lng: 126.9720)),
            NMFMarker(position: NMGLatLng(lat: 37.5570, lng: 126.9700)),
            NMFMarker(position: NMGLatLng(lat: 37.5550, lng: 126.9750)),
            NMFMarker(position: NMGLatLng(lat: 37.5530, lng: 126.9686))
        ]

        // 네이버에서 제공하는 Overlay 핸들러(네이버 지도 마커의 터치 이벤트를 처리하는 클로저)
        let handler: (NMFOverlay) -> Bool = { [weak self] overlay in
            guard let marker = overlay as? NMFMarker else { return false }
            // 유저 Info 딕셔너리를 디코딩해서 쓰는 방법 ??? enum이나 어딘가에 담아서 써야 함
            let isSelected = marker.userInfo[MarkerUserInfo.seleted.rawValue] as? Bool ?? false
            if isSelected {
                marker.iconImage = NMFOverlayImage(name: "kickBoard")
                marker.userInfo[MarkerUserInfo.seleted.rawValue] = false
                self?.rentButton.isHidden = true
            } else {
                marker.iconImage = NMFOverlayImage(name: "seletedKickBoard")
                marker.userInfo[MarkerUserInfo.seleted.rawValue] = true
                self?.rentButton.isHidden = false
            }

            return true
        }

        for marker in markers {
            marker.mapView = myView.mapView
            marker.iconImage = NMFOverlayImage(name: "kickBoard")
            marker.userInfo[MarkerUserInfo.seleted.rawValue] = false
            marker.touchHandler = handler
        }
    }

 

  이렇게 작성했더니, 마커에 터치 이벤트를 핸들링할 수 있게 되었다.

 

  

   그러나 위의 화면처럼 중복 선택이 가능한 문제가 있어

  다음 시간에는 예외처리에 집중하고자 한다

최근댓글

최근글

skin by © 2024 ttuttak