웹 서비스에 지도 기능을 추가하려 할 때, 대부분의 개발자들은 구글 맵만 생각한다. 하지만 국내 사용자를 타겟으로 한다면 네이버 맵이 훨씬 더 자연스럽고 빠르게 로드된다는 점을 놓치고 있다. 문제는 대부분의 튜토리얼이 JavaScript 클라이언트 중심으로만 설명되어 있다는 것이다. PHP 백엔드에서 Naver Map API v5를 제대로 연동하는 방법, 특히 마커 데이터를 서버에서 생성해서 프론트엔드로 안전하게 전달하는 방식을 정확히 알고 있는 개발자는 드물다. 이번에는 API 키 발급부터 시작해서 PHP에서 마커를 동적으로 생성하고, JavaScript에서 지도에 표시하는 실전 방법을 완벽하게 정리해서 소개하겠다.

 

1단계. Naver Cloud Platform 개발자 계정 생성과 API 키 발급

먼저 Naver Cloud Platform(NCP) 콘솔에 접속해서 개발자 계정을 만들어야 한다. 구글이나 카카오와 달리 네이버는 별도의 결제 정보가 꼭 필요하지는 않지만, 본인 인증 절차는 거쳐야 한다. 계정을 만들고 로그인한 후 상단 메뉴에서 'Console'을 클릭하면 프로젝트 목록 페이지가 나온다. 여기서 '프로젝트 생성'을 눌러 새로운 프로젝트를 만든다. 프로젝트 이름은 자유롭게 설정해도 되는데, 보통은 서비스명이나 기능명으로 짓는 게 좋다(예: "내 지도 앱", "배달 서비스").

프로젝트를 생성한 후, 같은 페이지에서 '애플리케이션 등록'을 클릭한다. 이때 애플리케이션 종류는 'Web Application'을 선택하고, 허용할 URI 항목에는 실제 서비스가 돌아갈 도메인을 입력해야 한다. 로컬 테스트라면 http://localhost:8000 같은 식으로 입력하면 된다. 여러 개의 URI를 등록할 수 있으니 개발 환경과 프로덕션 환경을 모두 추가해두는 게 안전하다. 등록을 완료하면 Client ID가 발급되는데, 이것이 바로 지도를 로드할 때 필요한 API 키다.

 

2단계. JavaScript 클라이언트에 Naver Map 로드하기

API 키를 받았으면 이제 HTML에서 Naver Map 라이브러리를 로드할 차례다. 지도는 클라이언트 측에서만 렌더링되므로 JavaScript로 초기화해야 한다. 다만 마커 데이터는 PHP에서 준비해서 JSON으로 전달받는 방식을 사용할 것이다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Naver Map 지도</title>
    <style>
        #map { width: 100%; height: 600px; }
    </style>
</head>
<body>
    <div id="map"></div>
    <script type="text/javascript" src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID"></script>
    <script src="map.js"></script>
</body>
</html>

✕ 잘못된 방법: Client ID를 하드코딩하거나, 스크립트 로드 시점을 JavaScript가 실행되기 전으로 설정하는 경우.

✓ 올바른 방법: Client ID를 개발 환경 변수나 PHP에서 렌더링한 값으로 동적으로 주입하고, 외부 라이브러리를 먼저 로드한 후 map.js를 실행하도록 순서를 정한다.

 

3단계. PHP에서 마커 데이터를 JSON으로 생성하기

이제 핵심 부분이다. PHP 백엔드에서 마커 정보(위도, 경도, 제목, 설명 등)를 데이터베이스나 배열에서 가져와서 JSON으로 변환한다. 일반적으로 API 엔드포인트를 만들어서 AJAX로 마커 데이터를 받아오는 방식을 사용한다.

<?php
// api/markers.php
header('Content-Type: application/json; charset=utf-8');

// 마커 데이터 (실제로는 데이터베이스에서 조회)
$markers = [
    [
        'lat' => 37.4979,
        'lng' => 127.0276,
        'title' => '강남역',
        'description' => '서울 강남구 강남대로'
    ],
    [
        'lat' => 37.5665,
        'lng' => 126.9780,
        'title' => '서울역',
        'description' => '서울 중구 남대문로'
    ]
];

echo json_encode($markers, JSON_UNESCAPED_UNICODE);
?>

✕ 잘못된 방법: json_encode()에서 JSON_UNESCAPED_UNICODE 옵션을 빼면 한글이 유니코드로 이스케이프되어 가독성이 떨어진다.

✓ 올바른 방법: JSON_UNESCAPED_UNICODE 옵션을 반드시 포함해서 한글이 정상적으로 표시되도록 한다.

 

4단계. JavaScript에서 마커를 지도에 표시하기

이제 PHP에서 받은 마커 데이터를 Naver Map 라이브러리를 사용해서 지도에 표시한다.

// map.js
let map;
let markers = [];

// 1. 지도 초기화
function initMap() {
    const mapOptions = {
        center: new naver.maps.LatLng(37.4979, 127.0276),
        zoom: 10
    };
    map = new naver.maps.Map('map', mapOptions);
    
    // 마커 데이터 로드
    loadMarkers();
}

// 2. 마커 데이터 로드 (PHP API 호출)
function loadMarkers() {
    fetch('/api/markers.php')
        .then(response => response.json())
        .then(data => {
            data.forEach(markerData => {
                addMarker(markerData);
            });
        })
        .catch(error => console.error('마커 로드 실패:', error));
}

// 3. 개별 마커 추가
function addMarker(markerData) {
    const position = new naver.maps.LatLng(markerData.lat, markerData.lng);
    
    const marker = new naver.maps.Marker({
        position: position,
        map: map,
        title: markerData.title
    });
    
    // 마커 클릭 시 정보창 표시
    const infoWindow = new naver.maps.InfoWindow({
        content: `
            <div style="padding: 10px; min-width: 200px;">
                <h4>${markerData.title}</h4>
                <p>${markerData.description}</p>
            </div>
        `
    });
    
    naver.maps.Event.addListener(marker, 'click', function() {
        infoWindow.open(map, marker);
    });
    
    markers.push(marker);
}

// 페이지 로드 후 지도 초기화
document.addEventListener('DOMContentLoaded', initMap);

✕ 잘못된 방법: fetch 결과를 기다리지 않고 바로 map 객체를 사용하거나, 마커를 배열에 저장하지 않으면 나중에 마커를 제거하거나 수정할 수 없다.

✓ 올바른 방법: async/await 또는 .then()으로 비동기 처리를 명확히 하고, 마커 객체를 배열에 저장해서 나중에 접근할 수 있도록 한다.

 

5단계. 실전 예제: 데이터베이스에서 마커 조회하기

실제 서비스에서는 마커 데이터를 데이터베이스에서 조회해야 한다. 아래는 MySQL과 PDO를 사용한 예제다.

<?php
// api/markers.php (개선된 버전)
header('Content-Type: application/json; charset=utf-8');

try {
    $pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', 'password');
    
    // 쿼리 매개변수로 지역 필터링 (선택사항)
    $region = isset($_GET['region']) ? '%' . $_GET['region'] . '%' : '%';
    
    $stmt = $pdo->prepare("SELECT id, latitude, longitude, name, description FROM locations WHERE region LIKE ? ORDER BY id");
    $stmt->execute([$region]);
    $markers = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    // 필드명 변환 (데이터베이스 필드 <-> JavaScript 필드)
    $result = array_map(function($row) {
        return [
            'lat' => floatval($row['latitude']),
            'lng' => floatval($row['longitude']),
            'title' => $row['name'],
            'description' => $row['description'],
            'id' => intval($row['id'])
        ];
    }, $markers);
    
    echo json_encode($result, JSON_UNESCAPED_UNICODE);
    
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['error' => '마커 데이터 조회 실패'], JSON_UNESCAPED_UNICODE);
}
?>

✕ 잘못된 방법: 좌표를 문자열 상태로 JSON 인코딩하면 JavaScript에서 계산할 때 타입 변환 오버헤드가 생기고, 필터링 없이 모든 마커를 반환하면 대량의 데이터로 인해 성능이 저하된다.

✓ 올바른 방법: floatval()로 좌표를 숫자 타입으로 변환하고, 선택적 필터링을 통해 필요한 마커만 반환한다.

 

6단계. 마커 제거 및 업데이트 기능

사용자 입력에 따라 마커를 동적으로 제거하거나 추가해야 하는 경우도 있다. 예를 들어 필터 버튼을 클릭했을 때 마커를 다시 로드하는 기능을 추가할 수 있다.

// 모든 마커 제거
function clearMarkers() {
    markers.forEach(marker => marker.setMap(null));
    markers = [];
}

// 마커 새로 고침 (필터 적용 후)
function refreshMarkers(region) {
    clearMarkers();
    
    fetch(`/api/markers.php?region=${encodeURIComponent(region)}`)
        .then(response => response.json())
        .then(data => {
            data.forEach(markerData => {
                addMarker(markerData);
            });
        })
        .catch(error => console.error('마커 새로 고침 실패:', error));
}

// 버튼 클릭 이벤트 연결
document.getElementById('filterBtn').addEventListener('click', function() {
    const region = document.getElementById('regionInput').value;
    refreshMarkers(region);
});

✕ 잘못된 방법: 마커를 제거할 때 배열만 비우고 setMap(null)을 호출하지 않으면 메모리 누수가 발생하고, 클릭 이벤트가 남아 있어서 오류가 생길 수 있다.

✓ 올바른 방법: setMap(null)로 지도에서 완전히 제거한 후 배열을 비운다.

 

주의사항 및 흔한 실수

1. CORS 에러 발생
PHP API를 다른 도메인에서 호출할 때 CORS 에러가 날 수 있다. 이 경우 PHP 응답 헤더에 CORS 설정을 추가해야 한다.

<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
// ... 나머지 코드
?>

2. 한글 마커 정보가 깨지는 경우
데이터베이스 인코딩이 utf8mb4가 아니면 한글이 깨질 수 있다. 테이블을 생성할 때 반드시 charset을 utf8mb4로 설정하고, PHP 연결 시에도 charset=utf8mb4를 명시해야 한다.

3. 마커 클릭 시 정보창이 여러 개 표시되는 경우
이전 정보창을 닫지 않고 새 정보창을 열면 중첩된다. addMarker() 함수에서 이전 정보창을 저장해뒀다가 새 마커를 클릭하면 이전 것을 닫도록 수정할 수 있다.

let currentInfoWindow = null;

naver.maps.Event.addListener(marker, 'click', function() {
    if (currentInfoWindow) {
        currentInfoWindow.close();
    }
    infoWindow.open(map, marker);
    currentInfoWindow = infoWindow;
});

 

마무리

Naver Map API v5는 국내 사용자 경험을 최우선으로 설계되어 있고, PHP와 함께 사용하면 강력한 지도 기능을 구현할 수 있다. API 키 발급부터 마커 동적 로드, 필터링까지 이 글의 패턴을 따르면, 추후에 배달 앱이나 위치 기반 서비스처럼 복잡한 기능도 쉽게 확장할 수 있을 것이다. 특히 마커 데이터를 PHP에서 JSON으로 준비하는 방식은 다른 외부 API 연동에도 적용할 수 있는 기본 패턴이니 꼭 기억해두자.