1. 설치
npm install react-kakao-maps-sdk
2. <script> 태그 추가
이 라이브러리를 사용하기 위해서는 필수적으로 Kakao Maps API를 불러와야 한다.
API 키 발급받는 법
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY&libraries=services,clusterer" ></script>
KakaoMap.jsx
import React, { useState, useEffect } from "react"; import { Map, MapMarker } from "react-kakao-maps-sdk"; const KakaoMap = ({ keyword, latitude, longitude }) => { const { kakao } = window; const [info, setInfo] = useState(); const [markers, setMarkers] = useState([]); const [map, setMap] = useState(); const [mapLength, setMapLength] = useState(1000); useEffect(() => { const overlayBox = document.querySelector(".overlaybox"); if (overlayBox) { const grandParent = overlayBox.parentElement.parentElement; if (grandParent) { setBorder(grandParent.style.border); // border 값을 업데이트합니다. grandParent.style.border = "0"; // border 값을 0으로 설정합니다. } } }, []); useEffect( () => { if (!map) return; if(mapLength===1000){ createMap(keyword); } if(mapLength===0){ createMap(keyword); // 예 let address = "충청북도 청주시 상당구 미원면"; //만약에 검색된 스터디카페 값이 없다면 충청북도 청주시 까지로 해서 검색 let parts = keyword.split(" "); let result = parts[0] + " " + parts[1]; console.log(result); // 출력: "충청북도 청주시" createMap( result); } }, [map,mapLength]); async function createMap(keyword){ const ps = new kakao.maps.services.Places(); ps.keywordSearch(`${keyword} 스터디카페`, async (data, status, _pagination) => { setMapLength(data.length); if (status === kakao.maps.services.Status.OK) { // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해 // LatLngBounds 객체에 좌표를 추가합니다 const bounds = new kakao.maps.LatLngBounds(); let markers = []; for (var i = 0; i < data.length; i++) { // @ts-ignore markers.push({ position: { lat: data[i].y, lng: data[i].x, }, content: data[i].place_name, }); // @ts-ignore bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x)); } setMarkers(markers); // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다 map.setBounds(bounds); //보더값 제거 배경색 투명 const overlayBox = document.querySelector(".overlaybox"); if (overlayBox) { const grandParent = overlayBox.parentElement.parentElement; if (grandParent) { setBorder(grandParent.style.border); // border 값을 업데이트합니다. setBackgroundColor(grandParent.style.background); grandParent.style.border = "0"; // border 값을 0으로 설정합니다. grandParent.style.background = "rgba(255,255,255,0)"; } } } }); } return ( <div> <Map center={{ lat: latitude, lng: longitude }} style={{ width: "100%", height: "700px", borderRadius: "20px", }} level={3} onCreate={setMap} > {/* //지도에 보여줄 위치 지정 (위도,경도) */} {markers.map((marker) => ( <MapMarker style={{ border: "tranparent" }} //position={{ lat: latitude, lng: longitude }} key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`} position={marker.position} onClick={() => setInfo(marker)} image={{ src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png", size: { width: 50, height: 50 } }} > {info && info.content === marker.content && ( <> <div style={{ color: "#000", fontSize: "16px", fontWeight: "600", border: "2px solid #e30d7c", borderRadius: "5px", padding: "5px", backgroundColor: "#ffffff", boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", width: "300px", textAlign: "center", }} > ☕???? {marker.content} </div> </> )} </MapMarker> ))} </Map> </div> //핀에 적힐 이름 (위치 이름) ); }; export default KakaoMap;
StudyCafeFinder
import Header from "../../components/Header"; import { useLoaderData } from "react-router-dom"; import { API_BASE_URL } from "../../api-config"; import { getAuthToken, getUser } from "../../util/auth"; import KakaoMap from "../../components/KakaoMap"; import UserTable from "../../components/UserTable"; const { kakao } = window; const StudyCafeFinder = () => { const { findCafeInfo } = useLoaderData(); return ( <> <div> <Header /> <main className="container mt-5"> {findCafeInfo && findCafeInfo.users && ( <UserTable users={findCafeInfo.users} /> )} <KakaoMap keyword={findCafeInfo.address} latitude={findCafeInfo.lat} longitude={findCafeInfo.lng} /> </main> </div> </> ); }; export default StudyCafeFinder; // 로더 함수 export async function loader({ request, params }) { const token = getAuthToken(); const user = getUser(); const response = await fetch(`${API_BASE_URL}/users/studyCafeFinder`, { method: "POST", body: JSON.stringify(user), headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, }); if (!response.ok) { return json({ message: "데이터를 가져올 수 없습니다." }, { status: 500 }); } else { //좌표 let findCafeInfo = ""; const searchOptionArray = []; let users = ""; let getAddress = ""; const resData = await response.json(); users = resData.result; const addresses = await users.map((user) => { return user.address; }); //샘플 예 // const addresses = [ // "서울 송파구 동남로 99", // "서울 강남구 테헤란로 212", // "대구 수성구 동대구로 441", // ]; const newAddresses = addresses.map((address) => { const parts = address.split(" "); parts.pop(); // 마지막 요소(번지수)를 제거합니다. return parts.join(" "); // 주소를 다시 합칩니다. }); try { //1.주소로 좌표 구하기 await latAndLong(newAddresses, searchOptionArray); //2.중간좌표 구하기 findCafeInfo = await centerPosition(searchOptionArray); //3.중간좌표로 주소 구하기 getAddress = await getAddressFn(findCafeInfo) .then((res) => res) .catch((err) => console.error(err)); } catch (error) { console.log("error :", error); } findCafeInfo.users = users; findCafeInfo.address = getAddress; return { findCafeInfo: findCafeInfo }; } } //1.주소로 좌표 구하기 async function latAndLong(addresses, searchOptionArray) { const geocoder = new kakao.maps.services.Geocoder(); const promises = addresses.map( (address) => new Promise((resolve, reject) => { geocoder.addressSearch(address, function (result, status) { if (status === kakao.maps.services.Status.OK) { const coords = new kakao.maps.LatLng(result[0].y, result[0].x); const searchOption = { location: coords, radius: 2000, sort: kakao.maps.services.SortBy.DISTANCE, }; resolve(coords); } else { reject(new Error("Failed to get coordinates")); } }); }) ); await Promise.all(promises) .then((coordsArray) => { searchOptionArray.push(...coordsArray); }) .catch((error) => console.error(error)); return searchOptionArray; } //2. 중간좌표 구하기 async function centerPosition(locations) { //ex) const locations = [ // { La: 127.047486752713, Ma: 37.504059366187 }, // { La: 126.976425200039, Ma: 37.5626231544518 }, // { La: 126.978989954189, Ma: 37.5735042429813 } // ]; let sumLat = 0; let sumLng = 0; locations.forEach((location) => { sumLat += location.Ma; sumLng += location.La; }); const avgLat = sumLat / locations.length; const avgLng = sumLng / locations.length; return { lat: avgLat, lng: avgLng }; } //3.좌표를 이용해서 주소값 가져오기 async function getAddressFn({ lat, lng }) { const geocoder = new window.kakao.maps.services.Geocoder(); const res = await new Promise((resolve, reject) => { geocoder.coord2RegionCode(lng, lat, (result, status) => { if (status === window.kakao.maps.services.Status.OK) { for (let i = 0; i < result.length; i++) { if (result[i].region_type === "H") { resolve(result[i].address_name); } } } else { reject(new Error("주소를 찾을 수 없습니다.")); } }); }); return res; }
넥스트14 적용 방법
스터디 카페 검색
useKakaoMaps.ts
// utils/useKakaoMaps.ts import { useEffect, useState } from 'react'; export function useKakaoMaps(appKey: string) { const [kakao, setKakao] = useState<any>(null); useEffect(() => { const kakaoMapScript = document.createElement('script'); kakaoMapScript.async = true; kakaoMapScript.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${appKey}&libraries=services,clusterer&autoload=false`; document.head.appendChild(kakaoMapScript); const onLoadKakaoAPI = () => { if (window.kakao && window.kakao.maps) { setKakao(window.kakao); } else { console.error('Kakao Maps API is not loaded.'); } }; kakaoMapScript.addEventListener('load', onLoadKakaoAPI); return () => { kakaoMapScript.removeEventListener('load', onLoadKakaoAPI); }; }, [appKey]); return kakao; }
"use client"; import React, { useState, useEffect } from "react"; import { useKakaoMaps } from "@/utils/properties/useKakaoMaps"; import { Map, MapMarker } from "react-kakao-maps-sdk"; interface PropertyKakaoMapProps { keyword?: string; latitude?: number; longitude?: number; } const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ keyword }) => { const defaultKeyword = "수원시 장안구 스터디 카페"; //기본값 설정지역 const defaultLatitude = 37.5665; // 서울특별시 위도 const defaultLongitude = 126.978; // 서울특별시 경도 const currentKeyword = keyword || defaultKeyword; const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || ""); const [info, setInfo] = useState<any>(null); const [markers, setMarkers] = useState<{ position: { lat: number; lng: number }; content: string }[]>([]); const [map, setMap] = useState<any>(null); const [mapLength, setMapLength] = useState<number>(1000); useEffect(() => { console.log("!map || !kakao " ,map, kakao ); if (!map || !kakao) return; if (mapLength === 1000) { createMap(currentKeyword); } if (mapLength === 0) { createMap(currentKeyword); let parts = currentKeyword.split(" "); let result = parts[0] + " " + (parts[1] || ""); createMap(result); } }, [map, mapLength, currentKeyword, kakao]); const createMap = (keyword: string) => { if (!kakao) return; const ps = new kakao.maps.services.Places(); ps.keywordSearch(`${keyword}`, (data: any[], status: string) => { if (status !== kakao.maps.services.Status.OK) { setMapLength(0); return; } setMapLength(data.length); if (status === kakao.maps.services.Status.OK) { const bounds = new kakao.maps.LatLngBounds(); const newMarkers = data.map((item) => ({ position: { lat: parseFloat(item.y), lng: parseFloat(item.x), }, content: item.place_name, })); setMarkers(newMarkers); newMarkers.forEach((marker) => { bounds.extend(new kakao.maps.LatLng(marker.position.lat, marker.position.lng)); }); map.setBounds(bounds); } }); }; return ( <div> {kakao && ( <div id="map" style={{ width: "100%", height: "700px", borderRadius: "20px" }}> <Map center={{ lat: defaultLatitude, lng: defaultLongitude }} style={{ width: "100%", height: "700px", borderRadius: "20px" }} level={3} onCreate={setMap} > {markers.map((marker) => ( <MapMarker key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`} position={marker.position} onClick={() => setInfo(marker)} image={{ src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png", size: { width: 50, height: 50, }, }} > {info && info.content === marker.content && ( <div style={{ color: "#000", fontSize: "16px", fontWeight: "600", border: "2px solid #e30d7c", borderRadius: "5px", padding: "5px", backgroundColor: "#ffffff", boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", width: "300px", textAlign: "center", }} > ☕ {marker.content} </div> )} </MapMarker> ))} </Map> </div> )} </div> ); }; export default PropertyKakaoMap;
"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useKakaoMaps } from "@/utils/properties/useKakaoMaps"; import { Map, MapMarker, MapTypeId } from "react-kakao-maps-sdk"; interface PropertyKakaoMapProps { address: string; } const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ address }) => { const defaultAddress = "제주특별자치도 제주시 첨단로 242"; const currentAddress = address || defaultAddress; const defaultLatitude = 33.450701; const defaultLongitude = 126.570667; const [latitude, setLatitude] = useState(defaultLatitude); const [longitude, setLongitude] = useState(defaultLongitude); const [level, setLevel] = useState(3); const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || ""); const [info, setInfo] = useState<any>(null); const [marker, setMarker] = useState<{ position: { lat: number; lng: number }; content: string } | null>(null); const [map, setMap] = useState<any>(null); const [mapType, setMapType] = useState<MapTypeId>(kakao.maps.MapTypeId.ROADMAP); useEffect(() => { if (!kakao) return; // SDK 로드가 완료된 후에 지도를 생성합니다. window.kakao.maps.load(() => { const geocoder = new kakao.maps.services.Geocoder(); geocoder.addressSearch(currentAddress, (result: any[], status: string) => { if (status === kakao.maps.services.Status.OK) { const coords = new kakao.maps.LatLng(result[0].y, result[0].x); setLatitude(coords.getLat()); setLongitude(coords.getLng()); setMarker({ position: { lat: coords.getLat(), lng: coords.getLng() }, content: currentAddress, }); } else { console.error("주소 검색 실패:", status); } }); }); }, [kakao, currentAddress]); useEffect(() => { if (!map) return; map.setMapTypeId(mapType); }, [mapType, map]); const toggleMapType = useCallback((type: MapTypeId) => { setMapType(type); }, []); const zoomIn = useCallback(() => { setLevel((prev) => (prev > 1 ? prev - 1 : prev)); }, []); const zoomOut = useCallback(() => { setLevel((prev) => (prev < 14 ? prev + 1 : prev)); }, []); return ( <div> {kakao && ( <div style={{ position: "relative" }}> <div style={{ position: "absolute", top: "10px", right: "10px", zIndex: 2, display: "flex", flexDirection: "column", gap: "10px", }} > <button onClick={() => toggleMapType(kakao.maps.MapTypeId.ROADMAP)} style={{ backgroundColor: "#ffffff", border: "none", padding: "10px", borderRadius: "5px", boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)", cursor: "pointer", }} > 일반지도 보기 </button> <button onClick={() => toggleMapType(kakao.maps.MapTypeId.HYBRID)} style={{ backgroundColor: "#ffffff", border: "none", padding: "10px", borderRadius: "5px", boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)", cursor: "pointer", }} > 스카이뷰 보기 </button> <button onClick={() => toggleMapType(kakao.maps.MapTypeId.ROADVIEW)} style={{ backgroundColor: "#ffffff", border: "none", padding: "10px", borderRadius: "5px", boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)", cursor: "pointer", }} > 로드뷰 보기 </button> <button onClick={zoomIn} style={{ backgroundColor: "#ffffff", border: "none", padding: "10px", borderRadius: "5px", boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)", cursor: "pointer", }} > + </button> <button onClick={zoomOut} style={{ backgroundColor: "#ffffff", border: "none", padding: "10px", borderRadius: "5px", boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)", cursor: "pointer", }} > - </button> </div> <div id="map" style={{ width: "100%", height: "700px", borderRadius: "20px" }}> <Map center={{ lat: latitude, lng: longitude }} style={{ width: "100%", height: "700px", borderRadius: "20px" }} level={level} onCreate={setMap} > {marker && ( <MapMarker position={marker.position} onClick={() => setInfo(marker)} image={{ src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png", size: { width: 50, height: 50, }, }} > {info && info.content === marker.content && ( <div style={{ color: "#000", fontSize: "16px", fontWeight: "600", border: "2px solid #e30d7c", borderRadius: "5px", padding: "5px", backgroundColor: "#ffffff", boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", width: "300px", textAlign: "center", }} > ☕ {marker.content} </div> )} </MapMarker> )} </Map> </div> </div> )} </div> ); }; export default PropertyKakaoMap;
tailwind 적용
"use client"; import React, { useState, useEffect } from "react"; import { useKakaoMaps } from "@/utils/properties/useKakaoMaps"; import { Map, MapMarker } from "react-kakao-maps-sdk"; interface PropertyKakaoMapProps { address: string; } const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ address }) => { const defaultAddress = "제주특별자치도 제주시 첨단로 242"; const currentAddress = address || defaultAddress; const defaultLatitude = 33.450701; const defaultLongitude = 126.570667; const [latitude, setLatitude] = useState(defaultLatitude); const [longitude, setLongitude] = useState(defaultLongitude); const [level, setLevel] = useState(3); const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || ""); const [info, setInfo] = useState<any>(null); const [marker, setMarker] = useState<{ position: { lat: number; lng: number }; content: string } | null>(null); const [map, setMap] = useState<any>(null); const [isSkyview, setIsSkyview] = useState(false); useEffect(() => { if (!kakao) return; // SDK 로드가 완료된 후에 지도를 생성합니다. window.kakao.maps.load(() => { const geocoder = new kakao.maps.services.Geocoder(); geocoder.addressSearch(currentAddress, (result: any[], status: string) => { if (status === kakao.maps.services.Status.OK) { const coords = new kakao.maps.LatLng(result[0].y, result[0].x); const newMarker = { position: { lat: coords.getLat(), lng: coords.getLng() }, content: currentAddress, }; setLatitude(coords.getLat()); setLongitude(coords.getLng()); setMarker(newMarker); } else { console.error("주소 검색 실패:", status); } }); }); }, [kakao, currentAddress]); useEffect(() => { if (!map) return; map.setMapTypeId(isSkyview ? kakao.maps.MapTypeId.HYBRID : kakao.maps.MapTypeId.ROADMAP); }, [isSkyview, map, kakao]); const toggleMapType = () => { setIsSkyview((prev) => !prev); }; const zoomIn = () => { setLevel((prev) => (prev > 1 ? prev - 1 : prev)); }; const zoomOut = () => { setLevel((prev) => (prev < 14 ? prev + 1 : prev)); }; return ( <div> {kakao && ( <div className="relative"> <div className="absolute top-2 right-2 z-10 flex flex-col space-y-2"> <button onClick={toggleMapType} className="bg-blue-500 border border-blue-600 text-white p-2 rounded shadow-lg hover:bg-blue-600 focus:outline-none" > {isSkyview ? "일반지도 보기" : "스카이뷰 보기"} </button> <button onClick={zoomIn} className="bg-blue-500 border border-blue-600 text-white p-2 rounded shadow-lg hover:bg-blue-600 focus:outline-none" > + </button> <button onClick={zoomOut} className="bg-blue-500 border border-blue-600 text-white p-2 rounded shadow-lg hover:bg-blue-600 focus:outline-none" > - </button> </div> <div id="map" className="w-full h-[700px] rounded-2xl"> <Map center={{ lat: latitude, lng: longitude }} // 기본 중심 좌표 style={{ width: "100%", height: "700px", borderRadius: "20px" }} level={level} onCreate={setMap} > {marker && ( <MapMarker position={marker.position} onClick={() => setInfo(marker)} image={{ src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png", size: { width: 50, height: 50, }, }} > {info && info.content === marker.content && ( <div className="text-black text-lg font-semibold border-2 border-blue-500 rounded p-2 bg-white shadow-lg w-[300px] text-center" > ☕ {marker.content} </div> )} </MapMarker> )} </Map> </div> </div> )} </div> ); }; export default PropertyKakaoMap;
댓글 ( 0)
댓글 남기기