1. 프로젝트 소개
- 네이버 맵 API를 통해 지도를 띄우는 기능
- 몽고 db에 저장된 위치 데이터로 마커와 인포 윈도우로 해당 데이터를 표시하는 기능
- 카카오 맵 API를 통해서 키워드 검색 결과를 조회하는 기능
- 저장을 누르면 해당 위치 데이터를 몽고 db에 저장하는 기능
- 중복된 데이터를 처리하는 방법
2. react - router 설정
AppRouter.tsx
import { BrowserRouter,Routes, Route } from 'react-router-dom';
import Upload from './pages/Upload';
import Home from './pages/Home';
const AppRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="upload" element={<Upload />} />
</Routes>
</BrowserRouter>
);
};
export default AppRouter;
Home- index.tsx
function Home() {
return <div>Home</div>
}
export{};
export default Home;
Upload - index.tsx
function Home() {
return <div>Home</div>
}
export{};
export default Home;
App.tsx
import React from "react";
import AppRouter from "./AppRouter";
function App() {
return <AppRouter />;
}
export default App;
3. 네비게이션 바 뜯어보기
- 각각의 컴포넌트로 분리
- Box, Logo, Divider, Block, Button
1) Block 생성
- memo를 써서 props 값이 변하지 않는다면 Block 컴포넌트도 리렌더링되지 않음
Block.tsx
import React,{memo} from 'react';
import styled from 'styled-components';
//width: 100%
//height: props.height
//onclick:props.onClick
interface BlockProps {
height:string;
onClick?:()=>void;
}
const StyledBlock = styled.div<BlockProps>`
width:100%;
height: ${(props) => props.height};
cursor: ${(props) =>props.onClick && 'pointer'};
`;
const Block = ({height, onClick}: BlockProps) => {
return (
<StyledBlock height={height} onClick={onClick} />
);
};
export default memo(Block);
2) Button 생성
Button.tsx
import React, {memo} from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
//children
//onClick
//버튼을 클릭했을때 default
//다른 페이지로 이동시키는 Link
//type:'link' : 'button'
//url
interface ButtonProps {
children?: React.ReactNode;
onClick?: (e:any) => void;
type?:"link" | "button";
url?: string;
}
const StyledButton = styled.button<ButtonProps>`
outline:none;
border:none;
display:flex;
align-items:center;
justify-content:center;
`;
const Button = ({children, onClick, type="button", url}:ButtonProps) => {
//type에 따라서 linkButton Button
const RealButton = <StyledButton onClick={onClick}>{children}</StyledButton>;
const RealLink = (
<StyledButton>
<Link to={url!}>{children}</Link>
</StyledButton>
)
return type==="link" && url ? RealLink : RealButton;
};
export default memo(Button);
3) Divider 생성
Divider.tsx
import React,{memo} from 'react';
import styled from 'styled-components';
//width
//height
interface DividerProps {
width:string;
height?:string;
}
const StyledDivider = styled.div<DividerProps>`
width:${(props) => (props.width ? props.width:"1px")};
height: ${(props) => (props.height ? props.height : "20px")};
opacity:0.2;
background-color: #707070;
margin: 0 8px;
`;
const Divider = ({width, height} :DividerProps) => {
return (
<StyledDivider width={width} height={height}/>
);
};
export default memo(Divider);
4) Span 생성
Span.tsx
import React,{memo} from 'react';
import styled from 'styled-components';
//children
//size : 'small' | 'nomal' | 'title'
//color:string
interface SpanProps {
children?: React.ReactNode;
size?: 'small' | 'normal' | 'title';
color?: string;
}
const StyledSpan = styled.span<SpanProps>`
color: ${(props) => props.color || "black"};
&.small {
font-size: 0.8rem;
}
&.normal {
font-size: 1rem;
}
&.title {
font-size: 1rem;
font-weight: bold;
}
`;
const Span = ({children, size="normal", color}:SpanProps) => {
return (
<StyledSpan className={size} color={color}>{children}</StyledSpan>
);
};
export default memo(Span);
5) ShadowBox 생성
ShadowBox.tsx
import React,{memo} from 'react';
import styled from 'styled-components';
//children
interface ShadowBoxProps {
children?: React.ReactNode;
}
const StyledShadowBox = styled.div`
display:flex;
align-items: center;
position:absolute;
top:16px;
left:16px;
right:16px;
max-width:400px;
border-radius:10px;
padding:6px 8px;
box-shadow: rgb(0 0 0 / 16%) 0px 3px 6px 0px;
border: 1px solid #e8e8e8;
box-sizing: border-box;
z-index: 101; //navermap 100
background:#ffffff;
`
const ShadowBox = ({children}: ShadowBoxProps) => {
return (
<StyledShadowBox>{children}</StyledShadowBox>
);
};
export default memo(ShadowBox);
6) Input 생성
Input.tsx
import React,{memo} from 'react';
import styled from 'styled-components';
//children
//name
//value
//onChange
//onSubmit
interface InputProps {
children?: React.ReactNode;
name?: string;
value?:string;
onChange?:(e:any) => void;
onSubmit?: () => void;
}
const StyledInput = styled.input<InputProps>`
display:inline-block;
border:none;
width:100%;
min-height: 2em;
font-size:14px;
`;
const Input = ({children, name, value, onChange, onSubmit}:InputProps) => {
const onEnterSubmit = (e:any) => {
if(!onSubmit) return;
if(e.key ==="Enter" ) {
onSubmit();
}
}
return (
<StyledInput name={name} value={value} onChange={onChange} onKeyDown={onEnterSubmit}>
{children}
</StyledInput >
);
};
export default memo(Input);
- 이벤트 어쩌구, 글쓰다가 다 날라감 걍 안적을거임
4. Frontend - 지도 생성하기
1) 네이버 맵 표시하기
- srcipt 복사해서 클라이언트 id 발급하고 public-index.html 파일로 가서 <title>위에 추가
- id가 map인 div 태그 하나 생성하고 스타일을 입력을 한 다음에 그 후에 스크립트 파일에서 options와 map이라는 객체를 생성해 주면 네이버 지도가 생성됨
Map/index.tsx
import React, {useEffect} from 'react';
interface MapProps {
width:string;
height:string;
}
const Map= ({width,height}:MapProps) => {
//컴포넌트가 최초로 렌더링 될 때 실행해야 하는 함수이기 때문에 useEffect 사용
useEffect(() => {
const mapOptions = {
center: new naver.maps.LatLng(37.3595704, 127.105399),
zoom: 10
};
const map = new naver.maps.Map('map', mapOptions);
},[]);
return (
<div id="map" style={{width,height}}></div>
);
};
export default Map;
MapContainer.tsx
import React from 'react';
import Map from './common/Map';
const MapContainer = () => {
return (
<Map width="100%" height="100%"/>
);
};
export default MapContainer;
Home/index.tsx
import MapContainer from "../../components/MapContainer";
import Navigation from "../../components/Navigation";
function Home() {
return (
<>
<Navigation />
<MapContainer />
</>
);
}
export {};
export default Home;
2) Jotai 소개 및 Map 객체 관리
- Jotai라는 전역상태 관리 라이브러리를 활용해서 map 객체를 전역 상태로 관리
- map 객체가 전역 상태로 관리 되어야 하는 이유
-> marker 표시 등 많이 사용됨
Jotai, primitive and flexible state management for React
Map.ts
import { atom } from "jotai";
//이후에는 state로 naver.maps.map 타입이 들어갈 예정이기 때문에
//null값 또는 naver.maps.map 타입이 주어질 것이라고 명시해 줘야 함
export const mapAtom = atom<naver.maps.Map | null>(null);
MapContainer.tsx
import React from 'react';
import Map from './common/Map';
import {mapAtom} from '../atoms/map';
import {useSetAtom} from 'jotai';
const MapContainer = () => {
const setMap = useSetAtom(mapAtom);
const initMap = (map:naver.maps.Map) => {
setMap(map);
}
return (
<Map width="100%" height="100%" initMap={initMap}/>
);
};
export default MapContainer;
Map/index.tsx
import React, {useEffect} from 'react';
interface MapProps {
width:string;
height:string;
initMap?:(map:naver.maps.Map) => void;
}
const Map= ({width,height,initMap}:MapProps) => {
//컴포넌트가 최초로 렌더링 될 때 실행해야 하는 함수이기 때문에 useEffect 사용
useEffect(() => {
const mapOptions = {
center: new naver.maps.LatLng(37.3595704, 127.105399),
zoom: 10
};
const map = new naver.maps.Map('map', mapOptions);
if(initMap) {
initMap(map);
}
},[initMap]);
return (
<div id="map" style={{width,height}}></div>
);
};
export default Map;
3) 맵 클릭 이벤트 추가
- 맵을 클릭했을 때 이벤트 실행
MapContainer.tsx
import React from 'react';
import Map from './common/Map';
import {mapAtom} from '../atoms/map';
import {useSetAtom} from 'jotai';
const MapContainer = () => {
const setMap = useSetAtom(mapAtom);
const initMap = (map:naver.maps.Map) => {
setMap(map);
naver.maps.Event.addListener(map, "click", ()=> {
console.log("맵 클릭!");
});
};
return (
<Map width="100%" height="100%" initMap={initMap}/>
);
};
export default MapContainer;
5. Frontend - 마커 생성하기
1) 마커 소개 및 생성하기
tyeps/Info.ts
export type Info = {
id: number;
addressName:string;
placeName: string;
position: {
lat: number;
lng: number;
}
}
data/infodata.ts
export const infos = [
{
id: 1,
addressName: "서울 용산구 동자동 43-205",
placeName: "서울역",
position: {
lat: 37.5546788388674,
lng: 126.970606917394,
},
},
{
id: 2,
addressName: "서울 강남구 역삼동 858",
placeName: "강남역 2호선",
position: {
lat: 37.49808633653005,
lng: 127.02800140627488,
},
},
{
id: 3,
addressName: "서울 관악구 봉천동 979-2",
placeName: "서울대입구역 2호선",
position: {
lat: 37.4812845080678,
lng: 126.952713197762,
},
},
];
- 마커 위치로 지도 이동
Marker/index.tsx
import {useEffect}from 'react';
interface MarkerProps {
map:naver.maps.Map;
position: {
lat:number;
lng:number;
};
content:string;
onClick?: () => void;
}
const Marker = ({map, position, content, onClick}:MarkerProps) => {
useEffect (()=> {
let marker: naver.maps.Marker | null = null;
if(map) {
marker = new naver.maps.Marker({
map,
position:new naver.maps.LatLng(position),
icon: {
content,
},
});
}
if(onClick) {
naver.maps.Event.addListener(marker, "click", onClick);
map.panTo(position);
}
return () => {
marker?.setMap(null);
};
},[map]);
return null;
};
export default Marker;
2) 데이터를 마커로 표시하기
atom/Info.ts
import {atom} from "jotai";
import {Info} from '../types/info';
export const infosAtom = atom<Info[]|null>(null);
export const selectInfoAtom = atom<Info|null>(null);
Home/index.tsx
- infos 전역 상태 관리
import MapContainer from "../../components/MapContainer";
import Navigation from "../../components/Navigation";
import {useSetAtom} from "jotai";
import {infosAtom} from "../../atoms/info";
import {infos} from "../../data/infos";
import MarkersContainer from "../../components/MarkersContainer";
function Home() {
const setInfos = useSetAtom(infosAtom);
if(infos) {
setInfos(infos);
}
return (
<>
<Navigation />
<MapContainer />
<MarkersContainer />
</>
);
}
export {};
export default Home;
Marker/index.tsx
import {useEffect}from 'react';
import './Marker.css';
interface MarkerProps {
map:naver.maps.Map;
position: {
lat:number;
lng:number;
};
content:string;
onClick?: () => void;
}
const Marker = ({map, position, content, onClick}:MarkerProps) => {
useEffect (()=> {
let marker: naver.maps.Marker | null = null;
if(map) {
marker = new naver.maps.Marker({
map,
position:new naver.maps.LatLng(position),
icon: {
content,
},
});
}
if(onClick) {
naver.maps.Event.addListener(marker, "click", onClick);
map.panTo(position);
}
return () => {
marker?.setMap(null);
};
},[map, content, position,onClick]);
return null;
};
export default Marker;
3) 마커 클릭 이벤트 추가
- 해당 마커를 클릭하게 되면 마커가 색이 변할 수 있고 해당 마커가 클릭된 것을 바로 확인해 볼 수 있도록 만들기
MarkerContainer.tsx
mport React from 'react';
import {useAtom,useAtomValue} from "jotai";
import {infosAtom, selectInfoAtom} from "../atoms/info";
import {mapAtom} from "../atoms/map";
import {Info} from "../types/info";
import Marker from './common/Marker';
const MarkersContainer = () => {
const map = useAtomValue(mapAtom);
const infos = useAtomValue(infosAtom);
const [selectInfo, setSelectInfo] = useAtom(selectInfoAtom);
if(!map ||!infos) return null;
return (
<>
{infos.map((info:Info) => (
<Marker
key={info.id}
map={map}
position={info.position}
content={'<div class="marker" />'}
onClick={() => {
setSelectInfo(info);
}}
/>
))}
{selectInfo && (
<Marker
key={selectInfo.id}
map={map}
position={selectInfo.position}
content={'<div class="marker select"/>'}
onClick={() => {
setSelectInfo(null);
}}
/>
)}
</>
);
};
export default MarkersContainer;
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 11장 컴포넌트 성능 최적화 (0) | 2023.11.15 |
---|---|
인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (2) (0) | 2023.11.10 |
[리액트를 다루는 기술] 10장 일정 관리 웹 애플리케이션 만들기 (0) | 2023.11.08 |
[리액트를 다루는 기술] 8장 Hooks (2) | 2023.11.03 |
[리액트를 다루는 기술] 7장 컴포넌트의 라이프사이클 메서드 (0) | 2023.11.03 |