프로젝트 개요
- 프로젝트 주제 : 주유소/전기차 충전소 정보 제공 및 현재 위치 기준 목적지 추천 시스템
- 기간 : 2024.11.08 ~ 2024.12.09
- 개발 내용 : 리액트 네이티브를 이용한 3차 프로젝트 모바일 어플리케이션 개발
- 기술 스택
프로젝트 취지
현대 사회에서 자동차는 필수 이동 수단으로 자리 잡았습니다. 자가용 및 승용차 이용 경험자는 90% 이상에 달하며, 평소 이용률 역시 70% 이상을 기록하고 있습니다.
현재 가장 흔히 볼 수 있는 가솔린 및 디젤 자동차는 여전히 전체 사용의 50% 이상을 차지하고 있으나, 전기 자동차와 하이브리드 자동차의 점유율도 빠르게 증가하여 현재 약 30%에 이르고 있습니다.
이에 따라 주유소, 전기차 충전소, 주차장 등 자동차 관련 시설에 대한 정보 수요가 꾸준히 증가하고 있습니다. 특히, 전기차와 하이브리드 자동차의 대중화가 가속화되면서 전기차 충전 인프라도 점차 확대되고 있지만, 사용자가 가까운 충전소나 주차장을 손쉽게 찾을 수 있도록 지원하는 서비스는 여전히 부족한 상황입니다.
더불어, 현재 위치와 목적지를 기준으로 다양한 시설 정보와 최적의 경로를 추천해주는 서비스에 대한 요구도 점점 커지고 있습니다.
이러한 필요를 충족하기 위해, 사용자에게 인근 충전소, 주유소, 주차장을 쉽고 편리하게 탐색할 수 있는 'CarPlanet' 프로젝트를 시작하게 되었습니다.
'CarPlanet' 프로젝트는 처음에 웹 기반으로 시작되었지만, 현대 사회에서는 모바일 기기를 활용한 지도 애플리케이션 사용이 크게 증가하고 있습니다. 이러한 트렌드를 반영하여, 보다 많은 사용자에게 편리한 서비스를 제공하기 위해 앱으로의 서비스 확장을 결심하게 되었고, 이를 바탕으로 개발을 진행하게 되었습니다.
메인 화면
전기차 충전소 화면
전기차 충전소 화면입니다.
지도는 카카오 지도 API를 사용하였으며 webview 로 구현하였습니다.
html변수에 html 코드로 카카오 지도 API를 사용했습니다.
const html = `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=API키&libraries=services,clusterer,drawing"></script>
<style>
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map;
(function() {
const container = document.getElementById('map');
const options = {
center: new kakao.maps.LatLng(${location.latitude}, ${location.longitude}),
level: 3
};
map = new kakao.maps.Map(container, options);
})();
</script>
</body>
</html>
`;
그 후 webview를 통해 랜더링했습니다.
<WebView
ref={webViewRef}
source={{ html }}
style={styles.webview}
onMessage={handleWebViewMessage}
/>
(지도 사진 첨부 예정)
최초 화면 진입 시, react-native-geolocation-service를 사용해 사용자 위치 정보를 요청합니다.
받은 위치를 기준으로 지도를 이동시키고, 해당 위치에 마커를 표시합니다.
useEffect(() => {
const requestLocationPermission = async () => {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
console.warn("위치 정보 요청이 거부되었습니다.");
return;
}
}
Geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
handleMoveToCurrentLocation(latitude, longitude, true);
},
(error) => console.error("location fetch 에러 발생: ", error),
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
};
requestLocationPermission();
}, []);
(위치 요청 사진 예정)
React Native의 Picker 라이브러리를 사용하여 드롭다운 리스트를 구현했습니다.
화면 상단에 드롭다운 리스트를 배치해 사용자가 원하는 지역의 전기차 충전소를 쉽게 검색할 수 있도록 구성했습니다.
드롭다운에서 시/도를 먼저 선택하면, 해당 시/도에 속하는 시/군 목록이 자동으로 표시됩니다.
각 시/도와 시/군에는 고유한 value가 설정되어 있으며, 사용자가 선택한 값을 전기차 충전소 API 호출에 활용하도록 설계했습니다.
{dropdownVisible && (
<View style={styles.dropdownContainer}>
<Text style={styles.dropdownTitle}>시/도 선택</Text>
<Picker
selectedValue={selectedCity}
style={styles.picker}
onValueChange={(value) => {
setSelectedCity(value);
setSelectedDistrict(''); // 시/도 변경 시 시/군 초기화
}}
>
<Picker.Item label="시/도" value="" />
<Picker.Item label="서울특별시" value="11" />
<Picker.Item label="부산광역시" value="26" />
<Picker.Item label="대구광역시" value="27" />
<Picker.Item label="인천광역시" value="28" />
<Picker.Item label="광주광역시" value="29" />
<Picker.Item label="대전광역시" value="30" />
<Picker.Item label="울산광역시" value="31" />
<Picker.Item label="세종특별자치시" value="36" />
<Picker.Item label="경기도" value="41" />
<Picker.Item label="충청북도" value="43" />
<Picker.Item label="충청남도" value="44" />
<Picker.Item label="전북특별자치도" value="52" />
<Picker.Item label="전라남도" value="46" />
<Picker.Item label="경상북도" value="47" />
<Picker.Item label="경상남도" value="48" />
<Picker.Item label="제주특별자치도" value="50" />
<Picker.Item label="강원특별자치도" value="51" />
</Picker>
<Text style={styles.dropdownTitle}>시/군 선택</Text>
<Picker
selectedValue={selectedDistrict}
style={styles.picker}
onValueChange={(value) => setSelectedDistrict(value)}
enabled={selectedCity !== ''}
>
<Picker.Item label="시/군" value="" />
{getDistrictsByCity(selectedCity).map((district) => (
<Picker.Item key={district.value} label={district.text} value={district.value} />
))}
</Picker>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.searchButton} onPress={handleSearch}>
<Text style={styles.searchButtonText}>검색</Text>
</TouchableOpacity>
{/* 취소 버튼 */}
<TouchableOpacity style={styles.cancelButton} onPress={closeDropdown}>
<Text style={styles.cancelButtonText}>취소</Text>
</TouchableOpacity>
</View>
</View>
)}
const cityDistricts = {
"11": [
{ value: "11680", text: "강남구" },
{ value: "11740", text: "강동구" },
...
],
"26": [
{ value: "26440", text: "강서구" },
{ value: "26410", text: "금정구" },
{ value: "26710", text: "기장군" },
...
],
"27": [
{ value: "27200", text: "남구" },
{ value: "27290", text: "달서구" },
{ value: "27710", text: "달성군" },
...
],
검색 버튼을 누르게 되면 selectedCity와 selectedDistrict에 저장된 값을 이용하여 전기차 충전소 API를 호출하게 구현했습니다.
const apiUrl = `http://apis.data.go.kr/B552584/EvCharger/getChargerInfo?serviceKey=${encodeURIComponent(apiKey)}&numOfRows=100&pageNo=1&dataType=JSON&zcode=${selectedCity}&zscode=${selectedDistrict}`;
이렇게 불러온 API를 map으로 가공해서 필요한 정보들을 담았습니다.
충전소 이름, 운영기관 이름, 연락처, 주소 등등 API에서 주는 값들을 최대한 받아서 저장했습니다.
또한 위도와 경도를 받아서 지도에 전달해 마커를 추가했습니다.
if (jsonData.items && jsonData.items.item) {
const chargerData = jsonData.items.item.map((charger) => ({
name: charger.statNm || "정보 없음", // 충전소명
operator: charger.busiNm || "정보 없음", // 운영기관명
contact: charger.busiCall || "정보 없음", // 운영기관 연락처
address: charger.addr || "정보 없음", // 주소
addressDetail: charger.addrDetail || "정보 없음", // 상세 주소
useTime: charger.useTime || "정보 없음", // 이용 가능 시간
output: charger.output || "정보 없음", // 충전 용량
chargerType: chargerTypeMap[charger.chgerType] || "알 수 없음", // 충전기 유형 매핑
status: charger.stat || "정보 없음", // 충전기 상태
statusUpdate: charger.statUpdDt || "정보 없음", // 상태 갱신 일시
lastStart: charger.lastTsdt || "정보 없음", // 마지막 충전 시작일시
lastEnd: charger.lastTedt || "정보 없음", // 마지막 충전 종료일시
currentChargingStart: charger.nowTsdt || "정보 없음", // 충전 중 시작일시
note: charger.note || "정보 없음", // 충전소 안내
limitDetail: charger.limitDetail || "정보 없음", // 제한 사항
lat: parseFloat(charger.lat) || 0, // 위도
lng: parseFloat(charger.lng) || 0, // 경도
}));
마커를 클릭하면 기본 정보를 담은 팝업이 뜨게 구현했고 해당 팝업에서 길찾기 버튼을 누르면 window.ReactNativeWebView.postMessage가 호출되고,
충전소의 좌표가 JSON 형식으로 react-native에 전달되게 구현했습니다.
react-native에서는 전달받은 값으로 handleRouteClick 함수에 있는 카카오맵 URL을 생성하게 구현했습니다.
카카오맵 URL은 사용자의 현재 위치인 location.latitude, location.longitude와 함께,
대상 충전소 위치인 targetLat, targetLng 를 이용해 URL을 생성하게 됩니다.
카카오맵 어플이 없을 경우 web에 요청하게 했습니다.
충전소 좌표 전달
const handleWebViewMessage = (event) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.lat && data.lng) {
handleRouteClick(data.lat, data.lng);
}
} catch (error) {
console.error('데이터 처리 오류:', error);
}
};
카카오 맵 URL 생성 부분
const handleRouteClick = (targetLat, targetLng) => {
if (location.latitude && location.longitude) {
const kakaoMapAppUrl = `kakaomap://route?sp=${location.latitude},${location.longitude}&ep=${targetLat},${targetLng}&by=CAR`;
const kakaoWebUrl = `https://m.map.kakao.com/scheme/route?sp=${location.latitude},${location.longitude}&ep=${targetLat},${targetLng}&by=car`;
Linking.canOpenURL(kakaoMapAppUrl)
.then((supported) => {
if (supported) {
Linking.openURL(kakaoMapAppUrl);
} else {
Linking.openURL(kakaoWebUrl);
}
})
.catch((err) => console.error('URL 열기 실패:', err));
} else {
Alert.alert('오류', '현재 위치를 가져올 수 없습니다.');
}
};
시연 화면
프로젝트 후기
이번 프로젝트는 처음에 웹으로 구현한 페이지에서 시작되었습니다.
최초 프로젝트 보고 당시, 구현하려는 서비스가 앱 환경에 더 적합하다는 피드백을 받았고, 이를 바탕으로 앱 개발로 확장하게 되었습니다.
1차 프로젝트에서 React를 사용한 경험이 있었기에, React Native를 공부하며 프로젝트를 시작할 수 있었습니다.
이 과정에서 다음과 같은 기술과 개념들을 학습하며 발전할 수 있었습니다:
- WebView를 활용해 HTML과 JavaScript 기반의 Kakao Maps API를 불러오고,
React Native와 웹 간 데이터를 주고받으며 지도를 동적으로 업데이트하는 로직 구현. - API 호출(fetch)로 서버 데이터를 가져오고, 이를 JavaScript로 가공하여 Kakao 지도에 마커를 추가하는 방법.
- WebView의 injectJavaScript를 사용해 데이터를 지도 위에 연동하는 과정.
- React Native의 useState, useEffect, useRef 등 Hook을 활용하여 상태 관리와 생명주기를 제어.
- Geolocation 라이브러리를 통해 사용자의 현재 위치를 가져오고 이를 상태로 관리.
- Android에서 PermissionsAndroid를 사용해 위치 권한 요청을 구현하며, 네이티브 기능을 React Native에 통합.
이번 개인 프로젝트를 통해 React Native의 고유한 기능과 WebView를 활용한 실제 서비스 로직 설계를 깊이 이해할 수 있는 소중한 경험을 쌓을 수 있었습니다. 특히, 지도 API와 같은 외부 라이브러리 연동, 네이티브 기능 활용, 상태 관리 및 생명주기 제어 등 다양한 기술을 익히며 한층 성장할 수 있었습니다.
다만, 전기차 충전소 이외에 팀원들이 구현한 주유소와 주차장 기능을 통합하지 못한 점, 그리고 커뮤니티나 회원가입과 같은 백엔드 로직을 포함한 기능을 구현하지 못한 점은 아쉬움으로 남습니다.
이번 프로젝트에서 배운 점과 아쉬운 점을 발판 삼아, 앞으로의 프로젝트에서는 더 완성도 높은 결과물을 만들어내기 위해 지속적으로 노력하고 성장해 나가겠습니다.