💪++20kg++ 회고
한 학기를 쏟아부은 첫 프로젝트가 끝났다.
엄밀히 말하면 첫 프로젝트가 아니지만, 강의를 수강하며 진행했던 다른 프로젝트들이 흐지부지 끝나버렸음을 고려할 때, 내 첫 프로젝트라 불러도 괜찮을 듯 싶다.
여러가지 문제가 발생했음에도 불구하고 어떻게든 결과물이 나왔고, 그 과정에서 느끼거나 배운 점이 여럿 있었기에 복기하는 차원에서 작성하였다.
전체 코드는 아래 링크로 확인할 수 있다.
github 바로가기
프로젝트 개요
이 프로젝트는 23년도 '데이터베이스' 과목 프로젝트로, 전라북도 전주시_체력단련장업 (https://www.data.go.kr/data/15112497/fileData.do) 공공데이터를 이용하여 자신의 현재 위치 기준 1km이내의 헬스장의 위치와 정보를 찾아주는 사이트 입니다.
⌛ 개발 기간
23.05.08일 ~ 23.05.30일
내 역할
ToDo list (CRD), 카카오맵 API를 활용하여 지도 및 마커 생성, 자신의 위치 기준 1km 이내 헬스장 탐색, CSS, 기말발표
⚙️ 개발환경
-
React 9.5.0
-
Firebase
-
IDE: Visual Studio Code
📌 주요기능
To Do List 상세보기
- DB 입출력 및 삭제
헬스장 위치 탐색 상세보기
- DB로 부터 데이터 읽어오기
- 불러온 데이터로부터 자신의 위치 기준 1km이내의 헬스장 탐색
- KakaoMap API 활용하여 지도 위에 헬스장 위치 표시
진행과정
사전계획
처음 계획된 어플은 학교 근처 음식점 탐색 어플이었고 다음과 같은 아키텍처를 가진 어플리케이션을 만들 계획이었다.
사전계획 아키텍처
- 프론트엔드 : React Native
- 백엔드 : Django
- 데이터베이스 : MySQL
이렇게 사용을 하기로 하였고 3명이서 각 1 파트씩 분배하였다.
그 중 나는 프론트엔드를 담당하기로 하였다.
문제는 기존에 내가 알고 있는 것은 HTML,CSS가 전부였고 React는 물론 JS 조차 문법이 숙지가 안 되있는 상태였다.
이에 따라 프로젝트를 진행하기 위한 언어 공부가 필요하였고, 아래 자료들을 통해 공부를 시작하였다.
공식문서 튜토리얼만으로는 전혀 이해가 안되어 2개의 강의를 추가적으로 시청하면서 React의 기본적인 문법과 동작원리를 익힐 수 있었다. 이러한 공부는 3월 2일~5월 7일 까지 진행되었다.
🔨 첫 번째 이슈
한 팀원이 개인적인 사정으로 인해 수업을 드랍하게 되었고, 그에 따라 프로젝트에서도 나가게 되면서 데이터베이스를 남은 두 명이서 분배하기로 하였다.
또한 주제선정에서도 문제가 생겼는데, 주제가 흔하다고 생각하여 헬스장의 위치를 찾아주고, 오늘 해야 할 운동과 운동방법을 알려주는 헬스 매니징 어플로 변경하였다. 또한 내가 React를 공부하면서 React Native까지 공부를 진행하는데 부담을 느껴 React 만을 이용하여 어플이 아닌 웹에서 구현하기로 하였다.
주제가 헬스 매니징 어플로 변경됨에 따라 구현하려는 주요기능도 아래와 같이 변경하였다.
- ★헬스장 위치 탐색
- ★요일 별 해야할 운동을 기록할 수 있는 메모장 기능
- 해야 할 운동을 자동으로 검색하여 운동 유튜브를 보여주는 링크 기능
만들 어플에서 가장 핵심적인 1번과 2번을 중심으로 제작하기로 하였다.
🔨 프론트엔드 설계
언어 공부를 마치고 실제 프로젝트를 시작함에 따라 내가 구현해야 할 것들을 다음과 같이 정리하였다.
- 헬스장의 정보 제공 기능
- 헬스장의 위치 시각화 기능
- 자신의 위치 찾기 기능
- 운동리스트 작성 기능
- 운동리스트 보기 기능
첫 번째 사진은 컴포넌트 구조를 요약, 두번째 사진은 결과물 예상 화면이다.
🔨 두 번째 이슈
Django를 이용하여 백엔드 서버를 구현하기로 했던 팀원이 너무 어려워서 못하겠다며 포기하였다( ... )
어찌되든 프로젝트는 완료를 해야하니 방법을 찾던 중 2학년 프로젝트 발표 때 많은 사람들이 활용하던 파이어베이스를 떠올렸고, 이에 React + Firebase로 프로젝트를 진행하게 되었다.
팀원이 코딩애플 님의 서버지식없이 당근마켓 만드는 법 (Firebase로 만드는 당근마켓 2시간컷)
영상을 참고하여 헬스장 공공데이터를 입력까지는 해줘서 저장된 데이터를 활용할 수 있었다.
💭 배운점
Kakaomap API
구글링을 하여 카카오맵 key를 발급받고 홈페이지에 있는 예제들을 따라 map.jsx를 제작하였다.
예제들이 잘 정리되어 있어서 조금만 응용을 하면 쉽게 원하는 기능을 구현할 수 있었다.
1가지 문제는 예시 코드들을 js로 작성되어 있었는데, 처음에는 React 또한 js기반이기에 별다른 수정없이 그대로 사용하였다.
처음에는 문제가 없었으나, 나중에 DB에서 데이터를 호출하는 과정에서 코드가 아래와 같이 꼬여버려 도저히 어디를 고쳐야 하는지 알 수 없는 지경에 이르렀다.
const { kakao } = window;
//두 point 거리 계산
const getDistanceFromLatLonInKm = (lat1, lng1, lat2, lng2) => {
const deg2rad = (deg) => {
return deg * (Math.PI / 180);
};
var R = 6371; // 지구의 반지름 (단위: km)
var dLat = deg2rad(lat2 - lat1); // 위도 차이
var dLon = deg2rad(lng2 - lng1); // 경도 차이
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) *
Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var distance = R * c; // 거리 (단위: km)
return distance;
};
//1km이내 데이터 배열 가져오기
const BringData = (targetlat, targetlon) => {
const [data, setData] = useState([]);
useEffect(() => {
firestore
.collection("health")
.get()
.then((snapshot) => {
const dataArray = [];
snapshot.forEach((doc) => {
const documentId = doc.id;
const lat = doc.data().위도;
const lon = doc.data().경도;
//내 위치 임의값 입력
const distance = getDistanceFromLatLonInKm(
lat,
lon,
targetlat,
targetlon
); // 거리 계산
if (distance < 1) {
dataArray.push({ id: documentId, lat, lon, distance });
}
});
setData(dataArray);
});
}, []);
return data;
};
const Map = () => {
//mapscript 관리
useEffect(
() => {
mapscript();
}, // eslint-disable-next-line
[]
);
//마커클릭시 이벤트
// let [show, setShow] =useState(false);
// const toggleVisibility = () => {
// setShow(!show);
// };
let mylat;
let mylon;
//지도생성
const mapscript = () => {
let container = document.getElementById("map");
let options = {
center: new kakao.maps.LatLng(35.84577171588417, 127.13318294215267),
level: 5,
draggable: false,
};
//헬스장 표시
const map = new kakao.maps.Map(container, options);
//내위치 찾기
const MyPosition = (locPosition, message) => {
// 마커를 생성합니다
var marker = new kakao.maps.Marker({
map: map,
position: locPosition,
});
var iwContent = message, // 인포윈도우에 표시할 내용
iwRemoveable = true;
// 인포윈도우를 생성합니다
var infowindow = new kakao.maps.InfoWindow({
content: iwContent,
removable: iwRemoveable,
});
// 인포윈도우를 마커위에 표시합니다
infowindow.open(map, marker);
// 지도 중심좌표를 접속위치로 변경합니다
map.setCenter(locPosition);
};
if (navigator.geolocation) {
// GeoLocation을 이용해서 접속 위치를 얻어옵니다
navigator.geolocation.getCurrentPosition(function (position) {
var lat = position.coords.latitude, // 위도
lon = position.coords.longitude; // 경도
var locPosition = new kakao.maps.LatLng(lat, lon), // 마커가 표시될 위치를 geolocation으로 얻어온 좌표로 생성합니다
message = '<div style="padding:5px;">현재 위치!</div>'; // 인포윈도우에 표시될 내용입니다
// 마커와 인포윈도우를 표시합니다
mylat = lat;
mylon = lon;
MyPosition(locPosition, message);
});
} else {
// HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다
var locPosition = new kakao.maps.LatLng(33.450701, 126.570667),
message = "geolocation을 사용할수 없어요..";
MyPosition(locPosition, message);
}
const findCenter = () => {
markerdata.forEach((item) => {
var healthmarker = new kakao.maps.Marker({
map: map,
position: new kakao.maps.LatLng(item.lat, item.lng),
title: item.title,
});
var iwContent = item.title, // 인포윈도우에 표시할 내용
iwRemoveable = true;
// 인포윈도우를 생성합니다
var infowindow = new kakao.maps.InfoWindow({
content: iwContent,
removable: iwRemoveable,
});
// 인포윈도우를 마커위에 표시합니다
infowindow.open(map, healthmarker);
// kakao.maps.event.addListener(healthmarker, 'click', ()=>{
// toggleVisibility();
// })
});
};
findCenter();
};
return (
<div>
<Button title="헬스장 위치 찾기" onClick={""} />
<Button
title="경로 찾기"
onClick={() => {
window.open(
`https://map.kakao.com/link/from/내 위치,${mylat},${mylon}`
);
}}
/>
<div
id="map"
style={{
width: "720px",
height: "539px",
}}
></div>
<div>
<Modal />
</div>
</div>
);
};
export default Map;
도저히 코드를 읽을 수 없었고 Bringdata()에서 에러가 발생하는 데도 원인을 찾지 못하였다.
이에 구글링을 해본 결과 react-kakao-maps-sdk docs
를 찾을 수 있었다.
React에서 카카오맵 API를 활용하기 위해 포팅한 라이브러리로 아래 예시만 봐도 깔끔함을 알 수 있다..
이 라이브러리 덕분에 위의 Map.jsx를 수정한 결과 아래와 같이 더욱 깔끔하게 변경할 수 있었고 그에 따라 버그를 고치기도 훨씬 쉬어졌다.
코드의 통일성, 가독성의 중요성을 알 수 있었다.
const getDistanceFromLatLonInKm = (lat1, lng1, lat2, lng2) => {
const deg2rad = (deg) => {
return deg * (Math.PI / 180);
};
var R = 6371; // 지구의 반지름 (단위: km)
var dLat = deg2rad(lat2 - lat1); // 위도 차이
var dLon = deg2rad(lng2 - lng1); // 경도 차이
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) *
Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var distance = R * c; // 거리 (단위: km)
return distance;
};
const Map = () => {
const [data, setData] = useState([]);
const [show, setShow] = useState(false);
const [Mystate, setMyState] = useState({
center: {
lat: 33.450701,
lng: 126.570667,
},
errMsg: null,
isLoading: true,
});
useEffect(() => {
//find my position with GeoLocation
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setMyState((prev) => ({
...prev,
center: {
lat: position.coords.latitude, // 위도
lng: position.coords.longitude, // 경도
},
isLoading: false,
}));
},
(err) => {
setMyState((prev) => ({
...prev,
errMsg: err.message,
isLoading: false,
}));
}
);
} else {
// HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다
setMyState((prev) => ({
...prev,
errMsg: "geolocation을 사용할수 없어요..",
isLoading: false,
}));
}
firestore
.collection("health")
.get()
.then((snapshot) => {
const dataArray = [];
snapshot.forEach((doc) => {
const documentId = doc.id;
const lat = doc.data().위도;
const lon = doc.data().경도;
// 내 위치 임의값 입력
const distance = getDistanceFromLatLonInKm(
lat,
lon,
Mystate.center.lat,
Mystate.center.lng
); // 거리 계산
if (distance < 1) {
dataArray.push({ id: documentId, lat, lon, distance });
}
});
setData(dataArray);
});
}, [Mystate.center]);
return (
<div>
<Button title="헬스장 위치 찾기" onClick={() => setShow(true)} />
<Button
title="경로 찾기"
onClick={() => {
window.open(
`https://map.kakao.com/link/from/내 위치,${mylat},${mylon}`
);
}}
/>
<Map
style={{ width: "720px", height: "539px" }}
center={{ lat: 35.84577171588417, lng: 127.13318294215267 }}
level={5}
draggable={true}
>
{!Mystate.isLoading && (
<MapMarker position={Mystate.center}>
<div style={{ padding: "5px", color: "#ff0000" }}>
{Mystate.errMsg ? Mystate.errMsg : "내 위치!"}
</div>
</MapMarker>
)}
{data.map(
(item) =>
show && (
<MapMarker
key={item.id}
position={{ lat: item.lat, lng: item.lon }}
clickable={true}
>
<div style={{ padding: "5px", color: "#000" }}>{item.id}</div>
</MapMarker>
)
)}
</Map>
{show && <Modal />}
</div>
);
};
export default Map;
화살표함수
앞서 링크를 걸었던 React 자습서에서 처음 접하게 된 화살표함수는 이어서 들었던 강의에서 다시 한번 접하게 되었다. 그러나 이 문법자체를 처음 접하게 된 나로서는 왜 이 문법을 사용하는지, 기존에 작성하던 데로 function()에 비해 어떤 장점을 가지게 모르겠어서 구글링과 chatGPT, 강의를 통해 찾아보았다.
이 문서에 따르면 화살표 함수는 다음과 같은 성질을 가진다.
- this, arguments나 super에 대한 자체 바인딩이 없고, methods로 사용해서는 안됩니다.
- new.target키워드가 없습니다.
- 일반적으로 스코프를 지정할 때 사용하는 call, apply, bind methods를 이용할 수 없습니다.
- 생성자(Constructor)로 사용할 수 없습니다.
- yield를 화살표 함수 내부에서 사용할 수 없습니다.
...여전히 이해가 되지 않아 chatGPT에게 추가적으로 질문을 하였다.
arrow func
..좀 더 자세히 알아보자
자바스크립트에서의 this는 현재 실행 컨텍스트 내에서 현재 객체를 참조하는데 사용되는 특별한 키워드입니다. 함수 내에서 this를 사용할 때, 함수를 어떻게 호출했느냐에 따라 this의 값이 달라집니다. 이때, 화살표 함수는 일반 함수와 다르게 this를 가지지 않고, 외부 스코프의 this를 상속받습니다. 이를 통해 몇 가지 장점 있습니다.
-
this 바인딩: 일반적인 함수에서는 함수를 호출한 주체에 따라 this가 동적으로 바인딩됩니다. 예를 들어, 객체의 메소드로서 함수를 호출하는 경우, 메소드 내부의 this는 해당 객체를 참조합니다. 그러나 콜백 함수 내부에서는 this가 전역 객체(window 객체)를 참조하기 때문에 문제가 발생할 수 있습니다. 화살표 함수는 외부 스코프의 this를 상속받으므로, 콜백 함수 내에서도 this가 원하는 대상을 참조할 수 있습니다.
-
메소드로서의 사용: 객체의 메소드로 함수를 정의할 때, 일반 함수를 사용하면 this는 해당 객체를 가리킵니다. 그러나 화살표 함수는 자체적인 this를 가지지 않기 때문에, 메소드 내부에서 this를 사용하면 외부 스코프에서의 this를 참조하게 됩니다. 이를 통해 메소드 정의를 간결하게 할 수 있습니다.
예를 들어, 다음은 일반 함수와 화살표 함수를 사용하여 객체의 메소드를 정의한 예입니다:
// 일반 함수를 사용한 메소드 정의
const obj = {
name: "John",
sayHello: function () {
console.log("Hello, " + this.name);
},
};
// 화살표 함수를 사용한 메소드 정의
const obj = {
name: "John",
sayHello: () => {
console.log("Hello, " + this.name);
},
};
위의 예제에서 일반 함수를 사용한 `sayHello` 메소드는 객체 내에서 호출될 때 해당 객체의 `name` 속성에 접근할 수 있습니다. 그러나 화살표 함수를 사용한 `sayHello` 메소드는 외부 스코프의 this를 상속받기 때문에 `this.name`은 `undefined`가 됩니다.
- 콜백 함수로의 사용: 화살표 함수는 콜백 함수로 자주 사용됩니다. 콜백 함수는 다른 함수의 인자로 전달되어 나중에 호출되는 함수입니다. 이때 일반 함수를 사용하면 함수 호출 시 this가 변경되는 문제가 발생할 수 있습니다. 그러나 화살표 함수를 사용하면 this가 상위 스코프에서 상속되므로, 콜백 함수 내에서도 원하는 this 값을 유지할 수 있습니다.
예를 들어, 배열의 `map` 메소드를 사용하여 각 요소에 대한 제곱 값을 계산하는 경우를 살펴봅시다:
const numbers = [1, 2, 3, 4, 5];
// 일반 함수를 사용한 콜백 함수
const squared1 = numbers.map(function (number) {
return number * number;
});
// 화살표 함수를 사용한 콜백 함수
const squared2 = numbers.map((number) => number * number);
위의 예제에서 일반 함수를 사용한 콜백 함수에서는 내부의 함수 컨텍스트에서 this가 변경되어 원하는 결과를 얻지 못할 수 있습니다. 그러나 화살표 함수를 사용한 콜백 함수에서는 외부 스코프의 this를 상속받기 때문에 this에 대한 문제가 발생하지 않고 간단하게 값을 계산할 수 있습니다.
이러한 이유로, 화살표 함수는 this 바인딩, 메소드로의 사용, 콜백 함수로의 사용 등 여러 상황에서 유용하게 활용될 수 있습니다.
이러한 화살표 함수의 특성들 중, 내가 직관적으로 느낄 수 있었던 것은 함수의 가독성 및 직관성이었다. 초심자인 내가 보기에도 추상적으로 생각함에 있어 직관적으로 코드가 읽히는 것이 화살표함수였기에, 앞으로 js를 작성하는데 있어 화살표함수 사용을 지향해야 겠다고 생각했다.
★데이터 사용량 문제
이 프로젝트를 진행함에 있어 가장 큰 문제였다. 위에서 말한 바와 같이 이 프로젝트는 React+Firebase 로 진행되었는데, React 사용의 미숙함+ 데이터베이스 처음 사용의 콤보로 테스트 도중 일일 사용량을 초과해버리고 말았다.
overflow
아무리 사용량이 초과했더라도 기본적인 기능은 쓰게 해줄 수 있으 줄 알았는데, 아예 접근을 못하게 되었다. 또한 데이터베이스를 팀원의 계정을 이용하여 사용하였기에 내가 즉시 할 수 있는 조치가 전혀 없어 시간에 쫒기던 중 고생했던 기억이 있다.
결국 팀원에게 요청해 데이터 사용량 유료결제를 함으로써 해결할 수 있었다. 과제 마감 직전 데이터사용량을 확인해보니 읽기에서만 19만회가 나왔다;
이에 대한 원인을 찾아보았는데, 직접적인 원인을 찾지는 못하고 대충 짐작해 보았다.
📌첫 번째 원인은 Modal component와 Map component에서 똑같은 데이터를 두번씩 호출한 것이 문제였던 것으로 생각된다. Modal component에서는 이름과 거리, 번호를, Map component에서는 거리와 이름만을 호출 하고 있는데 코드 작성 계획에서의 착오로 인해 데이터 호출 코드를 두 컴포넌트 모두 작성하였고 이로 인해 데이터 Read가 2배로 호출되었기에 데이터 양이 적음에도 불구하고 몇 만 회에 달하는 사용량이 나왔던 것 같다. 이는 listWrite 컴포넌트를 작성하고 테스트하면서 Write의 사용량이 몇백회 의 불과한 것에서 원인을 추측했다. 이 문제는 여전히 최종결과물에서도 남아있어 Read의 사용량을 잡아먹었다.
📌두 번째 원인으로는 잘못된 useEffect, useState의 사용이었던 것 같다. 이는 첫 번째 원인과 이어지는데 처음 코드를 작성할 때, js문법숙지의 부족으로 fecthData 함수의 onClick()을 제대로 작성하지 못했고, 그 결과 onClick 할 때만 fetchData를 해야할 것을 컴포넌트가 렌더링 될 때, 무조건 데이터를 호출하고, onClick 이벤트로 show가 되도록 작성하였다. 그 덕에 테스트페이지에서 새로고침을 누를 때 마다 데이터가 Read 되어 이러한 결과가 나왔던 것 같다. 잘못된 useEffect/useState의 문제는 위에서 말했던 react-kakao-maps-sdk를 사용함으로써 코드를 수정하는 과정에서 해결할 수 있었다.
해결된코드
📌 마지막으로 문제는 데이터를 전부 가져와서 프론트에서 처리한 데 있다고 생각한다. 처음 코드를 작성할 때 계획은 서버로 geolocation을 통해 구한 나의 위도/경도 위치를 전송하고, 이를 통해 서버의 데이터 베이스에서 나의 위치 기준 조건에 맞는 데이터만 넘겨주기로 계획을 했었다. 그러나 앞서 말했다시피 팀원의 Django GG선언으로 인해 데이터를 전부 가져오게 되면서 데이터 사용량이 늘어났던 것 같다.
💭느낀점
처음 계획을 세울 때만 해도 막막하게 느껴졌지만 다행히 수업을 드랍했던 팀원이 세워 준 큰 틀 덕분에 개인적으로 많은 것을 배운 프로젝트였다. 처음 React-Django-MySQL로 이루어지는 아키텍처를 구상한 것도, Javascript도 모르는 사람에게 React를 어떻게든 공부해 오라던 그 친구 덕분에 React도 배우게 되고, 전체적인 시각이 넓어진 느낌이다.
저번 방학 때 개인적으로 공부하던 html-css-js기초 책을 토대로 공부했을 때는 껍데기만 만드는 것이 전부였고, 내가 맞는 것을 공부하고 있는지 공부하면서도 의구심만 들었었다면, 비록 보잘 것 없지만 내가 계획하고 구상한 대로 동적으로 작동하는 사이트를 만들었다는데 에서 의미가 깊었다고 생각한다.
그러나 이와 동시에 스트레스 또한 심했던 것 같다. 긍정적으로 생각하면 Django를 못하겠다며 손놓고 GG를 외치던 팀원 덕분에 강제로 Firebase를 선택해야 했고, 이로 인해 프론트엔드를 지망하던 나 또한 서버를 조금이라도 알아야겠다고 다짐하는 계기가 되었다. 이번 방학을 통해 javascript를 공부하면서 Node.js도 조금은 공부를 진행할 생각이다.
또한 배포까지 가보고 싶었는데, 제출 시간+지속된 에러로 인해 빌드한 파일을 배포까지 해보지는 못했다. 우선 github pages와 firebase를 각각 이용해 배포시도를 해보았으나 github pages를 이용하면 DB에서 문제가 생기기도 하고, 굳이 배포를 안해도 될 것 같다는 팀원의 의견을 따랐다. 다음 프로젝트는 기회가 된다면 배포하는 과정 또한 진행해 보고 싶다.