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)
댓글 남기기