[개발일지] 05. 홈 독서 지도를 어떻게 구현했을까요? (MapKit 커스텀 + 항공 경로 시각화)

2026. 3. 16. 11:41·Flyleaf - 독서를 여행처럼/개발일지
728x90

MapTile을 적용하다

디자인 요구사항상 미니멀한 블랙 톤의 지도 스타일을 구현해야 했습니다.

하지만 MapKit의 기본 지도 스타일(standard, hybrid, imagery)만으로는 한계가 있었습니다.

 

도로나 지형, 지명, 텍스트, POI(장소 정보) 등과 같은 노이즈는 MapView의 pointOfInterestFilter 속성을 .excludingAll로 처리하여 제거할 수 있었지만, 지도 자체의 디자인(대륙 색상, 바다 색상 등)은 여전히 커스터마이징이 어려웠습니다. 

 

결국 기존 지도를 수정하는 방식으로는 한계가 있다고 판단했고, 지도 자체를 직접 구성하는 방향으로 접근을 전환했습니다. 이 과정에서 MapTile 개념을 알게 되었고, MapKit에서도 MKTileOverlay를 통해 타일 기반 지도를 적용할 수 있다는 점을 확인했습니다.

 

이를 통해 커스텀 타일 지도를 적용했고, 결과적으로 원하는 블랙 톤 기반의 미니멀한 지도 스타일을 구현할 수 있었습니다.

 

pointOfInterestFilter

MapKit이 자동으로 그려주는 POI(장소 정보)를 제어하는 속성입니다. 카페, 식당, 편의점, 병원, 학교와 같은 장소 정보를 필터링(제어)할 수 있습니다. 

// 특정 카테고리 포함
let filter = MKPointOfInterestFilter(including: [.cafe, .restaurant, .hotel])
mapView.pointOfInterestFilter = filter

// 특정 카테고리 제외
let filter = MKPointOfInterestFilter(excluding: [.parking, .gasStation])
mapView.pointOfInterestFilter = filter

// 모든 POI 숨김
mapView.pointOfInterestFilter = .excludingAll

// 모든 POI 표시
mapView.pointOfInterestFilter = .includingAll

위 코드처럼 특정 카테고리만 포함하거나, 특정 카테고리를 제외하거나, 모든 POI를 표시할 수도 있지만, Flyleaf 홈맵에서는 POI 정보가 필요하지 않기 때문에 .excludingAll을 사용하여 모든 POI를 숨김처리 했습니다.


MapTile?

맵타일은 지도를 작은 이미지 조각으로 나눠서 보여주는 방식입니다.

조금 쉽게 이야기해보자면, 지도 한 장이 아니라 

타일 타일 타일
타일 타일 타일
타일 타일 타일

이렇게 쪼개진 이미지(타일)들을 이어붙여서 지도처럼 보이게 하는 방식입니다.


MKTileOverlay

MapKit에서는 MKTileOverlay를 사용해서 맵타일을 적용합니다.

 

1. 타일 지도 생성

let tileOverlay = MKTileOverlay(
  urlTemplate: MapTile.darkNolabels
)
tileOverlay.canReplaceMapContent = true
mapView.addOverlay(tileOverlay, level: .aboveRoads)

 

urlTemplate는 타일 이미지를 가져오는 주소입니다.

https://tile.server/{z}/{x}/{y}.png

URL은 보통 위와 같은 형태이며, x, y는 타일 위치이고 z는 줌 레벨을 의미합니다. 

즉, 해당 위치 (x, y) 타일 이미지를 가져오고, 확대하면 z가 증가합니다.

 

보통 URL은 MapTile을 제공하는 서비스의 URL을 사용하기 때문에 URL자체에 신경 쓸 필요는 없습니다!

 

tileOverlay.canReplaceMapContent = true

canReplaceMapContent는 MapKit에서  지도의 기본 타일(배경 지도)을 커스텀 타일로 완전히 대체할 수 있는지 여부를 나타내는 속성입니다.

 

false이면 기본 지도 타일 위에 커스텀 타일을 오버레이로 덮어씌웁니다.

true이면 Apple 기본 지도 타일을 완전히 숨기고 커스텀 타일만 표시합니다. 

 

쉽게 생각하면, Apple 지도를 완전히 다른 지도로 커스텀하고 싶을때 canReplaceMapContent 속성을 true로 설정합니다.

 

mapView.addOverlay(tileOverlay)

addOverlay는 지도 위에 도형, 선, 타일 등을 추가하는 메소드입니다. 

 

2. 렌더러 등록

오버레이를 추가했다고 타일이 바로 적용되지 않습니다. delegate에서 렌더러를 반환해야 실제로 맵타일이 그려집니다.

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
  ) -> MKOverlayRenderer {
    if let tileOverlay = overlay as? MKTileOverlay {
      return MKTileOverlayRenderer(tileOverlay: tileOverlay)
    }
    return MKOverlayRenderer(overlay: overlay)
}

공항 어노테이션 표시

디자인 요구사항상, 실제 공항 위치에 출발지와 도착지를 명확하게 표시해야 했습니다. 애플 디벨로퍼 아카데미 스터디에서 MapKit의 어노테이션 기능을 사용해본 경험이 있었기 때문에, 이번에도 MapKit의 어노테이션을 활용하는 방향으로 구현을 시작했습니다. 

 

공항 어노테이션는 아래와 같은 요구사항/조건이 있었습니다.

1. 출발 공항인지 도착 공항인지 구분 되어야 함. (이륙 아이콘, 착륙 아이콘으로 구별)

2. 공항 코드가 함께 보여야 함.

 

즉, 단순한 마커/핀이 아닌 공항 정보를 가진 UI요소가 필요했습니다.

 

어노테이션 모델 정의

그래서 가장 먼저, 지도에 올릴 데이터를 명확하게 표현하기 위해 어노테이션 모델을 정의했습니다.

final class AirportAnnotation: NSObject, MKAnnotation {
  let iconType: AirportIconType
  let code: String
  let coordinate: CLLocationCoordinate2D
  
  init(
    iconType: AirportIconType,
    code: String,
    coordinate: CLLocationCoordinate2D,
  ) {
    self.iconType = iconType
    self.code = code
    self.coordinate = coordinate
  }
}

이렇게 모델을 분리한 이유는 단순히 좌표만 넘기기보다, 렌더링에 필요한 정보까지 하나의 타입으로 묶어두기 위해서였습니다.

 

특히 iconType은 출발/도착 공항에 따라 서로 다른 아이콘을 보여주기 위해 필요했고, code는 공항의 IATA 코드를 마커에 함께 표시하기 위해 추가했습니다. 


커스텀 어노테이션 뷰 구현

그 다음으로는 이 모델을 기반으로, 공항 마커를 원하는 디자인으로 출력할 수 있는 커스텀 어노테이션 뷰를 만들었습니다. 

final class AirportAnnotationView: MKAnnotationView

MapKit에서는 지도 위의 어노테이션을 표시할 때 기본적으로 MKAnnotaionView를 사용합니다. 기본 제공 스타일로는 앱에서 원하는 UI를 만들기 어려웠기 때문에, MKAnnotaionView를 상속한 AirportAnnotationView를 만들어 공항 마커를 직접 구성했습니다.

 

특히 커스텀 어노테이션 뷰에서 중요했던 부분은 prepareForReuse() 부분이었습니다.

override func prepareForReuse() {
  super.prepareForReuse()
  codeLabel.text = nil
  iconImageView.image = nil
}

MapKit의 어노테이션 뷰도 테이블뷰 셀처럼 재사용 됩니다. 

즉, 지도를 이동하거나 줌을 변경하는 과정에서 새로운 어노테이션 뷰가 계속 생성되는 것이 아니라, 기존 뷰를 다시 꺼내서 다른 어노테이션에 재사용할 수 있습니다. 

 

이때 이전 어노테이션의 표시 정보가 남아 있으면, 다른 공항이 같은 뷰를 재사용하는 순간 잘못된 IATA 코드나 아이콘이 잠깐 보이는 문제가 생길 수 있습니다.

 

그래서 prepareForReuse()에서 이전 상태를 초기화해주는 처리가 필요했습니다. 위 코드에서는 codeLabel.text와 iconImageView.image를 비워서, 재사용되는 순간 이전 데이터가 남아 있지 않도록 처리했습니다.

 

마찬가지로 어노테이션을 지도에 표시하려면 mapView(_:viewFor:) 델리게이트 메서드를 구현해야 합니다. 

public func mapView(
  _ mapView: MKMapView,
  viewFor annotation: any MKAnnotation
) -> MKAnnotationView? {
  if let airportAnnotation = annotation as? AirportAnnotation {
    let view = mapView.dequeueReusableAnnotationView(
      withIdentifier: AirportAnnotationView.identifier
    ) as? AirportAnnotationView ?? AirportAnnotationView(
      annotation: airportAnnotation,
      reuseIdentifier: AirportAnnotationView.identifier
    )

    view.annotation = airportAnnotation
    view.configure(annotation: airportAnnotation)
    return view
}

전달 받은 어노테이션의 타입을 확인한 후, dequeueReusableAnnotationView로 재사용 가능한 뷰를 꺼내고, 없으면 새로 생성합니다. 이후 configure() 메소드를 통해 데이터를 주입하고 뷰를 반환하면 지도에 마커가 표시됩니다.


경로 표시

공항 위치를 표시한 다음에는, 출발지와 도착지를 하나로 연결해 보여주는 경로 표현이 필요했습니다. 그래서 두 공항 좌표를 연결하는 방식으로 경로를 함께 출력하기로 했고, MapKit에서 경로를 표현할 수 있는 MKPolyline을 사용해 구현했습니다.

 

MKPolyline

MKPolyline은 여러 좌표를 순서대로 이어 선을 그리는 클래스입니다. 

 

1. 출발지와 도착지 좌표를 연결하는 overlay 생성

let coordinates = [departure, arrival]
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)

- coordinates: CLLocationCoordinate2D 배열로, 선을 이을 좌표들입니다. 배열 순서대로 점과 점이 연결됩니다.

- count: 배열 요소의 개수입니다. 배열 길이를 별도로 넘겨줘야합니다.

 

coordinates 배열은 출발지와 도착지 두 점만 있으니 직선 하나가 그려집니다.

 

2. 렌더러 등록

맵타일과 마찬가지로 polyline도 렌더러를 반환해야 지도에 표시됩니다.

public func mapView(
  _ mapView: MKMapView,
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
  if let polyline = overlay as? MKPolyline {
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.strokeColor = .n0.withAlphaComponent(0.5)
    renderer.lineWidth = 0.5
    renderer.lineCap = .round
    renderer.lineJoin = .round
    return renderer
  }

  return MKOverlayRenderer(overlay: overlay)
}

그래서 mapView(_:rendererFor:) delegate 메소드에서 MKPolylineRenderer를 반환하도록 구현했습니다.

 

polyline도 조금의 커스텀이 가능합니다.

- strokeColor: 라인 색상

- lineWidth: 라인 두께

- lineCap: 선 끝부분 모양

- lineJoin: 선이 꺾이는 부분 모양


비행기 어노테이션 표시

커스텀 어노테이션

공항 어노테이션과 마찬가지로, 비행기 어노테이션 역시 별도의 어노테이션으로 분리해 구현했습니다.

 

비행기는 단순히 고정된 위치에 표시되는 요소가 아니라, 사용자의 독서 진행률에 따라 위치가 계속 변경되는 동적인 요소였기 때문에 공항 어노테이션과는 성격이 조금 달랐습니다.

 

그래서 비행기 위치를 표시하기 위한 전용 어노테이션 모델을 따로 정의했습니다.

final class FlightAnnotation: NSObject, MKAnnotation {
  dynamic var coordinate: CLLocationCoordinate2D
  
  init(coordinate: CLLocationCoordinate2D) {
    self.coordinate = coordinate
  }
}

여기서 중요한 부분은 coordinate를 dynamic으로 선언한 것입니다.

 

MapKit은 어노테이션의 위치가 변경되었을 때 이를 감지해 화면을 갱신해야 하는데, 이때 내부적으로 KVO를 사용합니다.

coordinate를 dynamic으로 선언해야 MapKit이 이 변화를 감지할 수 있고, 그래야 비행기 위치를 업데이트했을 때 지도에서도 자연스럽게 이동하는 것처럼 보이게 됩니다.

 

그 다음으로는 비행기 마커를 위한 커스텀 어노테이션 뷰 MKAnnotaionView를 상속하여 구현했습니다.

final class FlightAnnotationView: MKAnnotationView

 

비행기 어노테이션 뷰 역시 재사용되기 때문에, 이전 상태가 남아 있으면 문제가 발생할 수 있습니다. 이후에 이야기 하겠지만, 비행기 어노테이션은 회전이 적용되기 때문에 재사용되는 과정에서 이전 각도가 그대로 남아 있으면 새로운 위치의 비행기가 엉뚱한 방향을 바라보는 문제가 발생할 수 있습니다.

override func prepareForReuse() {
  super.prepareForReuse()
  imageView.transform = .identity
}

그래서 prepareForReuse()에서 transform을 초기 상태(.identity)로 되돌려, 이 문제를 방지했습니다.


위치 계산

비행기 어노테이션은 단순히 출발지와 도착지의 중간 위치에 고정으로 놓은게 아니라, 현재 독서 진행률에 따라 경로 위의 특정 지점에 배치되도록 구현해야했습니다.

 

여기서 중요한 건, 위치 계산이었습니다.

 

출발 좌표와 도착 좌표만 가지고도 선을 그릴 수는 있지만, 비행기를 그 선위의 어디에 둘지는 별도의 계산이 필요했습니다. 

 

비행기 어노테이션의 위치 계산을 처음 구현할 때는, 출발지와 도착지 사이의 중간 좌표를 단순히 계산해서 배치하는 방식으로 접근했습니다. 처음에는 이 방식이 가장 직관적이라고 생각했습니다. 출발 좌표와 도착 좌표가 있으니, 두 점 사이를 progress 비율만큼 보간하면 경로 위 어딘가의 좌표가 나올 것이라고 판단했기 때문입니다.

func interpolatedCoordinate(
  start: CLLocationCoordinate2D,
  end: CLLocationCoordinate2D,
  progress: Double
) -> CLLocationCoordinate2D {
  let latitude = start.latitude + (end.latitude - start.latitude) * progress
  let longitude = start.longitude + (end.longitude - start.longitude) * progress
  
  return CLLocationCoordinate2D(
    latitude: latitude,
    longitude: longitude
  )
}

 

이렇게 계산한 좌표로 비행기 어노테이션을 찍었을 때, 처음 화면에서는 경로 위에 잘 올라가 있는 것처럼 보였습니다.

하지만 지도를 확대하면 비행기 어노테이션이 경로선에 위치한 것이 아니라, 조금씩 경로를 벗어나 있는 것처럼 보였습니다. 처음에는 렌더링 오차인가 싶었지만, 좌표 계산 방식에 문제가 있었습니다.

 

처음 구현은 위도/경도 값을 단순히 선형 보간한 방식이었는데, MapKit에서 실제로 지도 위에 그려지는 경로는 단순한 위도/경도 직선이 아니라 Map Projection을 거쳐 표현됩니다. 

 

즉, 제가 계산한 좌표는 위도/경도 기준 중간 지점이었고, 실제 화면에 그려진 경로는 지도 투영 이후의 선이었기 때문에 줌 레벨이 바뀌거나 확대해서 볼수록 두 결과 사이의 차이가 발생한 것이었습니다.

 

이 문제를 해결하기 위해, 위도/경도를 직접 보간하는 방식 대신 MapKit이 실제로 사용하는 좌표계인 MKMapPoint 기준으로 다시 계산하는 방식으로 접근을 수정했습니다.

func interpolatedCoordinate(
  start: CLLocationCoordinate2D,
  end: CLLocationCoordinate2D,
  progress: Double
) -> CLLocationCoordinate2D {
  let startPoint = MKMapPoint(start)
  let endPoint = MKMapPoint(end)
  
  let x = startPoint.x + (endPoint.x - startPoint.x) * progress
  let y = startPoint.y + (endPoint.y - startPoint.y) * progress
  
  return MKMapPoint(x: x, y: y).coordinate
}

비행기의 방향

비행기 어노테이션의 위치를 경로 위에 정확히 올린 이후, 다음으로 해결해야 했던 문제는 비행기의 방향이었습니다.

 

처음 구현에서는 단순히 비행기 아이콘을 경로 위에 올려두기만 했기 때문에, 아이콘이 항상 같은 방향을 바라보고 있었습니다. 하지만 실제로는 출발지에서 도착지로 이동하는 방향을 따라 비행기의 머리가 향해있어야(?) 했었습니다.

 

방향을 계산하기 위해 단순히 출발지와 도착지 좌표를 사용하는 대신,  두 좌표를 기준으로 방향 벡터를 계산하는 방식으로 접근했습니다. (구글링하고 AI한테 물어보니까 접선 기반 방향 계산 방식이 있다고 하네요.. atan2와 같은.. 그래서 그걸 활용하여 구현했습니다. 마침 swift에 atan2가 구현되어 있더군요!)

 

- atan2

https://en.wikipedia.org/wiki/Atan2

2D 평면에서 벡터의 방향을 계산할 때는 atan2(dy, dx)가 표준적으로 사용되며, 이는 x/y의 부호를 모두 고려하여 올바른 방향 각도를 반환한다.

 

- 접선 벡터

곡선 위 한 점에서의 방향 = 접선 벡터이고, 이 벡터는 경로의 진행 방향을 나타낸다.

 

- 두 점으로 방향 구하는 방식

벡터 = (end - start) why? 벡터 연산은 두 점의 차이!

방향 = atan2(dy, dx)


func flightRotationAngle(
  start: CLLocationCoordinate2D,
  end: CLLocationCoordinate2D
) -> CGFloat {
  let dx = end.longitude - start.longitude
  let dy = end.latitude - start.latitude
  
  return atan2(dy, dx)
}

처음에는 가장 단순하게, 출발지와 도착지의 위경도 차이를 이용해 방향 벡터를 계산하는 방식으로 접근했습니다. 즉, 지도 위 두 좌표를 기준으로 벡터를 만들고, 그 벡터의 각도를 atan2로 계산해 비행기 어노테이션에 적용하는 방식이었습니다. 

 

view.setRotation(angle)

계산된 각도는 어노테이션 뷰에 전달되어 비행기 이미지의 transform으로 적용됩니다.

 

func setRotation(_ angle: CGFloat) {
  imageView.transform = CGAffineTransform(rotationAngle: angle)
}

setRotation 메소드는 커스텀 어노테이션 뷰에 구현한 커스텀 메소드입니다.

 

?!

여기까지 구현하고 나니 또 하나의 문제가 발생했습니다. 지도를 회전시키면, 비행기도 같이 회전하면서 진행 방향과 상관없이 이상한 각도를 바라보는 현상이 나타났습니다.

 

MapKit의 어노테이션은 지도 좌표계 기준으로 회전됩니다. 즉, 지도 자체가 회전하면 그 위에 올라간 어노테이션도 함께 회전합니다.

 

결과적으로, 계산한 방향과 지도 자체의 회전 값 이 두 개가 동시에 적용되면서 비행기의 방향이 의도와 다르게 보이게 된 것입니다.

 

처음에는 이 문제를 해결하기 위해 지도의 회전값을 직접 보정하는 방법도 고민했습니다.

let mapRotation = mapView.camera.heading * (.pi / 180)
let finalAngle = angle - mapRotation

이 방식은 지리 좌표 기준으로 계산한 각도에서 지도 회전값을 빼주는 방식인데, 결국 이 문제를 더 근본적으로 해결하려면 애초에 방향을 계산하는 기준 자체를 바꾸는 것이 더 낫다고 판단했습니다.

 

그래서 최종적으로는 위경도 기준 계산을 버리고, 경로 앞뒤 두 지점을 화면 좌표로 변환한 뒤 방향을 계산하는 방식으로 수정했습니다.

func flightRotationAngle(
  start: CLLocationCoordinate2D,
  end: CLLocationCoordinate2D,
  progress: Double
) -> CGFloat {
  let previousProgress = max(0.0, progress - 0.01)
  let nextProgress = min(1.0, progress + 0.01)

  let previousCoordinate = interpolatedCoordinate(
    start: start,
    end: end,
    progress: previousProgress
  )

  let nextCoordinate = interpolatedCoordinate(
    start: start,
    end: end,
    progress: nextProgress
  )

  let previousPoint = mapView.convert(previousCoordinate, toPointTo: mapView)
  let nextPoint = mapView.convert(nextCoordinate, toPointTo: mapView)

  let dx = nextPoint.x - previousPoint.x
  let dy = nextPoint.y - previousPoint.y

  return atan2(dy, dx)
}

이 방식의 핵심은 mapView.convert(_:toPointTo:)였습니다. 이 메소드는 좌표를 단순히 위경도로 다루는 것이 아니라, 현재 지도 상태(확대, 이동, 회전)가 모두 반영된 실제 화면 좌표로 변환해줍니다.

 

즉, 방향 계산을 지리 좌표계가 아니라 사용자가 지금 보고 있는 화면 좌표계 기준으로 바꾼 것입니다.


마무리

이번 구현은 .. 쉽지 않은 구현이었습니다. 좌표계 표현 방식이나 거리 벡터.. atan2와 같은 생소한 개념을 활용해야했기 때문에 조금은 어려웠지만! 재밌었던 경험이었습니다. 

 

맵킷.. 그동안 마커 찍기 정도만 다뤄보아서 이렇게 어려울 줄 몰랐습니다요..! 그래도 이번 기회로 지도 좌표계와 화면 좌표계의 차이를 이해하게 된 것 같고, 잊었던 거리 벡터에 대해 다시 한 번 생각해보게 된 재밌고 좋은 경험이었던 것 같습니다!

 

끗!


레퍼런스

https://developer.apple.com/documentation/spatial/angle2d/atan2(y:x:)

https://blog.spiralmoon.dev/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%B4%EB%A1%A0-%EB%91%90-%EC%A0%90-%EC%82%AC%EC%9D%B4%EC%9D%98-%EC%A0%88%EB%8C%80%EA%B0%81%EB%8F%84%EB%A5%BC-%EC%9E%AC%EB%8A%94-atan2

https://en.wikipedia.org/wiki/Atan2

https://lime-juice.tistory.com/entry/%EC%9D%B4%EB%A1%A0-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B2%A1%ED%84%B0-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B1%B0%EB%A6%AC-%EA%B0%81%EB%8F%84-%EA%B3%84%EC%82%B0

https://openmaptiles.org/

https://developer.apple.com/documentation/mapkit/mappolyline

 

'Flyleaf - 독서를 여행처럼 > 개발일지' 카테고리의 다른 글

[개발일지] 07. Tuist Scaffold로 모듈 생성 자동화하기  (0) 2026.03.24
[개발일지] 06. 단위 테스팅을 해봅시다! (feat. AI)  (0) 2026.03.20
[개발일지] 04. Composition Root (feat. 어디까지 몰라야하는가?)  (0) 2026.03.11
[개발일지] 03. Flyleaf의 Micro-Features Architecture 구조  (0) 2026.03.10
[개발일지] 02. Micro-Features Architecture가 무엇이고, Flyleaf에 왜 적용했을까요?  (0) 2026.03.10
'Flyleaf - 독서를 여행처럼/개발일지' 카테고리의 다른 글
  • [개발일지] 07. Tuist Scaffold로 모듈 생성 자동화하기
  • [개발일지] 06. 단위 테스팅을 해봅시다! (feat. AI)
  • [개발일지] 04. Composition Root (feat. 어디까지 몰라야하는가?)
  • [개발일지] 03. Flyleaf의 Micro-Features Architecture 구조
여성일
여성일
  • 여성일
    성일노트
    여성일
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Flyleaf - 독서를 여행처럼
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 탭탭 - TapTap
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 애플 디벨로퍼 아카데미
        • 챌린지 회고
        • 하루의 날씨
      • Swift Student Challenge 202..
      • AI를 잘쓰는 개발자가 될래요
      • 우리 같이 협업하자
      • 사카마카 (살까말까 고민 될 때는 사카마카)
      • Book2OnNon (모바일 서재)
      • 바꿔조 (환율 계산기)
      • iOS
        • iOS
        • Vapor
        • Design Pattern
        • CoreData
        • Tuist
        • RxSwift
        • ReactorKit
        • TCA
      • Swift
        • Swift 기본기
        • UIkit
        • SwiftUI
      • UX, 사용성
      • 원티드 프리온보딩 챌린지 iOS 과정
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    여성일
    [개발일지] 05. 홈 독서 지도를 어떻게 구현했을까요? (MapKit 커스텀 + 항공 경로 시각화)
    상단으로

    티스토리툴바