카카오맵 API addressSearch() 사용 중 겪은 이슈 (400 Bad Request)

김방울·2022년 12월 5일
0

Issue & Memoirs🐱

목록 보기
2/2

"next": "12.3.1",
react-kakao-maps-sdk": "^1.1.5" 버전 기준으로 작성되었습니다.

Issue

{"errorType":"AccessDeniedError","message":"cannot find Authorization : KakaoAK header"}

카카오맵 API 사용 중 콘솔창에 다음과 같은 오류가 출력되었습니다.🤔

수정 전 코드

// _app.js
function MyApp({ Component, pageProps }) {
  const APP_KEY = process.env.NEXT_PUBLIC_KAKAOMAP_APPKEY;
  ...
 return (
    <>
      <Script src={`https://dapi.kakao.com/v2/maps/sdk.js?appkey=${APP_KEY}&libraries=services&autoload=false`} />
      <HeadInfo />
      <GlobalStyle />
      <ThemeProvider theme={defaultTheme}>
        <Component {...pageProps} />
      </ThemeProvider>
    </>
  )
}

APP Key와 Script는 _app.js 에서 로드해 주었습니다. 원래 카카오맵을 사용하는 컴포넌트에서 로드하고자 했는데, 자꾸 window 객체 관련 에러가 떠서 일단 최상위 컴포넌트에서 로드하였습니다. 이 문제도 dynamic import 같은 걸로 해결할 수 있을 것 같은데...🤔 추후에 다시 수정해봐야겠습니다.

import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { Map, MapMarker } from 'react-kakao-maps-sdk';

const MapView = (props) => {


  //  PARAM state
  const [loaded, setLoaded] = useState(false);
  const [latitude, setLatitude] = useState0(//지도 위도 좌표); // 지도 위도 좌표
  const [longitude, setLongitude] = useState(// 지도 경도 좌표); // 지도 경도 좌표

  // PARAM ref (지도 wrapper)
  const mapRef = useRef();

  // FUNCTION 지도 / 스카이뷰 전환
  const setMapType = (maptype) => {
    const map = mapRef.current;
    const roadmapControl = document.getElementById('btnRoadmap');
    const skyviewControl = document.getElementById('btnSkyview');
    if (maptype === 'roadmap') {
      map.setMapTypeId(kakao.maps.MapTypeId.ROADMAP);
      roadmapControl.className = 'selected_btn';
      skyviewControl.className = 'btn';
    } else {
      map.setMapTypeId(kakao.maps.MapTypeId.HYBRID);
      skyviewControl.className = 'selected_btn';
      roadmapControl.className = 'btn';
    }
  };

  // FUNCTION 지도 확대
  const zoomIn = () => {
    const map = mapRef.current;
    map.setLevel(map.getLevel() - 1);
  };

  // FUNCTION 지도 축소
  const zoomOut = () => {
    const map = mapRef.current;
    map.setLevel(map.getLevel() + 1);
  };

  // FUNCTION 좌표 검색
  const onSearchAddress = (address) => {
    // PARAM 주소-좌표 변환 객체
    const geocoder = new kakao.maps.services.Geocoder();
    geocoder.addressSearch(address, function (result, status) {
      // 정상적으로 검색이 완료됐을 때
      if (status === kakao.maps.services.Status.OK) {
        const coords = new kakao.maps.LatLng(
          result[0].y,
          result[0].x
        );
        setLatitude(coords.Ma);
        setLongitude(coords.La);
      }
    });
  };

  useEffect(() => {
    if (window.kakao) {
      const onLoadKakaoMap = () => {
        window.kakao.maps.load(() => {
          onSearchAddress(props.address);
          setLoaded(true);
        })
      }
      onLoadKakaoMap();
    }
  }, [props.address]);

  return (
    <>
      {
        loaded ?
          <div id='MapWrapper' className='MapWrapper'>
            <Map
              center={{ lat: latitude, lng: longitude }}
              style={{
                width: '100%',
                height: '18rem',
                position: 'relative',
                overflow: 'hidden',
              }}
              level={3}
              ref={mapRef}
            >
              <MapMarker
                className='MapWrapper__map-marker'
                position={{ lat: latitude, lng: longitude }}
              >
                {/* MapMarker의 자식을 넣어줌으로 해당 자식이 InfoWindow로 만들어지게 합니다 */}
                {/* 인포윈도우에 표출될 내용으로 HTML 문자열이나 React Component가 가능합니다 */}
              </MapMarker>
              <div className='MapWrapper__bubble'>
                <div className='MapWrapper__bubble-title'>{props.placeName}</div>
                <div className='MapWrapper__bubble-link-wrap'>
                  <a
                    href={`https://map.kakao.com/link/map/${props.placeName},${latitude},${longitude}`}
                    className='MapWrapper__bubble-link'
                    target='_blank'
                    rel='noreferrer'
                  >
                    큰지도보기
                  </a>
                  <a
                    href={`https://map.kakao.com/link/to/${props.placeName},${latitude},${longitude}`}
                    className='MapWrapper__bubble-link'
                    target='_blank'
                    rel='noreferrer'
                  >
                    길찾기
                  </a>
                </div>
              </div>
            </Map>
            {/* COMPONENT controller */}
            <div className='custom_typecontrol radius_border'>
              <span
                id='btnRoadmap'
                className='selected_btn'
                onClick={() => setMapType('roadmap')}
              >
                지도
              </span>
              <span
                id='btnSkyview'
                className='btn'
                onClick={() => {
                  setMapType('skyview');
                }}
              >
                스카이뷰
              </span>
            </div>
            <div className='custom_zoomcontrol radius_border'>
              <span onClick={zoomIn}>
                <div className='custom_zoomcontrol-icon'>
                  <Image
                    src='https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/ico_plus.png'
                    alt='확대'
                    layout='fill'
                  />
                </div>
              </span>
              <span onClick={zoomOut}>
                <div className='custom_zoomcontrol-icon'>
                  <Image
                    src='https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/ico_minus.png'
                    alt='축소'
                    layout='fill'
                  />
                </div>
              </span>
            </div>
          </div>
          : null
      }
    </>
  );
};

axios 비동기 통신으로 주소 정보를 받아온 뒤, props를 통해 카카오맵 api를 사용하는 MapView 컴포넌트로 주소 정보를 넘겨줍니다.

  // FUNCTION 좌표 검색
  const onSearchAddress = (address) => {
    // PARAM 주소-좌표 변환 객체
    const geocoder = new kakao.maps.services.Geocoder();
    geocoder.addressSearch(address, function (result, status) {
      // 정상적으로 검색이 완료됐을 때
      if (status === kakao.maps.services.Status.OK) {
        const coords = new kakao.maps.LatLng(
          result[0].y,
          result[0].x
        );
        setLatitude(coords.Ma);
        setLongitude(coords.La);
      }
    });
  };

주소를 카카오맵으로 출력하려면 위/경도 좌표로 변환해 주어야 하기 때문에, Kakao맵 API의 Geocoder 객체를 사용하는 onSearchAddress 라는 함수를 만들어 주었습니다.
정상적으로 검색이 완료되면 위도와 경도 정보를 반환하는데, 이를 컴포넌트의 state에
연동해 주었습니다.

[카카오맵 API 가이드 - Geocoder]
https://apis.map.kakao.com/web/sample/addr2coord/

  useEffect(() => {
    if (window.kakao) {
      const onLoadKakaoMap = () => {
        window.kakao.maps.load(() => {
          onSearchAddress(props.address);
          setLoaded(true);
        })
      }
      onLoadKakaoMap();
    }
  }, [props.address]);
...

return (
    <>
      {
        loaded ?
          <div id='MapWrapper' className='MapWrapper'>
            <Map
              center={{ lat: latitude, lng: longitude }}
              style={{
                width: '100%',
                height: '18rem',
                position: 'relative',
                overflow: 'hidden',
              }}
              level={3}
              ref={mapRef}
            >

useEffect 훅을 이용하여 props로 넘겨 준 주소 정보가 변경되면, onSearchAddress 함수를 실행하여 검색된 좌표를 state로 업데이트해 줍니다.
그 뒤 loaded state를 true로 변경해 주어 지도를 렌더링시켜 주었습니다.

지도가 잘 표시되어서 성공! 인 줄 알았습니다
콘솔창의 400 Bad Request를 보기 전까지 ... 😇

Solve Process

인터넷에 검색을 해 보니, REST API 방식으로 호출하였을 때 400 Bad Request 에러가 많이 뜨는 것 같았는데, 대부분 Authorization: KakaoAK ${REST_API_KEY} 에 올바른 REST API KEY를 넣어 주면 해결되는 것 같았습니다.

일단 Javascript API를 사용하고 있었고, 저는 Javascrpit KEY를 올바르게 넣어 줬기에 위 문제는 아닌 것 같다고 판단되어 계속 검색을 한 결과...🤔

주소 정보에 잘못된 값, 빈 값이 들어있으면 400 Bad Request를 출력합니다.

받아온 주소 정보는 잘못되지 않았는데... 뭔가 이상하다 싶어 다음과 같이 console을 찍어 보았습니다.

  useEffect(() => {
    if (window.kakao) {
      console.log(props.address);
      const onLoadKakaoMap = () => {
        window.kakao.maps.load(() => {
          onSearchAddress(props.address);
          setLoaded(true);
        })
      }
      onLoadKakaoMap();
    }
  }, [props.address]);

그렇습니다,, 비동기로 받아오면서 맨 처음엔 초기 address값을 지정해 주지 않아 undefined 상태였고, 그 후 데이터를 불러오면서 제대로 된 주소로 호출은 되었던.. 모양입니다 😇

  useEffect(() => {
    if (window.kakao && props.address) {
      console.log(props.address);
      const onLoadKakaoMap = () => {
        window.kakao.maps.load(() => {
          onSearchAddress(props.address);
          setLoaded(true);
        })
      }
      onLoadKakaoMap();
    }
  }, [props.address]);

defaultProps 등으로 초기값을 지정해 주는 방법도 있지만 저는 초깃값을 좌표로 따로 설정해 놓은 게 있었기에, 주소 정보가 있을 때만 onSearchAddress() 함수를 실행시키는 방법으로 변경해 보았습니다.

다음과 같이 address 정보가 있을 때만 실행이 잘! 됩니다.
콘솔에 빨간 게 안 보이면 기분이 조크든요 😎

회고

  • 비동기 통신을 사용할 때는 데이터 흐름을 꼼꼼히 파악해야겠습니다.
  • next.js를 사용하며 카카오맵 api 적용하면서 시행착오가 여럿 있었는데(키 권한 문제부터 주소가 바로바로 안 불러와진다던가.... 여러 문제들 😇) 그래도 카카오 api가 문서화가 잘 되어있고, 질의응답도 잘 활성화되어 있어 생각보다 이슈들을 금방 해결했던 것 같습니다. 문서화의 중요성을 다시 한번 더 실감하네요👀 문제 하나씩 해결해나가는 과정도 짜릿...했을지도? 혼자서 라이브러리를 선정하여 주도적으로 개발하는 과정이라 처음엔 조금 걱정이 되었지만 재미있게 작업했던 것 같습니다.🙆
profile
코딩하는 고양이🐱 / UI Developer, Front-end Developer

0개의 댓글