미리보기

마지막 프로젝트로 팀원분들과 편의점 음식 조합을 소개해주고 공유하는 서비스를 만들고 있습니다.
제가 담당한 파트로는 카카오맵으로 주변 편의점이 어디있는지 찾아주는 기능입니다.
링크 태그로 해당 지역의 카카오맵 링크를 찾아볼 수 있습니다.

준비물

https://developers.kakao.com/console 

 

카카오계정

 

accounts.kakao.com

- 우선 카카오 API를 사용하기 위해서 develop홈페이지에서 플랫폼을 등록하시고,

API Key를 코드내에 적용시킵니다.

 // index.html
 
 <!-- 카카오맵 API KEY -->
    <script
      type="text/javascript"
      src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%REACT_APP_KAKAO_API_KEY%&libraries=services,clusterer"
    ></script>

- 가져온 Javascript 키를 index.html에 넣어줍니다. 하드코딩 하는거 보다는 .env나 .env.local 파일에 코드를 변수로 할당해놓고 사용해주는 것이 안전하다 생각했습니다.

 

 

자신의 위치 좌표 찾기

  // KakaoMap.tsx
  
  // 현재 자신의 위치 좌표를 지정해줍니다.
  const setMyPosition = () => {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition((position) => {
        let lat = position.coords.latitude;
        let lng = position.coords.longitude;
        setMyLat(lat); 
        setMyLng(lng); 
      });

      console.log('위치 수정 완료');
    } else {
      // 현재위치를 알 수 없는 경우, 기본 값을 설정합니다.
      setMyLat(37); // 서울 위도
      setMyLng(127); // 서울 경도
    }
  };

-JavaScript API 로 navigator.geolocation로 사용자의 위치 정보를 알아올 수 있습니다. 

- 해당 getCurrentPosition() 함수는 콜백 함수를 매개 변수로 받아서 좌표를 얻어오면 position에 정보를 담아옵니다.

 

자신의 좌표 주변의 편의점 리스트 받아오기

// GetConvList.ts

const MAX_RESULTS = 5; // 가져올 편의점 개수

export const GetConvList = async (lat: number, lng: number): Promise<ConvsInform[]> => {
  try {
    const ps = new kakao.maps.services.Places();
    const result = await new Promise<any>((resolve, reject) => {
      ps.categorySearch(
        'CS2', // 편의점
        (data: any, status: any) => {
          if (status === kakao.maps.services.Status.OK) {
            resolve(data);
          } else {
            reject(new Error('편의점 검색 실패'));
          }
        },
        {
          location: new kakao.maps.LatLng(lat, lng),
          useMapCenter: true,
          size: MAX_RESULTS,
          radius: 5000,
          sort: kakao.maps.services.SortBy.DISTANCE
        }
      );
    });

    const convs: ConvsInform[] = [];

    const nearestConvs: ConvsInform[] = [];
    const brandArr = ['세븐일레븐', 'CU', 'GS25', '이마트24', '미니스톱'];

    for (let i = 0; i < Math.min(MAX_RESULTS, result.length); i++) {
      const data = result[i];
      const splitedPlace = data.place_name.split(' ');
      console.log(splitedPlace);
      const [brand_name, position_name] = [splitedPlace[0], splitedPlace[1]];

      const newConv: ConvsInform = {
        position: {
          lat: data.y,
          lng: data.x
        },
        brand_name,
        position_name,
        full_name: data.place_name,
        distance: GetDistanceBtw(lat, lng, Number(data.y), Number(data.x), 'meter')
      };
      convs.push(newConv);
    }

    for (const brand of brandArr) {
      let conv = convs.find((conv) => brand === conv.brand_name);
      conv !== undefined
        ? nearestConvs.push(conv)
        : nearestConvs.push({  // 없으면 빈 배열을 넣어줍니다
            brand_name: brand,
            position_name: '',
            full_name: '',
            distance: 0,
            position: { lat: 0, lng: 0 }
          });
    }
    // console.log(nearestConvs)
    return nearestConvs;
    // return convs;
  } catch (error) {
    console.error('편의점 리스트 가져오기 오류:', error);
    return [];
  }
};

- <ConvsInfrom> 은 받아온 편의점 자료를 받아올 타입입니다. types>types.ts 폴더에서 가져옵니다.

-ps에 카카오 장소 제공 서비스 객체를 담아와서 사용해줍니다.

- 카테고리 코드에 맞게 장소를 제공해주는 categorySearch함수를 사용해줍니다.

- 매개변수로 결과값을 받을 콜백함수도 있고 그 안에서 데이터를 처리해줍니다.

- options: {} 매개변수로 또, options가 있는데 locaition으로 사용자의 중심 위치를 설정하고

radius로 중심에서 -- m 반경 검색, sort로 거리순으로 받아오기 옵션을 선택해줄 수 있습니다.

-편의점 리스트 원소로 distance가 있는데 사용자와 편의점의 거리를 의미합니다.

-> 이는, GetDistanceBtw 함수로 사용자 좌표, 편의점 좌표, 타입(m,kilometer)를 이용해 거리를 구해줍니다.

 

거리 구하는 함수

/**
 * 두 지점간의 거리 계산 함수
 *
 * @param lat1 지점 1 위도
 * @param lon1 지점 1 경도
 * @param lat2 지점 2 위도
 * @param lon2 지점 2 경도
 * @param unit 거리 표출단위(kilometer, meter)
 * @return
 */

export const GetDistanceBtw = (lat1: number, lon1: number, lat2: number, lon2: number, unit: string): number => {
  /**
   * 1000미만이면 소수점 없앤 정수 반환, 이상이면 km 소수점 1로 소수 반환
   * @param m 미터를 기준
   * @returns 정수(--)m, 소수(--.-)km number타입으로 반환
   */
  const calculDistance = (m: number): number => {
    let meter;

    if (m < 1000) {
      meter = Math.floor(m);
    }
    if (m >= 1000) {
      meter = m / 1000;
      meter = Number(meter.toFixed(1));

      // if (meter % 1 === 0) meter = meter.toFixed(0);
    }
    return meter as number;
    // meter가 정수이면 m단위, 소수점이 있으면 km단위
    //(예외: 1.0km -> 1km로 바꾸는 작업 필요)
  };

  const deg2rad = (deg: number) => deg * (Math.PI / 180);
  const rad2deg = (rad: number) => rad * (180 / Math.PI);

  const theta = lon1 - lon2;
  let dist =
    Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));

  dist = Math.acos(dist);
  dist = rad2deg(dist);
  dist = dist * 60 * 1.1515;

  if (unit === 'kilometer') {
    dist = dist * 1.609344;
  } else if (unit === 'meter') {
    dist = dist * 1609.344;
  }
  return calculDistance(dist);
};

- 다른 블로그에서 정보를 얻어 해당 함수를 가져왔습니다. 

- 좌표를 받아서 결과값이 999까지는 소수점 없는 정수로 m로 표현하기 위해서

1000이상은 소수점 첫째짜리로 만들어 반환하게 해줬습니다. km로 표현하기 위해서

 

랜더링

 return (
    <S.Container>
      {nearConv && (
        <>
          <S.Title>지금 나랑 가장 가까운 편의점은?</S.Title>
          {nearConv.distance ? (
            <>
              <S.LocationButton
                to={`https://map.kakao.com/link/map/${nearConv?.full_name},${myLat},${myLng}`}
                target="_blank"
              >
                <S.IconBox>
                  <IconMap />
                </S.IconBox>
                위치 보기
              </S.LocationButton>
              <S.NearByStore>
                <S.NearByLogo> {Logo && <Logo />}</S.NearByLogo>
                <S.StoreInfo>
                  <S.StoreName>{nearConv.position_name}</S.StoreName>
                  <S.Distance>
                    {Math.floor(nearConv.distance) === nearConv.distance
                      ? nearConv.distance + 'm'
                      : nearConv.distance + 'km'}
                  </S.Distance>
                </S.StoreInfo>
              </S.NearByStore>
            </>
          ) : (
            <S.NearByStore>
              <S.NoStore>{'반경 5km 내\n가까운 편의점이 없습니다.'}</S.NoStore>
            </S.NearByStore>
          )}
        </>
      )}
      <S.NearByBrand>
        {convs
          .filter((item) => {
            return item.brand_name !== nearConv?.brand_name;
          })
          .map((item, index) => {
            return <NearByBox key={index} brand={item} />;
          })}
      </S.NearByBrand>
    </S.Container>
  );
};

 

- 여태해준 작업 함수를 호출해주고 값을 편의점 리스트에 저장해서 값을 적절하게 랜더링 시켜줍니다.

- 받아올 때 useEffect나 useMemo 훅을 이용하여 좌표가 변할 시에만 편의점 리스트 값을 새로 불러오게끔 

해주는 과정이 중요합니다. 이렇게 안하면 순식간에 초당 300건의 자료를 불러오는 불상사가 일어납니다 ... ㅜ

 

 

 

작업파일

📦kakaoMap
 ┣ 📜GetConvList.ts
 ┣ 📜GetDetailAddress.tsx
 ┣ 📜GetDistanceBtw.ts
 ┗ 📜KakaoMap.tsx

 

 

참조

https://fruitdev.tistory.com/189

 

https://apis.map.kakao.com/web/sample/categoryFromBounds/

 

+ Recent posts