본문 바로가기

react

인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (1)

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

 

Jotai, primitive and flexible state management for React

Jotai takes a bottom-up approach to global React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders are optimized based on atom dependency. This solves the extra re-render issue of React context and

jotai.org

 

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;