
방끗 프로젝트에서 지도를 불러오는 Kakao map api 를 사용하려면 밑과 같이 script 를 불러와야 한다.(스크립트를 불러오지 않고 해당 맵 기능을 쓰게 되면 에러가 생긴다..)이 방법은 카카오 map 공식페이지 에서 알려주는 방법이다. 당연히 나도 방끗 프로젝트에 다음과 같이 적용했다.
<html>
<title>방끗</title>
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=<%= process.env.KAKAO_MAP_KEY %>&libraries=services "
>
</script>
</html>
스크립트를 직접 html에 넣는 방법은 다음과 같은 장점이 있다.
하지만 최적화 측면에서는 위 방법이 정답이 아닐 수도 있다.
방끗의 가장 첫 페이지인 소개 페이지를 lighthouse 로 검사했을 때 다음과 같은 점이 눈에 띄었기 때문이다.
내가 html에 넣었던 kakao script 때문에, 렌더링 블로킹이 되고 있었다. 소개 페이지에서는 이 스크립트를 사용하지도 않고 있다!
우선 확실하게 렌더링 블로킹을 하는지 알아봐야 했다. 개발자도구의 성능 탭으로 보니, 확실하게 렌더링을 블로킹하고 있었다.
위의 보라색 선은 렌더링을 나타낸다. 새로고침 시점부터 렌더링이 바로 되지 않는 것을 볼 수 있다. 이번 우테코 미션이 최적화 미션인 만큼, 생소하지만 호기심이 가는 이 문제를 풀고 싶었다. 다음과 같은 이유가 있었다.
FCP(firstful content paint) 을 늘리고 있다. 가장 쉬운 것은 스크립트를 html 이 블로킹하지 않게 로드하는 것이다. defer 속성을 사용하면 해당 스크립트를 html 과 같이 로드하면서, 실행은 html 이 끝난뒤에 되므로 렌더링 블로킹을 막을 수 있을 거라고 생각했다.
하지만, kakao map script는 defer를 허용하지 않는다.(ㅠㅠ) 대신 카카오에서 제시하는 방법은 다음과 같다.
export default function KakaoMap(props) {
...
useEffect(() => {
const script = document.createElement("script");
script.src =
"https://dapi.kakao.com/v2/maps/sdk.js?appkey={api key}&autoload=false";
document.head.appendChild(script);
script.onload = () => {
kakao.maps.load(() => {
const map = new kakao.maps.Map(container.current);
//setMapCenter(center);
setKakaoMap(map);
});
};
}, [container]);
위의 방법은 해당 컴포넌트에서만 카카오 script 를 비동기적으로 로드할 수 있는 방법이다. 이로 인해 초기 페이지에서 스크립트 때문에 렌더링이 블로킹 되는 문제 는 해결된 것으로 보인다...! 하지만 useEffect 를 사용하면서 왠지 모르게 찝찝함이 생겼다.
우리 프로젝트에서는 카카오 맵 api 을 다음과 같은 곳에 사용한다.
| 디테일 페이지 | 실시간 주소 페이지 |
|
|
두 페이지는 방끗의 핵심 기능들이다. 사용자가 매번 해당 페이지를 열 때마다 똑같은 kakao map 스크립트를 매번 불러오는 것은 비효율적이라는 생각이 들었다. 이렇게 된다면, html 에서 script 를 한번만 불러오는 처음의 상황이 더 낫다는 생각이 들었다.
예상과 같이 네트워크 탭을 확인해 보니, 위 페이지/ 모달을 새로 열때마다 똑같은 kakao map script 를 계속 불러왔다.
그래서 내가 생각한 해결책은,
나는 한번 스크립트를 불러온뒤 그 상태를 저장하고, 이미 불러온 상태이면 스크립트를 로드하지 않는 유틸 을 만드는 것이었다.


처음에는 위와 같이 카카오 map script 에만 국한된 코드였으나, 우리가 쓰는 다른 다음 주소 api 도 해당 스크립트 유틸을 사용하면 좋을 것 같다는 생각이 들었다. 그래서 다음과 같은 유틸 코드를 만들었다.
type ScriptType = 'kakaoMap' | 'daumAddress';
const scripts = {
kakaoMap: {
url: `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.KAKAO_MAP_KEY}&autoload=false&libraries=services`,
loaded: false,
},
daumAddress: {
url: '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js',
loaded: false,
},
};
const loadExternalScriptWithCallback = (scriptType: ScriptType, callback: () => void) => {
const scriptInfo = scripts[scriptType];
if (!scriptInfo) {
throw new Error('지원되지 않는 스크립트 타입입니다.');
}
if (scriptInfo.loaded) {
callback();
return;
}
const script = document.createElement('script');
script.src = scriptInfo.url;
script.async = true;
script.onload = () => {
scriptInfo.loaded = true;
if (callback) callback();
};
script.onerror = error => {
console.error(`스크립트 로드에 실패했습니다: ${scriptType}`, error);
};
document.head.appendChild(script);
};
export default loadExternalScriptWithCallback;
위와 같이 유틸을 만들고, 해당 스크립트 이후에 실행될 동작을 callback 함수에 넣어주어 사용하였다. 덕분에, 다음과 같은 효과를 얻었다!
힘든 과정이었지만, 쓸만한 유틸을 만든 것 같아서 마음에 든다.😁