JaeSeoKim / react-kakao-maps-sdk

React components for using kakao map api
https://react-kakao-maps-sdk.jaeseokim.dev
MIT License
279 stars 30 forks source link

TypeError: Cannot read properties of undefined (reading 'OverlayType' #51

Closed swjo207 closed 1 year ago

swjo207 commented 1 year ago

Next JS (13.2.4) 마이그레이션 하다가, 우연히 발견하게 되었습니다.

DrawingManager를 사용해야 하는데, 계속 문제에 부딪쳐서 한번 여쭤보고자 합니다. https://react-kakao-maps-sdk.jaeseokim.dev/docs/sample/library/basicDrawingLibrary 샘플을 /app/map/page.tsx 에서 복사해서 띄워 보는데, TypeError: Cannot read properties of undefined (reading 'OverlayType') 타입 에러가 뜨고 진전이 없습니다.

tsconfig.json 파일에,

"types": [ "kakao.maps.d.ts" ]

가 설정되어 있으며, vscode 에디터에서도 OverlayType이 레퍼런싱이 되는 걸로 봐서는 문제가 없을 것 같은데,


테스트한 소스는 다음과 같습니다.

'use client' import Script from "next/script" import { useRef } from "react" import { Map, DrawingManager } from "react-kakao-maps-sdk"

declare global { interface Window { kakao: any; } }

let map_src = //dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_API_KEY}&autoload=false&libraries=drawing,services,clusterer;

export default function MapEdit() { // ref 객체를 통해 kakao.maps.drawng.DrawingManager 객체를 전달 받아 사용합니다. // 또한 TypeScript를 사용하기 떄문에 전달 받는 DrawingManager에서 사용하는 OverlayType에 대해서 정의해야 합니다. const managerRef = useRef< kakao.maps.drawing.DrawingManager< | kakao.maps.drawing.OverlayType.ARROW | kakao.maps.drawing.OverlayType.CIRCLE | kakao.maps.drawing.OverlayType.ELLIPSE | kakao.maps.drawing.OverlayType.MARKER | kakao.maps.drawing.OverlayType.POLYLINE | kakao.maps.drawing.OverlayType.RECTANGLE | kakao.maps.drawing.OverlayType.POLYGON

(null)

function selectOverlay(type: kakao.maps.drawing.OverlayType) {
    const manager = managerRef.current
    manager?.cancel()
    manager?.select(type)
}

return (
    <>
        <Script
            src={map_src}
            strategy="beforeInteractive"
        // onLoad={() => {
        //   console.log("카카오맵 스크립트 로딩 완료. ~~~~~");
        // }}
        // onReady={() => {
        //   console.log("카카오맵 스크립트 준비 완료. ~~~~~");
        // }}
        />
        <Map
            center={{
                // 지도의 중심좌표
                lat: 33.450701,
                lng: 126.570667,
            }}
            style={{
                width: "100%",
                height: "450px",
            }}
            level={3} // 지도의 확대 레벨
        >
            <DrawingManager
                ref={managerRef}
                drawingMode={[
                    kakao.maps.drawing.OverlayType.ARROW,
                    kakao.maps.drawing.OverlayType.CIRCLE,
                    kakao.maps.drawing.OverlayType.ELLIPSE,
                    kakao.maps.drawing.OverlayType.MARKER,
                    kakao.maps.drawing.OverlayType.POLYLINE,
                    kakao.maps.drawing.OverlayType.RECTANGLE,
                    kakao.maps.drawing.OverlayType.POLYGON,
                ]}
                guideTooltip={["draw", "drag", "edit"]}
                markerOptions={{
                    // 마커 옵션입니다
                    draggable: true, // 마커를 그리고 나서 드래그 가능하게 합니다
                    removable: true, // 마커를 삭제 할 수 있도록 x 버튼이 표시됩니다
                }}
                polylineOptions={{
                    // 선 옵션입니다
                    draggable: true, // 그린 후 드래그가 가능하도록 설정합니다
                    removable: true, // 그린 후 삭제 할 수 있도록 x 버튼이 표시됩니다
                    editable: true, // 그린 후 수정할 수 있도록 설정합니다
                    strokeColor: "#39f", // 선 색
                    hintStrokeStyle: "dash", // 그리중 마우스를 따라다니는 보조선의 선 스타일
                    hintStrokeOpacity: 0.5, // 그리중 마우스를 따라다니는 보조선의 투명도
                }}
                rectangleOptions={{
                    draggable: true,
                    removable: true,
                    editable: true,
                    strokeColor: "#39f", // 외곽선 색
                    fillColor: "#39f", // 채우기 색
                    fillOpacity: 0.5, // 채우기색 투명도
                }}
                circleOptions={{
                    draggable: true,
                    removable: true,
                    editable: true,
                    strokeColor: "#39f",
                    fillColor: "#39f",
                    fillOpacity: 0.5,
                }}
                polygonOptions={{
                    draggable: true,
                    removable: true,
                    editable: true,
                    strokeColor: "#39f",
                    fillColor: "#39f",
                    fillOpacity: 0.5,
                    hintStrokeStyle: "dash",
                    hintStrokeOpacity: 0.5,
                }}
                arrowOptions={{
                    draggable: true, // 그린 후 드래그가 가능하도록 설정합니다
                    removable: true, // 그린 후 삭제 할 수 있도록 x 버튼이 표시됩니다
                    editable: true, // 그린 후 수정할 수 있도록 설정합니다
                    strokeColor: "#39f", // 선 색
                    hintStrokeStyle: "dash", // 그리중 마우스를 따라다니는 보조선의 선 스타일
                    hintStrokeOpacity: 0.5, // 그리중 마우스를 따라다니는 보조선의 투명도
                }}
                ellipseOptions={{
                    draggable: true,
                    removable: true,
                    editable: true,
                    strokeColor: "#39f",
                    fillColor: "#39f",
                    fillOpacity: 0.5,
                }}
            />
        </Map>
        <div
            style={{
                display: "flex",
                gap: "8px",
            }}
        >
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.POLYLINE)
                }}
            >
                선
            </button>
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.ARROW)
                }}
            >
                화살표
            </button>
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.CIRCLE)
                }}
            >
                원
            </button>
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.MARKER)
                }}
            >
                마커
            </button>
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.POLYGON)
                }}
            >
                다각형
            </button>
            <button
                onClick={(e) => {
                    selectOverlay(kakao.maps.drawing.OverlayType.RECTANGLE)
                }}
            >
                사각형
            </button>
        </div>
    </>
)

}

JaeSeoKim commented 1 year ago

일단 해당 문제점은 kakao 객체는 SSR시점에서는 접근이 불가능한 객체 입니다.

해당 문제점의 힌트는 본문에 작성하신 declare global { module Window { kakao: any; } } 입니다. nextjs에서는 아마 kakao.maps.d.ts를 설치하였어도, kakao.maps 형태로 직접 접근하는 것이 타입 추론을 해주지 않고, window.kakao.maps로 접근해야 합니다. 아마 kakao.maps declare 선언을 하신 이유가 해당 사실에 대해서 모르고 있으셨던 것 같습니다. 다시 본론으로 돌아와서 window.kakao.maps에서 접근하는 것을 nextjs에서 강제한 이유는 Client시점에서만 접근이 가능한 객체라는 것을 인지 시키기 위해서 입니다. Script를 이용해서 load를 하거나, useInjectKakaoMapApi hook를 이용하여 load를 하게 되면 외부에서 js를 로드하여 브라우저에서 실행을 시켜, window.kakao.map에 객체를 추가를 하게 됩니다.

이때 page.tsxkakao.maps.drawing.OverlayType의 경우 단순한 type이 아닌 TypeScript의 특이한 객체인 enum입니다. 일반적으로 TypeScript에서 새로 추가된 문법은 단순하게 트랜스파일 시점에만 활용하는 부가 정보를 담은 문법이지만, enum의 경우 실제 JavaScript로 생성되는 코드에 추가적으로 객체를 생성하는 문법입니다. 참고: https://www.typescriptlang.org/ko/docs/handbook/enums.html

따라서 해당 Type정의의 경우 실제 객체가 있다는 것을 가정하고 작성하였기 때문에 windows.kakao.maps가 로딩 되기 전까지는 접근이 불가능한 객체이므로 런타임에서 오류가 발생하는 것 입니다.

이를 해결하기 위해서는 몇가지 방법이 있습니다. 첫번째로는 Map 컴포넌트의 children으로 넣는 객체의 경우 라이브러리 내부적으로 kakao.maps가 로딩완료된 이후에 렌더링 하도록 되어 있습니다. 이를 이용하여, kakao.maps.drawing.OverlayType를 사용하는 부분을 다른 컴포넌트로 분리하여 작성하게 되면 문제가 해결 됩니다. 두번째로는 useInjectKakaoMapApi 혹인 next.js에서 제공하는 Script에서 onLoad 등의 이벤트를 활용하여, kakao.maps가 로딩이 끝났는지를 체크하여, 렌더링을 하는 것을 직접 제어 하는 것 입니다. 마지막 방법은 제가 지원을 해야 하는 방법입니다. enum의 경우 객체가 있어야 한다는 문제점을 해결하기 위해, 내부 값을 추론할 수 있도록 string literal union type을 추가하여 지원하는 방법입니다. 문제점은 kakao측에서 내부에서 사용하는 객체의 값이 변경된 경우 즉시 enum 객체값을 1대1로 대응시킬 수 없기 때문에 변경이 발생하면 오류가 발생할 수 있습니다.

JaeSeoKim commented 1 year ago

마지막 세번째 방법에 대해서 코드를 다시확인 해본 결과 내부적인 값이 변경될 일이 없을 것 같아서 해당 부분 작업하여, 추후 다음 버전에 반영하도록 하겠습니다.

https://tsplay.dev/W4bRaN