- 아웃소싱 프로젝트 - (2)2025년 03월 05일
- redpome
- 작성자
- 2025.03.05.:59
https://redpome.tistory.com/37
아웃소싱 프로젝트 - (1)
아웃소싱 프로젝트를 통해 외부 API를 사용하여 카카오맵과 유튜브 API를 연결시키고 지도상에서 장소와 관련된 영상을 제공해주는 서비스를 기획하게되었다. 맡은 역할아직까지 정해야할 사항
redpome.tistory.com
아웃소싱 프로젝트가 끝나고나서 튜터님들의 피드백을 받고 기록으로 남기고자 한다.
우선 내가 작성한 코드는 다음과 같다
import { useEffect, useRef, useState } from 'react'; import supabase from '@lib/api/supabaseAPI'; import MapController from '@components/map-api/MapController'; import { openAlert } from '@lib/utils/openAlert'; import { ALERT_TYPE } from '@constants/alert-constant'; import useAreaStore from '@store/zustand/useAreaStore'; import DetailModal from '@components/modal/detail-modal'; import YoutubeModal from '@components/modal/youtube-modal'; const { ERROR } = ALERT_TYPE; function KakaoMap() { const mapContainer = useRef(null); const [markerData, setMarkerData] = useState([]); const { mapCenter, setMapCenter } = useAreaStore(); const [selectedMarker, setSelectedMarker] = useState(null); const [dataLoading, setDataLoading] = useState(true); const [openModal, setOpenModal] = useState({ detail: false, youtube: false }); //모달창을 위한 상태 const [activeMarkerId, setActiveMarkerId] = useState(null); const markersRef = useRef({}); //마커 토글 상태 const overlaysRef = useRef({}); // 커스텀 오버레이 상태 // Supabase에서 데이터 호출 useEffect(() => { const handlemarkerData = async () => { try { const { data, error } = await supabase.from('hotplaces').select('*'); if (error) { throw error; } setMarkerData(data); } catch (error) { openAlert({ type: ERROR, text: '데이터 로드에 실패했습니다' }); console.log(error); } finally { setDataLoading(false); } }; handlemarkerData(); }, []); // KakaoMap API 호출 useEffect(() => { if (window.kakao && window.kakao.maps) { const { kakao } = window; const options = { center: new kakao.maps.LatLng(mapCenter.lat, mapCenter.lon), level: 5, }; const map = new kakao.maps.Map(mapContainer.current, options); if (!dataLoading && markerData.length === 0) { openAlert({ type: ERROR, text: '마커 데이터를 불러오는데 실패했습니다, 새로고침해주세요' }); return; } // 커스텀 오버레이 생성 함수 const makeCustomOverlay = ({ item }) => { const overlayContent = document.createElement('div'); overlayContent.innerHTML = ` <div class="bg-orange-500 w-[200px] p-4 rounded-md shadow-lg text-white"> <h3 class="font-bold">${item.name}</h3> <p>${item.area}</p> <button id="detail-btn-${item.id}" class="bg-lime-600 px-2 py-1 mt-2 text-white rounded-md w-full"> 자세히 보기 </button> </div> `; return overlayContent; }; markerData.forEach((item) => { const lat = parseFloat(item.latitude); const lon = parseFloat(item.longitude); const markerPosition = new kakao.maps.LatLng(lat, lon); // 마커 이미지 설정 const clickedMarkerImgSrc = '/activeMarker.svg'; const hotplaceMarkerImgSrc = '/hotplaceMarker.svg'; const hotplaceMarkerSize = new kakao.maps.Size(25, 45); const hotplaceMarkerOption = { offset: new kakao.maps.Point(20, 40) }; // 마커 생성 const marker = new kakao.maps.Marker({ map: map, position: markerPosition, image: new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption), }); markersRef.current[item.id] = marker; // 마커 인스턴스 저장 // 커스텀 오버레이 생성 const overlayContent = makeCustomOverlay({ item }); const customInfoOverlay = new kakao.maps.CustomOverlay({ content: overlayContent, removable: true, position: markerPosition, yAnchor: 2, zIndex: 3, }); overlaysRef.current[item.id] = customInfoOverlay; // 커스텀 오버레이 인스턴스 저장 // "자세히 보기" 버튼 이벤트 overlayContent.querySelector(`#detail-btn-${item.id}`).addEventListener('click', () => { setSelectedMarker({ id: item.id, name: item.name, area: item.area }); setOpenModal({ detail: true, youtube: false }); }); // 마커 클릭 이벤트: 한 번에 하나의 마커만 활성화 kakao.maps.event.addListener(marker, 'click', function () { setActiveMarkerId((prevId) => { // 다른 마커가 클릭되었을때, 클릭 전 상태 if (prevId && prevId !== item.id) { const prevMarker = markersRef.current[prevId]; const prevOverlay = overlaysRef.current[prevId]; if (prevMarker) { prevMarker.setImage( new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption) ); } if (prevOverlay) { prevOverlay.setMap(null); } // 클릭 마커 활성화 marker.setImage( new kakao.maps.MarkerImage(clickedMarkerImgSrc, new kakao.maps.Size(40, 75), { offset: new kakao.maps.Point(28, 70), }) ); customInfoOverlay.setMap(map); return item.id; } // 같은 마커를 다시 클릭하면 토글하여 비활성화 if (prevId === item.id) { marker.setImage( new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption) ); customInfoOverlay.setMap(null); return null; } // 아무것도 활성화되지 않은 경우, 현재 마커 활성화 marker.setImage( new kakao.maps.MarkerImage(clickedMarkerImgSrc, new kakao.maps.Size(40, 75), { offset: new kakao.maps.Point(28, 70), }) ); customInfoOverlay.setMap(map); return item.id; }); }); }); } else { openAlert({ type: ERROR, text: '마커 데이터 로드를 실패했습니다' }); } }, [markerData, mapCenter, dataLoading]); const handlePlaceSelect = (lat, lon) => { setMapCenter(lat, lon); }; return ( <div className='flex flex-col w-[1000px] mt-[-25px]'> <div className='flex w-full h-[50px] mx-auto mb-4'> <MapController handlePlaceSelect={handlePlaceSelect} /> </div> <div className='flex w-full h-[480px]'> <div id='map' ref={mapContainer} className='w-full h-full border-[5px] border-accent rounded-lg'></div> {openModal.detail && selectedMarker && <DetailModal id={selectedMarker.id} setOpenModal={setOpenModal} />} {openModal.youtube && selectedMarker && <YoutubeModal id={selectedMarker.id} setOpenModal={setOpenModal} />} </div> </div> ); } export default KakaoMap;
확실히 분리가 안 된 모습이 보인다. 리액트를 접하면서 모든것을 컴포넌트로 하다보니 자연스레 컴포넌트 내부에서 코드를 작성하게 되었다.
아래는 분리하고 조금 정리한 것이다.... import { makeCustomOverlay } from './CustomInfo'; ... function KakaoMap() { const mapContainer = useRef(null); const [markerData, setMarkerData] = useState([]); const { mapCenter, setMapCenter } = useAreaStore(); const [selectedMarker, setSelectedMarker] = useState(null); const [dataLoading, setDataLoading] = useState(true); const [openModal, setOpenModal] = useState({ detail: false, youtube: false }); const [activeMarkerId, setActiveMarkerId] = useState(null); const markersRef = useRef({}); //마커 토글 상태 const overlaysRef = useRef({}); // 커스텀 오버레이 상태 const clickedMarkerImgSrc = '/activeMarker.svg'; const hotplaceMarkerImgSrc = '/hotplaceMarker.svg'; // Supabase에서 데이터 호출 ... const createMap = () => { if (!window.kakao || !window.kakao.maps) { alert('카카오맵 API 호출 실패'); return; } if (dataLoading) return; if (markerData.length === 0) { alert('마커 데이터 호출에 실패했습니다'); return; } const { kakao } = window; const mapOption = { center: new kakao.maps.LatLng(mapCenter.lat, mapCenter.lon), level: 5, }; const map = new kakao.maps.Map(mapContainer.current, mapOption); return map; }; const createCustomMarker = (map, item) => { const { kakao } = window; const lat = parseFloat(item.latitude); const lon = parseFloat(item.longitude); const markerPosition = new kakao.maps.LatLng(lat, lon); const hotplaceMarkerSize = new kakao.maps.Size(25, 45); const hotplaceMarkerOption = { offset: new kakao.maps.Point(20, 40) }; const marker = new kakao.maps.Marker({ map, position: markerPosition, image: new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption), }); markersRef.current[item.id] = marker; return { marker, hotplaceMarkerSize, hotplaceMarkerOption }; }; useEffect(() => { const { kakao } = window; const map = createMap(); if (!map) return; markerData.forEach((item) => { const { marker, hotplaceMarkerSize, hotplaceMarkerOption } = createCustomMarker(map, item); const { overlayContent, customInfoOverlay } = makeCustomOverlay({ kakao, item }); overlaysRef.current[item.id] = customInfoOverlay; overlayContent.querySelector(`#detail-btn-${item.id}`)?.addEventListener('click', () => { setSelectedMarker({ id: item.id, name: item.name, area: item.area }); setOpenModal({ detail: true, youtube: false }); }); kakao.maps.event.addListener(marker, 'click', () => { setActiveMarkerId((prevId) => { if (prevId && prevId !== item.id) { const prevMarker = markersRef.current[prevId]; const prevOverlay = overlaysRef.current[prevId]; if (prevMarker) { prevMarker.setImage( new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption) ); } if (prevOverlay) { prevOverlay.setMap(null); } marker.setImage( new kakao.maps.MarkerImage(clickedMarkerImgSrc, new kakao.maps.Size(40, 75), { offset: new kakao.maps.Point(28, 70), }) ); customInfoOverlay.setMap(map); return item.id; } if (prevId === item.id) { marker.setImage(new kakao.maps.MarkerImage(hotplaceMarkerImgSrc, hotplaceMarkerSize, hotplaceMarkerOption)); customInfoOverlay.setMap(null); return null; } marker.setImage( new kakao.maps.MarkerImage(clickedMarkerImgSrc, new kakao.maps.Size(40, 75), { offset: new kakao.maps.Point(28, 70), }) ); customInfoOverlay.setMap(map); return item.id; }); }); }); }, [markerData, mapCenter, dataLoading]); const handlePlaceSelect = (lat, lon) => { setMapCenter(lat, lon); }; ..return 동일 export default KakaoMap;
컴포넌트 외부로 함수를 빼낸 모습이다, 참고로 커스텀 인포에서의 코드는 별도의 js 파일로 생성하여 호출하는 형식으로 만들었다.
마커 생성, 맵 생성, 커스텀 오버레이 생성으로 크게 나누었다고 볼 수 있다.
- 매직 넘버가 아닌 상수화 된 값 사용
- 함수형 패러다임을 사용하여 단일 책임 원칙이 컴포넌트에 적용될 것
점점 학습 속도를 따라가는데 벅차다. 기술은 배워도 좋은 코드를 만드는 법을 모르니 문제이다. 특히나 이번 API의 사용은 사실 한정된 개발 자료를 이용하여 만들다보니 오히려 배웠던 기술을 활용하는 방식이었는데 여전히 제대로 된 코드 한 줄 못 짜는 것 같다.
'내일배움캠프' 카테고리의 다른 글
롤 챔피언 정보 프로젝트 - (2) (0) 2025.03.12 롤 챔피언 정보 프로젝트 - (1) (0) 2025.03.11 아웃소싱 프로젝트 - (1) (0) 2025.02.27 MBTI 프로젝트 -(1):json-server와의 통신 (0) 2025.02.24 TanstackQuery - (1) (0) 2025.02.20 다음글이전글이전 글이 없습니다.댓글
스킨 업데이트 안내
현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)