본문 바로가기

그 땐 IT활동했지/그 땐 영일영 근무했지

[010/API] Kakao Map API | 가게 위치 ajax로 불러오기

728x90

문제 상황

db에 있는 핸드폰 대리점들은 대략 3000개가 넘어간다. 당시 코딩 능력이 부족했던 나는 아주 정직하게 코딩했다.

  1. view에서 db의 모든 핸드폰 대리점들을 불러와서 template으로 보낸다.
  2. template내에서 JavaScripts코드를 이용해 지도 영역 내에 해당하는 대리점들 정보만 골라낸다. (3000개가 넘어가는 대리점들을 for문에 돌리는 무리수..)

이러한 방식으로 지도에 마커를 생성하니 당연히 너무 느릴 수 밖에 없었다. 게다가 지도를 움직일 때마다 지도 영역이 바뀌고 바뀌는 지도 영역에 따라 3000개 대리점을 다시 for문 돌리고 마커를 생성하니 지도 관련 기능을 만질 때마다 속도가 느렸다. 개선의 필요성을 느꼈고 ajax를 활용해 개선하기로 했다.

원래 코드
#views.py
def map(request):
    stores = Store.objects.all()
    ctx = {'stores': stores}
    return render(request, 'store/map.html', ctx)

👉🏻view에서 모든 store들을 불러와 stores 변수에 담아 보낸다.

//map.html

//스토어 목록 json 형식으로 불러오기
var storeList =  {{ stores | safe }}

// 마커 이미지의 이미지 주소입니다
var imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";

var htmlStore = document.getElementById('store-in-map');

for (var i = 0; i < storeList.length; i ++) {
	// 마커 이미지의 이미지 크기 입니다
	var imageSize = new kakao.maps.Size(24, 35);
	// 마커 이미지를 생성합니다
	var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);

	// 마커를 생성합니다
	var marker = new kakao.maps.Marker({
		map: map, // 마커를 표시할 지도
		position: new kakao.maps.LatLng(storeList[i].lat, storeList[i].lon), // 마커를 표시할 위치
		title : storeList[i].name, // 마커의 타이틀, 마커에 마우스를 올리면 타이틀이 표시됩니다
		image : markerImage, // 마커 이미지
	});
}

👉🏻받아온 stores를 storeList 변수에 담아 for문을 돌려 마커를 찍었다. 그랬더니...

👉🏻결과가 이랬다ㅎㅎㅎ

💡많은 store의 정보를 view에서 담는데에도 시간이 오래 걸리고 너무 많은 마커를 찍느라 또 시간이 오래 걸렸다. 때문에 생각해낸 해결방안은 다음과 같았다.

  1. Front단에서 먼저 지도의 영역을 파악한다. 이 정보를 ajax를 통해 Back단으로 보내준다.
  2. Back단에서는 이 영역에 해당하는 store들을 이진 탐색을 통해  선별해서 보내준다.
  3. Front단에서 이 store들만 마커를 찍어준다.

🐰그럼 이제 해결책을 자세히 뜯어보자!

 

1단계 Front단에서 지도 영역을 파악해 Back단으로 보내준다.
//map.html
function getStoreInMap() { 
	var bounds = map.getBounds(); // 지도의 현재 영역 
	var swLatLng = bounds.getSouthWest(); // 영역의 남서쪽 좌표
	var neLatLng = bounds.getNorthEast(); // 영역의 북동쪽 좌표
	let latlonData = {
		'swLat': swLatLng['Ma'],
		'swLon': swLatLng['La'],
		'neLat': neLatLng['Ma'],
		'neLon': neLatLng['La'],
	}
	$.ajax({
		url : '{% url 'get_store' %}',
		type : 'POST',
		headers : {
			'X-CSRFTOKEN' : '{{ csrf_token }}'
		},
		data : JSON.stringify(latlonData),
		success : function(storeInMap) {
			addMarker(storeInMap);
		},
		error : function() {
			alert('실패');
		}
	});
}

👉🏻getSouthWest와 getNorthEast를 사용하면 결과가 아래와 같은 형식으로 나왔다. 여기서 La는 경도이고 Ma는 위도이다.

  • pa {La: 126.58350296329486, Ma: 33.45975156071544}

👉🏻이 값을 활용해 지도 위의 영역 위도는 swLat에 아래 영역 위도 neLat에 담았고 서쪽 경도는 swLon에 동쪽 경도는 neLon에 담았고, 최종적으로 지도의 위경도 영역을 담은 latlonData라는 객체를 만들었다.

👉🏻그리고 ajax를 통해 'get_store'라는 url로 latlonData를 보내주었다.

 

2단계 Back단에서는 이 영역에 해당하는 store들만 선별해서 보내준다.
#views.py
def get_store(request):
    jsonObject = json.loads(request.body)
    all_stores = Store.objects.all().order_by('lon')

    storeInMap = {}
    left = right = middle = start = 0
    end = len(all_stores) - 1
    find = False
    while start < end:
        middle = (start + end) // 2
        if all_stores[middle].lon < jsonObject.get('swLon'):
            start = middle + 1
        elif all_stores[middle].lon > jsonObject.get('swLon'):
            end = middle - 1
        else:
            left = middle
            find = True
            break
    if find is False:
        left = end

    middle = start = 0
    end = len(all_stores) - 1
    find = False
    while start < end:
        middle = (start + end) // 2
        if all_stores[middle].lon < jsonObject.get('neLon'):
            low = middle + 1
        elif all_stores[middle].lon > jsonObject.get('neLon'):
            high = middle - 1
        else:
            right_bound = middle
            find = True
            break
    if find is False:
        right = start

    for i in range(left, right):
        if all_stores[i].lat < jsonObject.get('neLat') and all_stores[i].lat > jsonObject.get('swLat'):
            storeInMap.setdefault(len(storeInMap),
                                  {'id': all_stores[i].id,
                                   'name': all_stores[i].name,
                                   'lat': all_stores[i].lat,
                                   'lon': all_stores[i].lon,
                                   'location': all_stores[i].location}
                                   )

    return JsonResponse(storeInMap)

👉🏻전체 소스코드는 다음과 같다. 하나씩 뜯어보자면..

jsonObject = json.loads(request.body)
#필요한 값을 꺼낼 때는 
#jsonObject.get('swLon')

👉🏻ajax로 받아온 데이터를 jsonObject에 저장한다. 이후에 get을 통해 필요한 값을 꺼내서 쓴다.

all_stores = Store.objects.all().order_by('lon')

👉🏻all_stores에 모든 대리점 정보를 저장하는데 경도(lon)를 기준으로 정한다. 이는 이진 탐색을 통해 store들을 가려낼 예정이기 때문이다.

storeInMap = {}
left = right = middle = start = 0
end = len(all_stores) - 1
find = False
while start < end:
    middle = (start + end) // 2
    if all_stores[middle].lon < jsonObject.get('swLon'):
        start = middle + 1
    elif all_stores[middle].lon > jsonObject.get('swLon'):
        end = middle - 1
    else:
        left = middle
        find = True
        break
if find is False:
    left = end

middle = start = 0
end = len(all_stores) - 1
find = False
while start < end:
    middle = (start + end) // 2
    if all_stores[middle].lon < jsonObject.get('neLon'):
        low = middle + 1
    elif all_stores[middle].lon > jsonObject.get('neLon'):
        high = middle - 1
    else:
        right_bound = middle
        find = True
        break
if find is False:
    right = start

👉🏻두번의 이진 탐색을 통해 해당 경도에 속하는 대리점들을 찾아냈다.

💡이진 탐색을 사용하는 이유: 모든 대리점들을 for문에 돌리면서 지도의 경도 영역에 포함되는지 확인하는 것보다 이진탐색을 이용하면 시간을 훨씬 줄일 수 있다. (이진 탐색에 대한 내용은 아래 링크를 참조하자!↓)

2022.03.30 - [그 땐 Algorithm했지/그 땐 Python했지] - [TAVE/이코테] ch07 이진 탐색 | 개념 정리

 

[TAVE/이코테] ch07 이진 탐색 | 개념 정리

참고자료: 이것이 코딩테스트다 순차 탐색 📌리스트 안에 있는 특정한 데이터를 찾기 위해 앞에서부터 데이터를 하나씩 차례대로 확인하는 방법이다. 시간만 충분하다면 항상 원하는 원소를

itwithruilan.tistory.com

for i in range(left, right):
    if all_stores[i].lat < jsonObject.get('neLat') and all_stores[i].lat > jsonObject.get('swLat'):
        storeInMap.setdefault(len(storeInMap),
                              {'id': all_stores[i].id,
                               'name': all_stores[i].name,
                               'lat': all_stores[i].lat,
                               'lon': all_stores[i].lon,
                               'location': all_stores[i].location}
                               )

👉🏻그리고 다시 for문을 돌리면서 그 중에서 지도 위도 영역에 해당하는 대리점들을 찾아 위에서 정의한 StoreInMap이라는 딕셔너리에 저장했다. 딕셔너리에 저장한 이유는 Json으로 파싱하기 용이하기 때문이다.

 

3단계 Front단에서 이 store들만 마커를 찍어준다.
//map.html
function getStoreInMap() { 
	var bounds = map.getBounds(); // 지도의 현재 영역 
	var swLatLng = bounds.getSouthWest(); // 영역의 남서쪽 좌표
	var neLatLng = bounds.getNorthEast(); // 영역의 북동쪽 좌표
	let latlonData = {
		'swLat': swLatLng['Ma'],
		'swLon': swLatLng['La'],
		'neLat': neLatLng['Ma'],
		'neLon': neLatLng['La'],
	}
	$.ajax({
		url : '{% url 'get_store' %}',
		type : 'POST',
		headers : {
			'X-CSRFTOKEN' : '{{ csrf_token }}'
		},
		data : JSON.stringify(latlonData),
		success : function(storeInMap) {
			addMarker(storeInMap);
		},
		error : function() {
			alert('실패');
		}
	});
}

👉🏻Back에서 보낸 정보를 받아 addMarker 함수를 수행하고 있다! 그럼 addMarker함수를 살펴보자

// marker 객체를 저장하기 위한 배열
let markers = [];

//지도에 스토어들 마커 찍어주는 함수
function addMarker(List) {
	removeMarker();

	if (Object.keys(List).length > 0) {
        for (var i = 0; i < Object.keys(List).length; i ++) {
            var imageSize = new kakao.maps.Size(24, 35);
            var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);

            // 마커를 생성합니다
            let marker = new kakao.maps.Marker({
                map: map, // 마커를 표시할 지도
                position: new kakao.maps.LatLng(List[i].lat, List[i].lon), 
                title : List[i].name,
                image : markerImage,
                clickable: true 
            });

            // 마커를 marker 배열에 추가
            markers.push(marker);
        }
}

// markers에 있는 마커를 모두 제거하는 함수
function removeMarker() {
	for ( var i = 0; i < markers.length; i++ ) {
		markers[i].setMap(null);
	}
	markers = [];
}

👉🏻마커를 찍어주는 addMarker함수는 다음과 같다.

function removeMarker() {
	for ( var i = 0; i < markers.length; i++ ) {
		markers[i].setMap(null);
	}
	markers = [];
}

👉🏻먼저 첫 시작이 removeMarker함수로 시작한다. 이는 이전에 찍혀있던 마크가 있다면 지워주는 함수이다. 같은 자리에 두 개의 마커가 찍히게 할 수는 없기 때문이다.

if (Object.keys(List).length > 0) {
    for (var i = 0; i < Object.keys(List).length; i ++) {
        var imageSize = new kakao.maps.Size(24, 35);
        var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);

        // 마커를 생성합니다
        let marker = new kakao.maps.Marker({
            map: map, // 마커를 표시할 지도
            position: new kakao.maps.LatLng(List[i].lat, List[i].lon), 
            title : List[i].name,
            image : markerImage,
            clickable: true 
        });

        // 마커를 marker 배열에 추가
        markers.push(marker);
    }

👉🏻그리고 익숙한 마커를 찍어주는 함수이다! 마지막에 marker배열에 마커를 추가하는 코드가 있는 것 외에는 특별한게 없으니 Pass!

🐰ㅎㅎ이러한 과정을 거치면서 timeout 에러에서 벗어날 수 있었다! 동아리 활동 때는 어려워서 활용하지 못 한 ajax를 성공적으로 활용한 케이스라 나름 뿌듯했던 기억이 난다ㅎㅎ

728x90