[FEAT/#100] web-hook 기반 카카오 맵 연동#101
Conversation
|
Thanks for the contribution! Please review the labels and make any necessary changes. |
There was a problem hiding this comment.
Code Review
This pull request implements a map feature using the Kakao Maps API within a React Native WebView. It introduces a custom useUserLocation hook to track the user's coordinates and heading, a KakaoMap component that manages the map's state and overlays, and a UI button to center the map on the user's current location. The review feedback focuses on improving robustness through better error handling for environment variables and asynchronous location requests, enhancing code readability within the WebView's HTML template, and optimizing performance by memoizing the location focus handler.
|
|
||
| export const KakaoMap = forwardRef<KakaoMapHandle, KakaoMapProps>( | ||
| function KakaoMap({ initialCenter = SOONGSIL, myLocation, heading }, ref) { | ||
| const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY ?? ""; |
There was a problem hiding this comment.
process.env.EXPO_PUBLIC_KAKAO_JS_KEY가 설정되지 않은 경우, appKey는 빈 문자열이 되어 카카오맵 SDK가 정상적으로 동작하지 않습니다. 이 경우 WebView 내부에서 오류가 발생하여 디버깅이 어려울 수 있습니다. 환경 변수가 없을 때 즉시 오류를 발생시켜 개발 단계에서 문제를 빠르게 인지할 수 있도록 하는 것이 좋습니다.
const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY;
if (!appKey) {
throw new Error(
"Kakao Map JavaScript API key is not set. Please set EXPO_PUBLIC_KAKAO_JS_KEY in your .env file.",
);
}
| (async () => { | ||
| const { status } = await Location.requestForegroundPermissionsAsync(); | ||
| if (status !== "granted") { | ||
| setCenter(SOONGSIL); | ||
| return; | ||
| } | ||
|
|
||
| const loc = await Location.getCurrentPositionAsync({}); | ||
| const userLoc = { lat: loc.coords.latitude, lng: loc.coords.longitude }; | ||
| setCenter(userLoc); | ||
| setMyLocation(userLoc); | ||
|
|
||
| positionSub = await Location.watchPositionAsync( | ||
| { | ||
| accuracy: Location.Accuracy.High, | ||
| timeInterval: 2000, | ||
| distanceInterval: 3, | ||
| }, | ||
| (l) => | ||
| setMyLocation({ lat: l.coords.latitude, lng: l.coords.longitude }), | ||
| ); | ||
|
|
||
| headingSub = await Location.watchHeadingAsync((hdg) => { | ||
| const rounded = Math.round(hdg.magHeading); | ||
| if ( | ||
| lastHeadingRef.current === null || | ||
| Math.abs(rounded - lastHeadingRef.current) >= HEADING_THRESHOLD_DEG | ||
| ) { | ||
| lastHeadingRef.current = rounded; | ||
| setHeading(rounded); | ||
| } | ||
| }); | ||
| })(); |
There was a problem hiding this comment.
useFocusEffect 내의 비동기 IIFE에서 오류 처리가 누락되었습니다. Location.getCurrentPositionAsync와 같은 함수는 다양한 이유(예: 타임아웃, 서비스 비활성화)로 실패하여 Promise를 reject할 수 있습니다. 이 경우 처리되지 않은 Promise rejection이 발생하여 앱이 예기치 않게 동작할 수 있습니다. try...catch 블록을 사용하여 오류를 처리하고, 오류 발생 시 적절한 폴백(fallback) 로직을 수행하는 것이 중요합니다.
(async () => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setCenter(SOONGSIL);
return;
}
const loc = await Location.getCurrentPositionAsync({});
const userLoc = { lat: loc.coords.latitude, lng: loc.coords.longitude };
setCenter(userLoc);
setMyLocation(userLoc);
positionSub = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 2000,
distanceInterval: 3,
},
(l) =>
setMyLocation({ lat: l.coords.latitude, lng: l.coords.longitude }),
);
headingSub = await Location.watchHeadingAsync((hdg) => {
const rounded = Math.round(hdg.magHeading);
if (
lastHeadingRef.current === null ||
Math.abs(rounded - lastHeadingRef.current) >= HEADING_THRESHOLD_DEG
) {
lastHeadingRef.current = rounded;
setHeading(rounded);
}
});
} catch (error) {
console.error("Failed to get user location:", error);
setCenter(SOONGSIL);
}
})();| try { | ||
| const data = JSON.parse(event.nativeEvent.data) as { type: string }; | ||
| if (data.type === "MAP_READY") setIsMapReady(true); | ||
| } catch {} |
There was a problem hiding this comment.
handleMessage 함수의 catch 블록이 비어 있어 JSON.parse 실패 시 오류가 조용히 무시됩니다. 이는 디버깅을 어렵게 만들 수 있습니다. 최소한 개발 환경에서는 오류를 콘솔에 기록하는 것이 좋습니다.
try {
const data = JSON.parse(event.nativeEvent.data) as { type: string };
if (data.type === "MAP_READY") setIsMapReady(true);
} catch (error) {
console.error("Failed to parse message from WebView:", error);
}
| var content = | ||
| '<div style="position:relative;width:16px;height:16px;">' + | ||
| '<div id="loc-cone" style="position:absolute;bottom:8px;left:2px;width:12px;height:20px;transform-origin:6px 100%;transform:rotate(0deg);transition:transform 0.2s linear;">' + | ||
| '<svg width="12" height="20" viewBox="0 0 12 20" fill="${coneFill}"><polygon points="6,0 0,20 12,20"/></svg>' + | ||
| '</div>' + | ||
| '<div style="position:absolute;top:0;right:0;bottom:0;left:0;background:${primary};border:2.5px solid ${canvas};border-radius:50%;box-shadow:0 0 0 6px ${ring};"></div>' + | ||
| '</div>'; |
There was a problem hiding this comment.
createMyLocationOverlay 함수 내에서 content 변수를 생성할 때 문자열 접합(+)을 사용하고 있습니다. 이 방식은 가독성이 떨어지고 유지보수가 어렵습니다. 전체 HTML을 생성하는 buildMapHtml 함수에서처럼 템플릿 리터럴을 사용하면 코드가 더 깔끔하고 읽기 쉬워집니다.
var content = `
<div style="position:relative;width:16px;height:16px;">
<div id="loc-cone" style="position:absolute;bottom:8px;left:2px;width:12px;height:20px;transform-origin:6px 100%;transform:rotate(0deg);transition:transform 0.2s linear;">
<svg width="12" height="20" viewBox="0 0 12 20" fill="${coneFill}"><polygon points="6,0 0,20 12,20"/></svg>
</div>
<div style="position:absolute;top:0;right:0;bottom:0;left:0;background:${primary};border:2.5px solid ${canvas};border-radius:50%;box-shadow:0 0 0 6px ${ring};"></div>
</div>`;
| const handleFocusToMyLocation = () => { | ||
| if (!myLocation) return; | ||
| kakaoRef.current?.panTo(myLocation.lat, myLocation.lng); | ||
| }; |
There was a problem hiding this comment.
handleFocusToMyLocation 함수는 MapView 컴포넌트가 렌더링될 때마다 새로 생성됩니다. 이 함수는 myLocation 상태에 의존하므로, useCallback을 사용하여 메모이제이션하면 불필요한 함수 재생성을 방지하고 성능을 최적화할 수 있습니다. useCallback을 react에서 import해야 합니다.
| const handleFocusToMyLocation = () => { | |
| if (!myLocation) return; | |
| kakaoRef.current?.panTo(myLocation.lat, myLocation.lng); | |
| }; | |
| const handleFocusToMyLocation = useCallback(() => { | |
| if (!myLocation) return; | |
| kakaoRef.current?.panTo(myLocation.lat, myLocation.lng); | |
| }, [myLocation]); |
📝 요약
⚙️ 작업 내용 ─
shared/ui/kakao-map/KakaoMap.tsx: react-native-webview 기반 카카오맵 컴포넌트 신규 추가.forwardRef로panTo(lat, lng)인터페이스를 노출하여 외부에서 카메라 이동 제어 가능.
마커 생성 타이밍 개선:
initMap단계에서 내 위치 마커(파란 점 + 시야 cone)를 즉시 생성하도록 변경.widgets/map/model/useUserLocation.ts: 위치 권한·watchPositionAsync·watchHeadingAsync로직을 훅으로 분리.useFocusEffect로 탭 포커스 단위 구독·해제.widgets/map/ui/MapLocateButton.tsx: 우측 상단 위치 포커스 버튼 신규 컴포넌트. 누르면 어디를 보고 있든panTo로 내 위치까지부드럽게 카메라 이동.
설정.
.gitignore에.env추가.🔗 관련 이슈
✅ 체크리스트
💬 리뷰어에게
현재 구조는 카카오맵 JavaScript SDK를 react-native-webview에 띄우는 방식이라 두 손가락 회전 제스처를 SDK 차원에서 지원하지 않습니다. 카카오맵 모바일 앱이 회전되는 건 카카오 네이티브 SDK 기반이라 가능한 것인데 이 경우 저희 빌드 과정도 달라지고 별도의 추가적인 설정이 많이 필요하여 우선 웹훅 방식으로 구현하였습니다.
회전을 적용하려면
@react-native-kakao/map같은 네이티브 래퍼로 마이그레이션해야 하며, 이는 단순 코드 변경이 아니라 팀 워크플로우 전환을 동반합니다:코드 작업 자체는 수정하면 되긴 하는데, 워크플로우 전환은 팀 회의에서 결정할 사안이라 본 PR에서는 의도적으로 보류했습니다.
기타 참고