Skip to content

Commit ed55778

Browse files
committed
fix: kakao map 스크립트가 지연 로드될 시 에러가 발생할 수 있던 문제 수정
1 parent 42d23fd commit ed55778

4 files changed

Lines changed: 74 additions & 7 deletions

File tree

apps/pyconkr-2025/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<meta name="googlebot" content="index, follow" />
3939
<meta name="robots" content="index, follow" />
4040

41-
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
41+
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97&amp;autoload=false"></script>
4242
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
4343

4444
<title>PyCon Korea 2025</title>

apps/pyconkr-2026/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
<meta name="googlebot" content="index, follow" />
6161
<meta name="robots" content="index, follow" />
6262

63-
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
63+
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97&amp;autoload=false"></script>
6464
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
6565

6666
<title>PyCon Korea 2026</title>

apps/pyconkr-admin/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<meta name="googlebot" content="index, follow" />
3939
<meta name="robots" content="index, follow" />
4040

41-
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
41+
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97&amp;autoload=false"></script>
4242
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
4343

4444
<style>

packages/common/src/components/mdx_components/map.tsx

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, Button, Stack, Tab, Tabs } from "@mui/material";
1+
import { Box, Button, CircularProgress, Stack, Tab, Tabs, Typography } from "@mui/material";
22
import { CSSProperties, FC, SyntheticEvent, useEffect, useRef, useState } from "react";
33
import { renderToStaticMarkup } from "react-dom/server";
44

@@ -27,6 +27,51 @@ type MapStateType = {
2727
tab: number;
2828
};
2929

30+
/**
31+
* dapi.kakao.com 의 sdk.js 는 첫 줄에서 `window.kakao.maps` 를 빈 객체로 만들어 두고,
32+
* 실제 클래스(LatLng·Map 등)는 뒤이어 주입되는 kakao.js 가 붙여준다.
33+
* 게다가 autoload 모드에서는 로드 완료와 무관하게 readyState 를 즉시 2 로 올려서 `kakao.maps.load()` 콜백도 바로 실행된다.
34+
* 따라서 `window.kakao.maps` 존재 여부로는 판단할 수 없고, 실제로 쓸 생성자가 붙었는지를 확인해야 한다.
35+
* (느린 네트워크에서 브라우저가 document.write 로 삽입된 스크립트를 차단하면 끝내 붙지 않는다.)
36+
*/
37+
const isKakaoMapsReady = (): boolean => typeof window.kakao?.maps?.LatLng === "function" && typeof window.kakao?.maps?.Map === "function";
38+
39+
/**
40+
* `autoload=false` 로 심은 경우 이 호출이 실제 로딩을 시작시킨다(document.write 가 아닌 동적 스크립트 삽입).
41+
* autoload 모드에서는 콜백만 즉시 실행되고 끝나므로 무해하다. sdk.js 자체가 아직 없으면 false 를 돌려준다.
42+
*/
43+
const requestKakaoMapsLoad = (): boolean => {
44+
if (typeof window.kakao?.maps?.load !== "function") return false;
45+
window.kakao.maps.load(() => undefined);
46+
return true;
47+
};
48+
49+
const KAKAO_MAPS_POLL_INTERVAL_MS = 200;
50+
const KAKAO_MAPS_TIMEOUT_MS = 5000;
51+
52+
type KakaoMapsStatus = "loading" | "ready" | "failed";
53+
54+
const useKakaoMapsStatus = (): KakaoMapsStatus => {
55+
const [status, setStatus] = useState<KakaoMapsStatus>(() => (isKakaoMapsReady() ? "ready" : "loading"));
56+
57+
useEffect(() => {
58+
if (status !== "loading") return;
59+
60+
let loadRequested = requestKakaoMapsLoad();
61+
const startedAt = Date.now();
62+
const timer = window.setInterval(() => {
63+
if (!loadRequested) loadRequested = requestKakaoMapsLoad();
64+
65+
if (isKakaoMapsReady()) setStatus("ready");
66+
else if (Date.now() - startedAt >= KAKAO_MAPS_TIMEOUT_MS) setStatus("failed");
67+
}, KAKAO_MAPS_POLL_INTERVAL_MS);
68+
69+
return () => window.clearInterval(timer);
70+
}, [status]);
71+
72+
return status;
73+
};
74+
3075
export type MapDataType = {
3176
title: {
3277
ko: string;
@@ -59,6 +104,27 @@ const MapData: { [key in SupportedMapType]: MapDataType } = {
59104
},
60105
};
61106

107+
const KakaoMapArea: FC<{
108+
status: KakaoMapsStatus;
109+
mapRef: React.RefObject<HTMLDivElement | null>;
110+
style: CSSProperties;
111+
language: LangType;
112+
}> = ({ status, mapRef, style, language }) => {
113+
if (status === "ready") return <div ref={mapRef} style={style} />;
114+
115+
return (
116+
<Stack sx={{ ...style, alignItems: "center", justifyContent: "center", backgroundColor: "action.hover" }}>
117+
{status === "loading" ? (
118+
<CircularProgress size="1.5rem" />
119+
) : (
120+
<Typography variant="body2" color="text.secondary" align="center" sx={{ p: 2 }}>
121+
{language === "ko" ? "카카오맵을 불러오지 못했어요. 아래 버튼으로 열어주세요." : "Couldn't load Kakao Map. Please use the button below."}
122+
</Typography>
123+
)}
124+
</Stack>
125+
);
126+
};
127+
62128
/**
63129
* 카카오맵·구글지도·네이버지도 탭으로 특정 장소를 보여주는 지도 컴포넌트.
64130
* 각 지도 서비스로 바로 여는 버튼도 함께 렌더하며, 주로 행사장 위치 안내에 사용한다.
@@ -67,13 +133,14 @@ const MapData: { [key in SupportedMapType]: MapDataType } = {
67133
export const Map: FC<MapPropType> = ({ geo, placeName, placeCode, googleMapIframeSrc }) => {
68134
const { language } = useCommonContext();
69135
const kakaoMapRef = useRef<HTMLDivElement>(null);
136+
const kakaoMapsStatus = useKakaoMapsStatus();
70137
const [mapState, setMapState] = useState<MapStateType>({ tab: 0 });
71138
const selectedMapType = MAP_TYPES[mapState.tab] || "kakao";
72139
const setTab = (_: SyntheticEvent, tab: number) => setMapState((ps) => ({ ...ps, tab }));
73140

74141
useEffect(() => {
75142
const kakaoMapDiv = kakaoMapRef.current;
76-
if (!(window.kakao && window.kakao.maps && kakaoMapDiv)) return;
143+
if (kakaoMapsStatus !== "ready" || !kakaoMapDiv) return;
77144

78145
const kakaoMapUrl = MapData.kakao.basePlaceInfoUrl + placeCode.kakao;
79146
const content: string = renderToStaticMarkup(
@@ -108,7 +175,7 @@ export const Map: FC<MapPropType> = ({ geo, placeName, placeCode, googleMapIfram
108175
if (map) map.setCenter(new kakao.maps.LatLng(geo.lat, geo.lng));
109176
if (kakaoMapDiv) kakaoMapDiv.innerHTML = ""; // Clear the map container
110177
};
111-
}, [mapState.tab, geo, language, placeName, placeCode.kakao]);
178+
}, [mapState.tab, geo, language, placeName, placeCode.kakao, kakaoMapsStatus]);
112179

113180
const mapStyle: CSSProperties = { border: 0, width: "100%", aspectRatio: "3/2" };
114181

@@ -121,7 +188,7 @@ export const Map: FC<MapPropType> = ({ geo, placeName, placeCode, googleMapIfram
121188
<Tab key={k} label={d.title[language]} sx={{ textTransform: "none" }} />
122189
))}
123190
</Tabs>
124-
{selectedMapType === "kakao" && <div ref={kakaoMapRef} style={mapStyle} />}
191+
{selectedMapType === "kakao" && <KakaoMapArea status={kakaoMapsStatus} mapRef={kakaoMapRef} style={mapStyle} language={language} />}
125192
{selectedMapType === "google" && (
126193
<iframe title="map" src={googleMapIframeSrc} style={mapStyle} allowFullScreen loading="lazy" referrerPolicy="no-referrer-when-downgrade" />
127194
)}

0 commit comments

Comments
 (0)