✔ react-kakao-maps-sdk 커스텀오버레이 : 인포윈도우, 마커 작업
✔ 마이페이지
- 로그아웃 기능 추가 + 버튼 생성
- 수정페이지 스타일 변경 (모달 -> 페이지)
- 닉네임 중복체크 스타일 변경
✔ 헤더 스타일, 위치 변경 완료
✔ 하단 고정 버튼 위치 조정
지도 위에 특정 지점을 표시하고 정보를 알려주기 위해 마커와 인포윈도우 작업이 필요했다.
사용자가 산에대해 검색한 결과를 보여주거나, 이미 정복한 산을 마이페이지에서 지도에 찍어 보여주기위한 기능이다.
우리팀은 지도기능을 위해 react-kakao-maps-sdk 를 이용했는데,
react-kakao-maps-sdk docs 에 관련내용이 자세히 나와있어 작업하는데 큰 어려움은 없었다.
위의 지도에 표시된 파란색 핀 모양의 이미지가 마커인데, 마커를 생성하는건 엄청 간단하다
기본적인 Map 객체를 생성하는 Comeponent 안에 마커가 표시될 위치값과 함께 마커를 넣어주면 된다!
import React, { useState } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";
const KakaoMap = (props) => {
const { myLoca, level } = props;
const [location, setLocation] = useState({ lat: 36.5, lng: 127.8 });
return (
<Map // 지도를 표시할 Container
center={myLoca ? maLoca : location} // props로 내위치가 전달되면 내위치가 지도의 중심좌표
level={level}
style={{
width: "100%",
height: "100%",
}}
>
<MapMarker // 마커 생성
position={myLoca ? myLoca : location} // 마커가 표시될 위치
/>
</Map>
)
}
주의할 점(?)을 집어보자면 position 값으로는 위도, 경도가 포함된 객체 값을 전달해야한다는 것이다.
// 지도의 중심좌표 center 도 마찬가지로 위도, 경도가 포함된 객체 값!!
// 이런 형태여야 한다!!
myLoca={
lat: 33.450701,
lng: 126.570667,
}
나는 일단, props를 통해 사용자의 현재위치 객체값을 전달해주고,
현재위치값이 있을때는 그 값으로, 아닐경우는 지정된 location 값으로 넣어줬다.
이렇게 하면 일단 지도의 원하는 위치에 마커가 잘~ 뜬다!!
나는 마커뿐만아니라 해당 위치에대한 정보를 보여주는 기능도 필요했기때문에,
텍스트를 올릴 수 있는 말풍선 모양의 인포윈도우를 지도에 표시하는 기능을 추가했다.
인포윈도우(InfoWindow)는 지도 위의 특정 지점에 대한 상세정보를 제공하기 위한 용도로 사용된다.
우선, 디자인은 신경쓰지않고 기존의 마커에 인포윈도우를 함께 표시하는 코드를 작성했는데,
기존에 작성한 MapMarker의 자식으로 인포윈도우에 해당하는 HTML문자열이나 component를 넣어주면 된다.
그러면 해당 자식이 인포윈도우로 만들어진다!
import React, { useState } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";
import { Grid, Text, Image } from "../../elements/element";
const KakaoMap = (props) => {
const { myLoca, level, content } = props;
const [location, setLocation] = useState({ lat: 36.5, lng: 127.8 });
return (
<Map
center={myLoca ? maLoca : location}
level={level}
style={{
width: "100%",
height: "100%",
}}
>
<MapMarker // 마커 & 인포윈도우 생성하고 지도에 표시
position={{ lat: content.lat, lng: content.lng }} // 마커 & 인포윈도우가 표시될 위치
>
{/* 인포윈도우에 들어갈 내용은 HTML 문자열이나 React Component 로 자유롭게 입력 */}
{/* props로 content를 전달 : 해당 좌표에서 전달하고자 하는 정보를 담았다 */}
<Grid>
<Image src={content.mountainImgUrl} type="rectangle" />
<Grid>
<Text>{content.mountain}</Text>
<Text>{content.mountainAddress}</Text>
</Grid>
</Grid>
</MapMarker>
</Map>
)
}
위치에대한 정보를 평소엔 숨겨져있고, 마커를 클릭했을때만 보여주길 원했다.
즉, 마커에 click 이벤트를 등록해 마우스로 클릭했을때만 인포윈도우 정보를 보여준다.
인포윈도우의 open 여부의 정보를 저정하는 state 값을 useState로 만들어주고,
평소엔 인포윈도우가 보여지면 안되기때문에 초기값으로 false를 넣는다.
그리고, isOpen의 값이 true 일때만 인포윈도우 내용이 보일 수 있도록 코드를 작성한다.
// isOpen 이 true 면 뒤에 내용에 보임
{isOpen && (
<Grid>
... // 인포윈도우 내용
</Grid>
)}
click 이벤트를 추가해주면되는데,
마커를 누를때 click 이벤트가 발생하기 때문에 MapMarker 에 onClick 속성을 넣어준다.
마커를 onClick 하면, setIsOpen 으로 isOpen 값이 true 가 된다.
전체 코드를 정리하면 아래와 같다.
const KakaoMap = (props) => {
// 인포윈도우 Open 여부를 저장하는 state
const [isOpen, setIsOpen] = useState(false)
return (
<Map
...
>
<MapMarker
position={{ lat: content.lat, lng: content.lng }}
// 마커에 click 이벤트를 등록
onClick={() => setIsOpen(true)}
>
{isOpen && (
<Grid>
... // 인포윈도우 내용
</Grid>
)}
</MapMarker>
</Map>
);
}
이 때, 나는 마커를 다시 누르면 인포윈도우가 다시 안보이게 하고 싶었다.
onClick 이벤트에 setIsOpen 값으로 true 를 지정해놓지않고, 현재 isOpen 값의 반대 값으로 설정하면 된다!
onClick={() => setIsOpen(!isOpen)} // true 대신 !isOpen 를 넣는다!
기본적인 틀과 기능은 완성됐고, 이젠 마커와 인포윈도우를 우리 사이트만의 느낌으로 스타일을 줘야했다.
이것도 문서에 잘 나와있어서 그대로 따라하면 된다!
다른 이미지로 마커 생성하기
우리 디자이너분이 만들어준 마커이미지로 교체했다.
기본으로 필요한 것은 교체할 이미지의 주소값과 원하는 이미지 사이즈!
코드에는 마커를 생성하는 MapMarker 에 image 속성만 여러 옵션들과 함께 주면 된다.
const markerImg = "https://user-images.githubusercontent.com/91959791/169664489-10a08071-905f-4a44-9a14-ae065704ced5.png";
<MapMarker
position={{ lat: content.lat, lng: content.lng }}
...
image={{
src: markerImg, // 마커이미지 주소
size: { // 마커이미지 크기
width: 28,
height: 40,
},
options: { // 마커이미지 옵션
offset: { // 마커의 좌표와 일치시킬 이미지안에서의 좌표
x: 14,
y: 40,
},
},
}}
...
/>
마커가 원하는 이미지로 잘 바꼈다.
커스텀 오버레이
인포윈도우를 우리가 원하는 디자인으로 css를 수정하기 위해 커스텀오버레이를 통해 구현한다.
말그대로, 내가 커스텀해서 오버레이를 지도에 올릴 수 있기때문에, 내 맘대로 제어하기가 좋다.
카카오에서 api 에서 오버레이를 적용하는 방법이 따로 있지만,
내가 사용한 react-kakao-maps-sdk 에선 훨씬 간단하다!
CustomOverlayMap 로 기존의 인포윈도우를 감싸 커스텀 오버레이를 표시할 container 를 부여해주면 된다.
<MapMarker
position={{ lat: content.lat, lng: content.lng }}
...
onClick={() => setIsOpen(!isOpen)}
image={{
...
}}
>
{isOpen && (
<CustomOverlayMap // 커스텀 오버레이를 표시할 Container
position={{ lat: content.lat, lng: content.lng }} // 커스텀 오버레이가 표시될 위치
yAnchor={1} // 마커와의 간격을 조정할 수 있다
>
<Grid>
... // 커스텀오버레이 내용
</Grid>
</CustomOverlayMap>
)}
</MapMarker>
이제 원하는 디자인으로 css를 만지면 끝!
이미지마커와 커스텀오버레이가 잘 적용됐다
마지막으로 여러 다른 좌표들의 정보들을 다루고, map으로 데이터를 하나하나 불러와줬기 때문에
map을 그려주고 데이터를 map으로 뿌려주는 컴포넌트와, 각데이터의 마커와 정보를 관리하는 컴포넌트를 분리했다.
// KakaoMap.js
import React, { useState } from "react";
import { Map } from "react-kakao-maps-sdk";
import EventMarkerContainer from "./EventMarkerContainer";
const KakaoMap = (props) => {
const { myLoca, level, data } = props;
const [location, setLocation] = useState({ lat: 36.5, lng: 127.8 });
return (
<Map // 지도를 표시할 Container
center={myLoca ? maLoca : location}
level={level}
style={{
width: "100%",
height: "100%",
}}
>
{data?.map((p, idx) => { // props로 받은 data list를 map으로 뿌려줌
return (
<EventMarkerContainer // 마커 생성
key={idx}
content={p} // 각 data의 정보는 EventMarkerContainer 컴포넌트에 content로 전달됨
/>
);
})}
</Map>
)
}
// EventMarkerContainer.js
import React, { useState } from "react";
import { MapMarker, CustomOverlayMap } from "react-kakao-maps-sdk";
const EventMarkerContainer = (props) => {
const { content } = props; // props로 받아온 각 좌표의 content (좌표값, 데이터 정보들이 담겨있다)
const [isOpen, setIsOpen] = useState(false);
return (
<MapMarker
position={{ lat: content.lat, lng: content.lng }}
...
onClick={() => setIsOpen(!isOpen)}
image={{
...
}}
>
{isOpen && (
<CustomOverlayMap // 커스텀 오버레이를 표시할 Container
position={{ lat: content.lat, lng: content.lng }} // 커스텀 오버레이가 표시될 위치
yAnchor={1} // 마커와의 간격을 조정할 수 있다
>
<Grid>
... // 커스텀오버레이 내용
</Grid>
</CustomOverlayMap>
)}
</MapMarker>
);
...
}
클릭한 마커와 클릭하지 않은 마커의 이미지를 다르게 해서 차별화를 주고싶단 의견이 나왔다.
클릭하지 않았을땐 기본 마커이미지였다가,
마우스를 hover 할때, click 할때만 마커가 선택전용 마커이미지로 변해야한다.
이때부터 혼란이 왔는데..
여러가지 이유가 있었지만, 가장 큰 이유는(나에게만..ㅠㅠ)
map을 그리는 KakaoMap 컴포넌트와 마커와 커스텀오버레이등, 여러 이벤트를 수행하는 EventMarkerContainer 컴포넌트를 분리해서 관리했기때문이다.
일단 마우스를 hover 할때 정보창이 보이는 것은 어렵지 않다.
마커에 마우스 커서를 올렸을때 mouseover 이벤트가, 마우스 커서를 내리면 mouseout 이벤트가 발생하게 하면 된다.
const [isOpen, setIsOpen] = useState(false)
...
return (
...
<MapMaker
...
// 마커에 마우스오버 & 마우스아웃 이벤트를 등록
onMouseOver={() => setIsOpen(true)}
onMouseOut={() => setIsOpen(false)}
>
{isOpen && (
<CustomOverlayMap
...
저 코드 두줄을 MapMarker 에 넣어주면,
마커에 마우스오버 이벤트가 발생하면 인포윈도우를 마커위에 표시하고,
마커에 마우스아웃 이벤트가 발생하면 인포윈도우를 제거한다.
❗여기서 몇가지 문제점이 발생하는데,
먼저 내가 원하는 것은 마우스오버시 정보를 보여주는게 아니라 마커의 이미지를 바꿔줘야 하고,
두번째로는 마우스오버 이벤트와 클릭이벤트를 같은 state로 관리하면 충돌이 발생한다.
마커마다 click 이벤트로 정보창을 보여줄때
onClick={() => setIsOpen(!isOpen)}
이 코드로 동작하는데, 이대로 진행한다면 마커에 마우스 오버시 isOpen이 true로 변한상태이고,
그 때 마커를 클릭하면 현재 isOpen 값인 true와 반대되는 false가 돼서,
오히려 정보창이 안보이게 되는 것이다.
결국 마우스오버시 마커의 이미지를 바꿔줄 수 있는 state 값이 필요하고, 선택한 마커를 다른 마커들과 구분할 수 있어야했다.
먼저 마커의 상태를 기본 vs 마우스오버or클릭 의 두버전으로 관리해야해서
MapMarker의 이미지 속성의 값들을 변수로 관리했다.
문서(react-kakao-maps-sdk Docs)에선 한 이미지 파일을 이용해 이미지소스의 좌표값을 달리하면 다른 이미지의마커가 보여지는 구조였는데,
우리는 두개의 이미지마커의 파일을 달리해서 사용했다.
일단 click 이벤트를 생각하지 않고, mouseover 이벤트만 고려한 코드이다.
// EventMarkerContainer.js
const EventMarkerContainer = ({ content }) => {
const [isOver, setIsOver] = useState(false);
// isOver 의 값에따라 마커이미지, 사이즈, 이미지에서의 좌표값 변경해서 변수에 넣기
let markerImg = isOver
? "https://user-images.githubusercontent.com/91959791/169664489-10a08071-905f-4a44-9a14-ae065704ced5.png"
: "https://user-images.githubusercontent.com/91959791/169664175-5428595a-2e8e-4c76-b738-596aba4f070a.png";
let markerW = isOver ? 32 : 28;
let markerH = isOver ? 46 : 40;
let offsetX = isOver ? 16 : 14;
let offsetY = isOver ? 46 : 40;
return (
...
<MapMaker
...
onMouseOver={() => setIsOver(true)} // mouse over : isOver - true
onMouseOut={() => setIsOver(false)} // mouse out : isOver - false
image={{ // 변수 적용
src: markerImg,
size: {
width: markerW,
height: markerH,
},
options: {
offset: {
x: offsetX,
y: offsetY,
},
},
}}
>
...
이렇게하면 마커는 아래사진과 같이 마우스를 어떻게 하느냐에따라 다른 이미지로 보여진다.
이제 클릭이벤트를 새로 짜야하는데, 이때 컴포넌트가 분리되어있어 좀 헤맸다 🥲
필요한 것은 새로운 동작을 하는 onClick 이벤트와, 선택된마커임을 알려주는 selectedMarker state값
// KakaoMap.js
...
export const KakaoMap = (props) => {
const [selectedMarker, setSeleteMarker] = useState();
return (
...
{data?.map((p, idx) => {
return (
<EventMarkerContainer
index={index} // 각 마커마다 index값을 부여 (구별을 위해)
key={idx}
content={p}
onClick={()=>{setSeleteMarker(index)}} // 해당 마커가 클릭되면 -> selectedMarker 값이 해당 index가 됨
// 현재 selectedMarker 값(선택된 마커의 index)과 해당 index 가 같으면 isClicked = true로 props 전달
// 아니면 isClicked = false
isClicked={selectedMarker === index}
/>
);
// EventMarkerContainer.js
...
const EventMarkerContainer = ({ index, content, onClick, isClicked }) => {
...
return (
...
<MapMaker
...
onClick={onClick} // 마커 click 시 props로 전달된 onClick 함수(setSeleteMarker(index)) 실행
>
{isClicked && ( // isClicked 가 true 인 선택된마커만 정보를 보여줌
<CustomOverlayMap
...
일차로 이렇게 코드를 짠뒤 실행해보니, 마커를 선택하고 다시 또 해당 마커를 선택했을 때 정보창이 닫히게 하고싶었다.
그래서 isVisible 이라는 또 다른 state 값을 추가했다.
// EventMarkerContainer.js
...
const EventMarkerContainer = ({ index, content, onClick, isClicked }) => {
...
const [isVisible, setIsVisible] = useState(false); // 마커를 누를때마다 isVisible 값이 바뀔 것(true or false)
const markerClick = () => {
onClick(); // props의 onClick 함수도 실행되고,
setIsVisible(!isVisible); // isVisible 값도 현재 값과 반대로 해주기 -> 클릭할때마다 반대 boolean 값으로 변하게!!
}
...
return (
...
<MapMaker
...
onClick={markerClick} // 마커 click 시 markerClick 함수 실행(두 가지 일을 해야해서 따로 뺐다!)
>
{isClicked && isVisible && ( // isClicked 이면서 isVisible 값이 true 여야 정보창 보여줌
<CustomOverlayMap
...
이렇게하니 선택된 마커여도 다시한번 마커를 누르면 정보창이 사라졌다(해당마커는 여전히 선택된 마커!)
마지막으로, 선택된 마커의 마커이미지를 바꿔주는 코드도 추가한 EventMarkerContainer 컴포넌트의 전체코드이다
// EventMarkerContainer.js
...
const EventMarkerContainer = ({ index, content, onClick, isClicked }) => {
...
const [isOver, setIsOver] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const markerClick = () => {
onClick();
setIsVisible(!isVisible);
}
// 마커 이미지 hover, click 상황에따라 변경
let markerImg = isOver
? "https://user-images.githubusercontent.com/91959791/169664489-10a08071-905f-4a44-9a14-ae065704ced5.png"
: "https://user-images.githubusercontent.com/91959791/169664175-5428595a-2e8e-4c76-b738-596aba4f070a.png";
let markerW = isOver ? 32 : 28;
let markerH = isOver ? 46 : 40;
let offsetX = isOver ? 16 : 14;
let offsetY = isOver ? 46 : 40;
if (isClicked) { // 선택된 마커이미지 변경해주는 코드 추가
markerImg =
"https://user-images.githubusercontent.com/91959791/169664489-10a08071-905f-4a44-9a14-ae065704ced5.png";
markerW = 32;
markerH = 46;
offsetX = 16;
offsetY = 46;
}
...
return (
...
<MapMaker
...
onClick={markerClick}
onMouseOver={() => setIsOver(true)} // mouse over : isOver - true
onMouseOut={() => setIsOver(false)} // mouse out : isOver - false
image={{
src: markerImg,
size: {
width: markerW,
height: markerH,
},
options: {
offset: {
x: offsetX,
y: offsetY,
},
},
}}
>
{isClicked && isVisible && ( // isClicked 이면서 isVisible 값이 true 일때만
<CustomOverlayMap
...
...
선택된 마커와 다른 마커를 구분하고,
마커를 선택하면 해당 마커의 정보가 보여지고(다시 클릭하면 창 숨겨짐),
선택한 상황에서 다른 마커에 커서를 올리면, 올려진 곳도 mouse over 이벤트가 잘 적용된다!!