2026년 MapKit SwiftUI 면접: 어노테이션, 오버레이, 지오로케이션

iOS 면접을 위한 SwiftUI와 MapKit 마스터하기: 사용자 정의 어노테이션, 오버레이, 지오로케이션, 장소 검색, Maps 통합 패턴.

iOS 개발자를 위한 MapKit SwiftUI 면접 질문

지도는 많은 iOS 애플리케이션에서 핵심 기능입니다: 배달, 피트니스, 부동산, 위치 기반 소셜 네트워크 등이 있습니다. iOS 17부터 MapKit은 지도 통합을 크게 단순화하는 네이티브 SwiftUI API를 제공합니다. 채용 담당자는 기술 면접에서 이러한 기술을 정기적으로 평가합니다.

가이드 구조

각 질문은 실제 기술 면접 형식을 반영하며, 자세한 답변과 작동하는 코드를 포함합니다. 개념은 기초부터 고급까지 진행됩니다.

SwiftUI에서의 지도 기초

질문 1: SwiftUI로 기본 지도를 표시하려면 어떻게 합니까?

iOS 17부터 SwiftUI는 이전의 UIViewRepresentable 접근 방식을 대체하는 네이티브 Map 컴포넌트를 제공합니다. Map 뷰는 카메라 위치와 표시할 콘텐츠를 받습니다.

BasicMapView.swiftswift
import SwiftUI
import MapKit

struct BasicMapView: View {
    // Camera position controlling center and zoom
    @State private var cameraPosition: MapCameraPosition = .automatic

    var body: some View {
        // Basic map without additional content
        Map(position: $cameraPosition)
    }
}

// Map centered on a specific region
struct CenteredMapView: View {
    // Defines a region centered on Paris
    @State private var cameraPosition: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            // Span defines the zoom level (in degrees)
            span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
        )
    )

    var body: some View {
        Map(position: $cameraPosition) {
            // Map content (markers, annotations, etc.)
        }
        .mapStyle(.standard) // Map style: standard, imagery, hybrid
    }
}

position 속성은 다양한 모드를 허용하는 열거형인 MapCameraPosition을 사용합니다: .automatic(자동 조정), .region(고정 영역), .camera(완전 제어) 또는 .userLocation(사용자 중심).

질문 2: 사용 가능한 지도 스타일에는 어떤 것들이 있습니까?

MapKit은 다양한 사용 사례에 적합한 여러 지도 스타일을 제공합니다. .mapStyle() 수정자는 지도의 시각적 모습을 구성합니다.

MapStyles.swiftswift
import SwiftUI
import MapKit

struct MapStylesDemo: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var selectedStyle: MapStyleOption = .standard

    var body: some View {
        VStack {
            Map(position: $position)
                .mapStyle(selectedStyle.style)
                .frame(height: 400)

            // Style selector
            Picker("Style", selection: $selectedStyle) {
                ForEach(MapStyleOption.allCases) { option in
                    Text(option.name).tag(option)
                }
            }
            .pickerStyle(.segmented)
            .padding()
        }
    }
}

enum MapStyleOption: String, CaseIterable, Identifiable {
    case standard
    case imagery
    case hybrid
    case standardElevated
    case imageryElevated

    var id: String { rawValue }

    var name: String {
        switch self {
        case .standard: return "Standard"
        case .imagery: return "Satellite"
        case .hybrid: return "Hybrid"
        case .standardElevated: return "Terrain"
        case .imageryElevated: return "Satellite 3D"
        }
    }

    var style: MapStyle {
        switch self {
        case .standard:
            // Classic road map
            return .standard
        case .imagery:
            // Satellite view without labels
            return .imagery
        case .hybrid:
            // Satellite with roads and names
            return .hybrid
        case .standardElevated:
            // Standard with 3D terrain
            return .standard(elevation: .realistic)
        case .imageryElevated:
            // Satellite with 3D terrain
            return .imagery(elevation: .realistic)
        }
    }
}
관심 지점

.standard 스타일은 표시되는 카테고리를 필터링하는 pointsOfInterest 매개변수를 받습니다: .including([.restaurant, .cafe]) 또는 .excluding([.nightlife]).

질문 3: 지도에 마커와 어노테이션을 추가하려면 어떻게 합니까?

마커는 지도에서 관심 지점을 나타냅니다. SwiftUI MapKit은 여러 유형을 제공합니다: Marker(표준 핀), Annotation(사용자 정의 뷰), MapCircle/MapPolygon(기하학적 도형).

MarkersAndAnnotations.swiftswift
import SwiftUI
import MapKit

// Place model conforming to Identifiable
struct Place: Identifiable {
    let id = UUID()
    let name: String
    let coordinate: CLLocationCoordinate2D
    let category: PlaceCategory
}

enum PlaceCategory {
    case restaurant, hotel, attraction

    var icon: String {
        switch self {
        case .restaurant: return "fork.knife"
        case .hotel: return "bed.double"
        case .attraction: return "star"
        }
    }

    var tint: Color {
        switch self {
        case .restaurant: return .orange
        case .hotel: return .blue
        case .attraction: return .purple
        }
    }
}

struct MarkersMapView: View {
    @State private var position: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
        )
    )

    let places: [Place] = [
        Place(name: "Eiffel Tower", coordinate: CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945), category: .attraction),
        Place(name: "Le Meurice", coordinate: CLLocationCoordinate2D(latitude: 48.8651, longitude: 2.3281), category: .hotel),
        Place(name: "Le Jules Verne", coordinate: CLLocationCoordinate2D(latitude: 48.8583, longitude: 2.2944), category: .restaurant)
    ]

    var body: some View {
        Map(position: $position) {
            // Markers with system icon
            ForEach(places) { place in
                Marker(place.name, systemImage: place.category.icon, coordinate: place.coordinate)
                    .tint(place.category.tint)
            }
        }
    }
}

// Custom annotations with SwiftUI view
struct CustomAnnotationsView: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var selectedPlace: Place?

    let places: [Place] = [] // Data

    var body: some View {
        Map(position: $position, selection: $selectedPlace) {
            ForEach(places) { place in
                // Annotation with custom view
                Annotation(place.name, coordinate: place.coordinate) {
                    PlaceAnnotationView(place: place, isSelected: selectedPlace?.id == place.id)
                }
            }
        }
    }
}

// Custom view for annotation
struct PlaceAnnotationView: View {
    let place: Place
    let isSelected: Bool

    var body: some View {
        VStack(spacing: 4) {
            // Icon with colored background
            Image(systemName: place.category.icon)
                .font(.system(size: isSelected ? 20 : 16))
                .foregroundStyle(.white)
                .padding(8)
                .background(place.category.tint)
                .clipShape(Circle())
                .shadow(radius: 4)

            // Name displayed when selected
            if isSelected {
                Text(place.name)
                    .font(.caption)
                    .fontWeight(.medium)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(.ultraThinMaterial)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            }
        }
        .animation(.spring(duration: 0.3), value: isSelected)
    }
}

핵심 차이점: Marker는 Apple Maps의 네이티브 렌더링(표준 핀)을 사용하는 반면, Annotation은 모든 SwiftUI 뷰로 완전한 사용자 정의를 허용합니다.

지오로케이션 및 권한

질문 4: 위치 권한을 처리하고 사용자 위치를 표시하려면 어떻게 합니까?

지오로케이션은 명시적인 권한과 다양한 권한 부여 상태의 적절한 처리가 필요합니다. CLLocationManager가 권한 요청을 처리합니다.

LocationManager.swiftswift
import SwiftUI
import MapKit
import CoreLocation

@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    // Location state
    var location: CLLocation?
    var authorizationStatus: CLAuthorizationStatus = .notDetermined
    var isAuthorized: Bool {
        authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways
    }

    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        // Get current status
        authorizationStatus = manager.authorizationStatus
    }

    // Request location permission
    func requestAuthorization() {
        manager.requestWhenInUseAuthorization()
    }

    // Start location tracking
    func startUpdatingLocation() {
        guard isAuthorized else { return }
        manager.startUpdatingLocation()
    }

    // Stop tracking to save battery
    func stopUpdatingLocation() {
        manager.stopUpdatingLocation()
    }

    // MARK: - CLLocationManagerDelegate

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus

        if isAuthorized {
            startUpdatingLocation()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // Take the most recent position
        location = locations.last
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Location error: \(error.localizedDescription)")
    }
}

// View with map and user location
struct UserLocationMapView: View {
    @State private var locationManager = LocationManager()
    @State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)

    var body: some View {
        Map(position: $cameraPosition) {
            // Display the blue user location dot
            UserAnnotation()
        }
        .mapControls {
            // Button to recenter on position
            MapUserLocationButton()
            // Compass
            MapCompass()
            // Scale
            MapScaleView()
        }
        .onAppear {
            locationManager.requestAuthorization()
        }
        .overlay(alignment: .top) {
            if !locationManager.isAuthorized {
                PermissionBanner()
            }
        }
    }
}

// Banner requesting authorization
struct PermissionBanner: View {
    var body: some View {
        HStack {
            Image(systemName: "location.slash")
            Text("Enable location in Settings")
            Spacer()
            Button("Open") {
                if let url = URL(string: UIApplication.openSettingsURLString) {
                    UIApplication.shared.open(url)
                }
            }
            .buttonStyle(.bordered)
        }
        .padding()
        .background(.ultraThinMaterial)
    }
}
필수 Info.plist

NSLocationWhenInUseUsageDescription 및/또는 NSLocationAlwaysUsageDescription 키는 위치 사용에 대한 명확한 설명과 함께 Info.plist에 정의되어야 합니다.

질문 5: 실시간으로 위치를 추적하고 경로를 그리려면 어떻게 합니까?

실시간 추적은 피트니스 또는 내비게이션 앱에 필수적입니다. 이 질문은 위치 데이터의 연속적인 스트림을 처리하는 능력을 시험합니다.

RouteTrackingView.swiftswift
import SwiftUI
import MapKit
import CoreLocation

@Observable
class RouteTracker: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    // Position history for the trace
    var routeCoordinates: [CLLocationCoordinate2D] = []
    var currentLocation: CLLocation?
    var isTracking = false

    // Route statistics
    var totalDistance: CLLocationDistance = 0
    private var lastLocation: CLLocation?

    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        // Update even in background
        manager.allowsBackgroundLocationUpdates = true
        manager.pausesLocationUpdatesAutomatically = false
    }

    func startTracking() {
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
        isTracking = true
        routeCoordinates = []
        totalDistance = 0
        lastLocation = nil
    }

    func stopTracking() {
        manager.stopUpdatingLocation()
        isTracking = false
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let newLocation = locations.last else { return }

        // Filter inaccurate positions
        guard newLocation.horizontalAccuracy < 20 else { return }

        currentLocation = newLocation
        routeCoordinates.append(newLocation.coordinate)

        // Calculate distance traveled
        if let last = lastLocation {
            totalDistance += newLocation.distance(from: last)
        }
        lastLocation = newLocation
    }
}

struct RouteTrackingView: View {
    @State private var tracker = RouteTracker()
    @State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)

    var body: some View {
        ZStack {
            Map(position: $cameraPosition) {
                // Current position
                UserAnnotation()

                // Route trace as polyline
                if tracker.routeCoordinates.count > 1 {
                    MapPolyline(coordinates: tracker.routeCoordinates)
                        .stroke(.blue, lineWidth: 4)
                }
            }
            .mapStyle(.standard)

            // Control interface
            VStack {
                Spacer()

                // Statistics
                HStack {
                    VStack(alignment: .leading) {
                        Text("Distance")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                        Text(formatDistance(tracker.totalDistance))
                            .font(.title2)
                            .fontWeight(.bold)
                    }
                    Spacer()
                }
                .padding()
                .background(.ultraThinMaterial)
                .clipShape(RoundedRectangle(cornerRadius: 12))
                .padding()

                // Start/stop button
                Button {
                    if tracker.isTracking {
                        tracker.stopTracking()
                    } else {
                        tracker.startTracking()
                    }
                } label: {
                    Label(
                        tracker.isTracking ? "Stop" : "Start",
                        systemImage: tracker.isTracking ? "stop.fill" : "play.fill"
                    )
                    .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .tint(tracker.isTracking ? .red : .green)
                .padding(.horizontal)
                .padding(.bottom)
            }
        }
    }

    private func formatDistance(_ meters: CLLocationDistance) -> String {
        if meters < 1000 {
            return String(format: "%.0f m", meters)
        } else {
            return String(format: "%.2f km", meters / 1000)
        }
    }
}

iOS 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

오버레이와 기하학적 도형

질문 6: 지도에 사용자 정의 오버레이를 그리려면 어떻게 합니까?

오버레이는 지도에 영역, 경로 또는 지역을 표시할 수 있게 합니다. SwiftUI MapKit은 MapCircle, MapPolygon, MapPolyline을 제공합니다.

MapOverlays.swiftswift
import SwiftUI
import MapKit

struct DeliveryZoneMapView: View {
    @State private var position: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            span: MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15)
        )
    )

    // Delivery zones with different times
    let deliveryZones: [DeliveryZone] = [
        DeliveryZone(
            name: "Express Zone",
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            radius: 2000,
            deliveryTime: "15 min",
            color: .green
        ),
        DeliveryZone(
            name: "Standard Zone",
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            radius: 5000,
            deliveryTime: "30 min",
            color: .orange
        )
    ]

    var body: some View {
        Map(position: $position) {
            // Circles for delivery zones
            ForEach(deliveryZones) { zone in
                MapCircle(center: zone.center, radius: zone.radius)
                    .foregroundStyle(zone.color.opacity(0.2))
                    .stroke(zone.color, lineWidth: 2)
            }

            // Restaurant marker
            Marker("Restaurant", systemImage: "storefront", coordinate: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522))
                .tint(.red)
        }
        .overlay(alignment: .bottomLeading) {
            // Legend
            VStack(alignment: .leading, spacing: 8) {
                ForEach(deliveryZones) { zone in
                    HStack {
                        Circle()
                            .fill(zone.color)
                            .frame(width: 12, height: 12)
                        Text("\(zone.name) - \(zone.deliveryTime)")
                            .font(.caption)
                    }
                }
            }
            .padding()
            .background(.ultraThinMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 8))
            .padding()
        }
    }
}

struct DeliveryZone: Identifiable {
    let id = UUID()
    let name: String
    let center: CLLocationCoordinate2D
    let radius: CLLocationDistance
    let deliveryTime: String
    let color: Color
}

// Custom polygon (complex geographic zone)
struct PolygonOverlayView: View {
    @State private var position: MapCameraPosition = .automatic

    // Neighborhood coordinates
    let neighborhoodCoordinates: [CLLocationCoordinate2D] = [
        CLLocationCoordinate2D(latitude: 48.853, longitude: 2.347),
        CLLocationCoordinate2D(latitude: 48.858, longitude: 2.352),
        CLLocationCoordinate2D(latitude: 48.862, longitude: 2.348),
        CLLocationCoordinate2D(latitude: 48.860, longitude: 2.340),
        CLLocationCoordinate2D(latitude: 48.855, longitude: 2.342)
    ]

    var body: some View {
        Map(position: $position) {
            // Polygon automatically closed
            MapPolygon(coordinates: neighborhoodCoordinates)
                .foregroundStyle(.blue.opacity(0.3))
                .stroke(.blue, lineWidth: 3)
        }
    }
}

질문 7: 두 지점 사이의 경로를 표시하려면 어떻게 합니까?

경로 계산은 Apple 서버에서 경로를 가져오기 위해 MKDirections를 사용합니다. 표시는 그 후 MapPolyline으로 수행됩니다.

RouteDirections.swiftswift
import SwiftUI
import MapKit

@Observable
class RouteCalculator {
    var route: MKRoute?
    var isCalculating = false
    var error: String?

    // Calculate route between two points
    func calculateRoute(from source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) async {
        isCalculating = true
        error = nil

        // Create source and destination placemarks
        let sourcePlacemark = MKPlacemark(coordinate: source)
        let destinationPlacemark = MKPlacemark(coordinate: destination)

        // Configure request
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: sourcePlacemark)
        request.destination = MKMapItem(placemark: destinationPlacemark)
        request.transportType = .automobile // .walking, .transit available
        request.requestsAlternateRoutes = false

        let directions = MKDirections(request: request)

        do {
            let response = try await directions.calculate()
            // Take the first route
            route = response.routes.first
        } catch {
            self.error = error.localizedDescription
        }

        isCalculating = false
    }
}

struct DirectionsMapView: View {
    @State private var calculator = RouteCalculator()
    @State private var position: MapCameraPosition = .automatic

    let startPoint = CLLocationCoordinate2D(latitude: 48.8738, longitude: 2.2950) // Arc de Triomphe
    let endPoint = CLLocationCoordinate2D(latitude: 48.8530, longitude: 2.3499) // Notre-Dame

    var body: some View {
        ZStack {
            Map(position: $position) {
                // Start and end markers
                Marker("Start", systemImage: "car.fill", coordinate: startPoint)
                    .tint(.green)

                Marker("End", systemImage: "flag.fill", coordinate: endPoint)
                    .tint(.red)

                // Route trace
                if let route = calculator.route {
                    MapPolyline(route.polyline)
                        .stroke(.blue, lineWidth: 5)
                }
            }

            // Route information
            if let route = calculator.route {
                VStack {
                    Spacer()
                    RouteInfoCard(route: route)
                }
            }

            if calculator.isCalculating {
                ProgressView("Calculating route...")
                    .padding()
                    .background(.ultraThinMaterial)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            }
        }
        .task {
            await calculator.calculateRoute(from: startPoint, to: endPoint)
        }
    }
}

struct RouteInfoCard: View {
    let route: MKRoute

    var body: some View {
        HStack(spacing: 20) {
            VStack(alignment: .leading) {
                Text("Distance")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Text(formatDistance(route.distance))
                    .font(.headline)
            }

            Divider()
                .frame(height: 30)

            VStack(alignment: .leading) {
                Text("Estimated Time")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Text(formatDuration(route.expectedTravelTime))
                    .font(.headline)
            }

            Spacer()
        }
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .padding()
    }

    private func formatDistance(_ meters: CLLocationDistance) -> String {
        let km = meters / 1000
        return String(format: "%.1f km", km)
    }

    private func formatDuration(_ seconds: TimeInterval) -> String {
        let minutes = Int(seconds) / 60
        if minutes < 60 {
            return "\(minutes) min"
        } else {
            let hours = minutes / 60
            let remainingMinutes = minutes % 60
            return "\(hours)h \(remainingMinutes)min"
        }
    }
}

장소 검색 및 지오코딩

질문 8: MKLocalSearch로 장소 검색을 구현하려면 어떻게 합니까?

MKLocalSearch는 관심 지점, 주소 또는 비즈니스를 검색할 수 있게 합니다. 이 기능은 위치 기반 검색 바가 있는 애플리케이션에 필수적입니다.

PlaceSearch.swiftswift
import SwiftUI
import MapKit

@Observable
class PlaceSearchManager {
    var searchResults: [MKMapItem] = []
    var isSearching = false
    var searchText = ""

    private var searchTask: Task<Void, Never>?

    // Search with debounce
    func search(query: String, in region: MKCoordinateRegion) {
        searchTask?.cancel()

        guard !query.isEmpty else {
            searchResults = []
            return
        }

        searchTask = Task {
            // 300ms debounce
            try? await Task.sleep(for: .milliseconds(300))

            guard !Task.isCancelled else { return }

            isSearching = true

            let request = MKLocalSearch.Request()
            request.naturalLanguageQuery = query
            request.region = region
            request.resultTypes = [.pointOfInterest, .address]

            let search = MKLocalSearch(request: request)

            do {
                let response = try await search.start()
                if !Task.isCancelled {
                    searchResults = response.mapItems
                }
            } catch {
                print("Search error: \(error.localizedDescription)")
                searchResults = []
            }

            isSearching = false
        }
    }

    func clearResults() {
        searchResults = []
        searchText = ""
    }
}

struct SearchableMapView: View {
    @State private var searchManager = PlaceSearchManager()
    @State private var position: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
            span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
        )
    )
    @State private var selectedItem: MKMapItem?
    @State private var visibleRegion: MKCoordinateRegion?

    var body: some View {
        NavigationStack {
            Map(position: $position, selection: $selectedItem) {
                // Display search results
                ForEach(searchManager.searchResults, id: \.self) { item in
                    Marker(item: item)
                }
            }
            .onMapCameraChange { context in
                // Capture visible region for search
                visibleRegion = context.region
            }
            .searchable(text: $searchManager.searchText, prompt: "Search for a place")
            .onChange(of: searchManager.searchText) { _, newValue in
                if let region = visibleRegion {
                    searchManager.search(query: newValue, in: region)
                }
            }
            .overlay(alignment: .bottom) {
                if let item = selectedItem {
                    PlaceDetailCard(item: item)
                }
            }
            .navigationTitle("Map")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

struct PlaceDetailCard: View {
    let item: MKMapItem

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(item.name ?? "Unknown Place")
                .font(.headline)

            if let address = item.placemark.title {
                Text(address)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            HStack {
                if let phone = item.phoneNumber {
                    Button {
                        if let url = URL(string: "tel:\(phone)") {
                            UIApplication.shared.open(url)
                        }
                    } label: {
                        Label("Call", systemImage: "phone")
                    }
                    .buttonStyle(.bordered)
                }

                Button {
                    item.openInMaps()
                } label: {
                    Label("Directions", systemImage: "arrow.triangle.turn.up.right.diamond")
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(.ultraThickMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .padding()
    }
}

질문 9: 주소를 좌표로 변환하려면 어떻게 합니까(지오코딩)?

지오코딩은 텍스트 주소를 GPS 좌표로 변환합니다. 역지오코딩은 반대 작업을 수행합니다.

Geocoding.swiftswift
import SwiftUI
import MapKit
import CoreLocation

@Observable
class GeocodingManager {
    private let geocoder = CLGeocoder()

    var coordinate: CLLocationCoordinate2D?
    var address: String?
    var isLoading = false
    var error: String?

    // Geocoding: address → coordinates
    func geocode(address: String) async {
        isLoading = true
        error = nil

        do {
            let placemarks = try await geocoder.geocodeAddressString(address)

            if let placemark = placemarks.first,
               let location = placemark.location {
                coordinate = location.coordinate
            } else {
                error = "Address not found"
            }
        } catch {
            self.error = error.localizedDescription
        }

        isLoading = false
    }

    // Reverse geocoding: coordinates → address
    func reverseGeocode(coordinate: CLLocationCoordinate2D) async {
        isLoading = true
        error = nil

        let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)

        do {
            let placemarks = try await geocoder.reverseGeocodeLocation(location)

            if let placemark = placemarks.first {
                address = formatAddress(placemark)
            } else {
                error = "Address not found"
            }
        } catch {
            self.error = error.localizedDescription
        }

        isLoading = false
    }

    // Format readable address
    private func formatAddress(_ placemark: CLPlacemark) -> String {
        var components: [String] = []

        if let street = placemark.thoroughfare {
            if let number = placemark.subThoroughfare {
                components.append("\(number) \(street)")
            } else {
                components.append(street)
            }
        }

        if let city = placemark.locality {
            components.append(city)
        }

        if let country = placemark.country {
            components.append(country)
        }

        return components.joined(separator: ", ")
    }
}

struct GeocodingDemoView: View {
    @State private var manager = GeocodingManager()
    @State private var addressInput = ""
    @State private var position: MapCameraPosition = .automatic

    var body: some View {
        VStack {
            // Address input field
            HStack {
                TextField("Enter an address", text: $addressInput)
                    .textFieldStyle(.roundedBorder)

                Button("Search") {
                    Task {
                        await manager.geocode(address: addressInput)
                        if let coord = manager.coordinate {
                            position = .region(MKCoordinateRegion(
                                center: coord,
                                span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
                            ))
                        }
                    }
                }
                .buttonStyle(.borderedProminent)
                .disabled(addressInput.isEmpty || manager.isLoading)
            }
            .padding()

            // Map with marker
            Map(position: $position) {
                if let coord = manager.coordinate {
                    Marker("Result", coordinate: coord)
                        .tint(.red)
                }
            }

            if let error = manager.error {
                Text(error)
                    .foregroundStyle(.red)
                    .padding()
            }
        }
    }
}
지오코딩 제한

Apple의 지오코더는 요청 제한을 부과합니다. 대량 요청의 경우 로컬 캐시 또는 Mapbox나 Google Maps와 같은 타사 서비스를 사용하는 것이 권장됩니다.

고급 패턴 및 모범 사례

질문 10: 많은 마커로 성능을 최적화하려면 어떻게 합니까?

수백 개의 마커를 표시하면 성능에 영향을 미칠 수 있습니다. 클러스터링과 점진적 로딩은 효과적인 해결책입니다.

ClusteringMapView.swiftswift
import SwiftUI
import MapKit

struct ClusteringMapView: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var visiblePlaces: [Place] = []
    @State private var visibleRegion: MKCoordinateRegion?

    // All data (potentially thousands)
    let allPlaces: [Place]

    var body: some View {
        Map(position: $position) {
            // Display only visible places
            ForEach(visiblePlaces) { place in
                Marker(place.name, coordinate: place.coordinate)
            }
        }
        .onMapCameraChange(frequency: .onEnd) { context in
            // Update when user stops moving
            visibleRegion = context.region
            updateVisiblePlaces(in: context.region)
        }
    }

    // Filter places in visible region
    private func updateVisiblePlaces(in region: MKCoordinateRegion) {
        let minLat = region.center.latitude - region.span.latitudeDelta / 2
        let maxLat = region.center.latitude + region.span.latitudeDelta / 2
        let minLon = region.center.longitude - region.span.longitudeDelta / 2
        let maxLon = region.center.longitude + region.span.longitudeDelta / 2

        // Spatial filtering
        var filtered = allPlaces.filter { place in
            place.coordinate.latitude >= minLat &&
            place.coordinate.latitude <= maxLat &&
            place.coordinate.longitude >= minLon &&
            place.coordinate.longitude <= maxLon
        }

        // Limit number of displayed markers
        if filtered.count > 100 {
            // Sampling or clustering
            filtered = Array(filtered.prefix(100))
        }

        visiblePlaces = filtered
    }
}

// Simplified manual clustering
struct ClusteredPlace: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
    let count: Int
    let places: [Place]

    var isCluster: Bool { count > 1 }
}

@Observable
class ClusterManager {
    var clusters: [ClusteredPlace] = []

    // Group nearby places based on zoom level
    func cluster(places: [Place], in region: MKCoordinateRegion) {
        let gridSize = region.span.latitudeDelta / 10

        var grid: [String: [Place]] = [:]

        for place in places {
            // Grid key based on position
            let gridX = Int(place.coordinate.longitude / gridSize)
            let gridY = Int(place.coordinate.latitude / gridSize)
            let key = "\(gridX),\(gridY)"

            grid[key, default: []].append(place)
        }

        // Convert to clusters
        clusters = grid.map { (_, places) in
            let centerLat = places.map(\.coordinate.latitude).reduce(0, +) / Double(places.count)
            let centerLon = places.map(\.coordinate.longitude).reduce(0, +) / Double(places.count)

            return ClusteredPlace(
                coordinate: CLLocationCoordinate2D(latitude: centerLat, longitude: centerLon),
                count: places.count,
                places: places
            )
        }
    }
}

struct ClusterAnnotationView: View {
    let cluster: ClusteredPlace

    var body: some View {
        if cluster.isCluster {
            // Display a circle with count
            ZStack {
                Circle()
                    .fill(.blue)
                    .frame(width: 40, height: 40)

                Text("\(cluster.count)")
                    .font(.system(size: 14, weight: .bold))
                    .foregroundStyle(.white)
            }
        } else {
            // Simple marker
            Image(systemName: "mappin.circle.fill")
                .font(.title)
                .foregroundStyle(.red)
        }
    }
}

질문 11: MapKit을 다른 Apple 서비스와 통합하려면 어떻게 합니까?

MapKit은 풍부한 경험을 제공하기 위해 여러 Apple 프레임워크와 통합됩니다: Look Around, CarPlay, WidgetKit.

LookAroundIntegration.swiftswift
import SwiftUI
import MapKit

struct LookAroundMapView: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var selectedPlace: Place?
    @State private var lookAroundScene: MKLookAroundScene?
    @State private var showLookAround = false

    let places: [Place]

    var body: some View {
        Map(position: $position, selection: $selectedPlace) {
            ForEach(places) { place in
                Marker(place.name, coordinate: place.coordinate)
            }
        }
        .onChange(of: selectedPlace) { _, newPlace in
            if let place = newPlace {
                Task {
                    await loadLookAroundScene(for: place.coordinate)
                }
            }
        }
        .sheet(isPresented: $showLookAround) {
            if let scene = lookAroundScene {
                LookAroundPreview(scene: scene)
                    .frame(height: 300)
            }
        }
        .safeAreaInset(edge: .bottom) {
            if selectedPlace != nil && lookAroundScene != nil {
                Button("View in Look Around") {
                    showLookAround = true
                }
                .buttonStyle(.borderedProminent)
                .padding()
            }
        }
    }

    // Load Look Around scene for a position
    private func loadLookAroundScene(for coordinate: CLLocationCoordinate2D) async {
        let request = MKLookAroundSceneRequest(coordinate: coordinate)

        do {
            lookAroundScene = try await request.scene
        } catch {
            lookAroundScene = nil
            print("Look Around not available: \(error.localizedDescription)")
        }
    }
}

// Interactive Look Around component
struct LookAroundPreview: View {
    let scene: MKLookAroundScene
    @State private var isNavigating = false

    var body: some View {
        LookAroundPreviewRepresentable(scene: scene, isNavigating: $isNavigating)
            .overlay(alignment: .topTrailing) {
                Button {
                    isNavigating.toggle()
                } label: {
                    Image(systemName: isNavigating ? "stop.fill" : "play.fill")
                        .padding()
                        .background(.ultraThinMaterial)
                        .clipShape(Circle())
                }
                .padding()
            }
    }
}

// UIViewRepresentable for Look Around
struct LookAroundPreviewRepresentable: UIViewRepresentable {
    let scene: MKLookAroundScene
    @Binding var isNavigating: Bool

    func makeUIView(context: Context) -> MKLookAroundViewController {
        let controller = MKLookAroundViewController(scene: scene)
        controller.isNavigationEnabled = isNavigating
        return controller
    }

    func updateUIView(_ uiView: MKLookAroundViewController, context: Context) {
        uiView.isNavigationEnabled = isNavigating
    }
}

iOS 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

결론

SwiftUI와 함께하는 MapKit은 iOS 애플리케이션에 지도를 통합하기 위한 현대적이고 강력한 API를 제공합니다. 어노테이션, 오버레이, 지오로케이션 및 장소 검색에 대한 숙달은 기술 면접에서 경험 많은 iOS 개발자를 돋보이게 합니다.

검토 체크리스트

  • ✅ Map으로 지도를 표시하고 MapCameraPosition 구성하기
  • ✅ 다양한 지도 스타일 사용하기(standard, imagery, hybrid)
  • ✅ 사용자 정의 Marker 및 Annotation 추가하기
  • ✅ CLLocationManager로 위치 권한 처리하기
  • ✅ 실시간으로 위치 추적하고 경로 그리기
  • ✅ 오버레이 그리기(MapCircle, MapPolygon, MapPolyline)
  • ✅ MKDirections로 경로 계산하기
  • ✅ MKLocalSearch로 장소 검색 구현하기
  • ✅ 지오코딩 및 역지오코딩 수행하기
  • ✅ 마커 클러스터링으로 성능 최적화하기
  • ✅ 몰입형 보기를 위한 Look Around 통합하기

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

iOS 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 4월 29일 업데이트

태그

#mapkit
#swiftui
#ios
#geolocation
#interview

공유

관련 기사