개발일기

NextJs 에서 Kakao Maps와 Geolocation으로 인한 초기 위치 문제 해결하기 본문

SideProject(My-Selectshop-Finder)

NextJs 에서 Kakao Maps와 Geolocation으로 인한 초기 위치 문제 해결하기

황대성 2024. 7. 4. 01:32

개요

사이드 프로젝트를 진행중 홈페이지에 들어가면 먼저 내 위치의 주변 카페가 나오게 하려고 했는데 초기값으로 넣어둔 카카오맵 제주도 본사가 나온다. 코드는 잘 작성한거 같은데 해결되지 않아서 생각보다 오랜 시간이 걸렸다. console.log()를 활용해서 어디가 문제인지 확인 했고, 어떻게 코드가 돌아가는지 더 자세히 알아봤다. 코드를 한번 확인해 보자.

 

문제

import React, { useEffect, useState } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";
import styled from "styled-components";
import SearchContainer from "@/components/SearchContainer";
import { useRecoilState } from "recoil";
import { cafesState } from "@/globalState/recoilState";

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

interface MarkersType {
  position: {
    lat: number;
    lng: number;
  };
}

const Home = () => {
  const [state, setState] = useState({
    center: {
      lat: 33.450701,
      lng: 126.570667,
    },
    errMsg: null,
    isLoading: true,
  });
  const [map, setMap] = useState<any>();
  const [markers, setMarkers] = useState<any>();
  const [, setCafes] = useRecoilState<any>(cafesState);

  useEffect(() => {
    kakao.maps.load(() => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position: GeolocationPosition) => {
            const lat = position.coords.latitude; // 위도
            const lng = position.coords.longitude; // 경도
            setState((prev) => { //내 위치 state값에 저장하기
              return { ...prev, center: { lat, lng }, isLoading: false };
            });
          }
        );
      }
    const searchPlaces = () => {
      const ps = new kakao.maps.services.Places();
      const category = "CE7"; // 카페
      const options = {
        location: new kakao.maps.LatLng(state.center.lat, state.center.lng),
        sort: kakao.maps.services.SortBy.DISTANCE,
      };
      ps.categorySearch(category, placesSearchCB, options);
    };
    const placesSearchCB = (data: any, status: string, pagination: any) => {
      if (status === kakao.maps.services.Status.OK) {
        setCafes(data);  // 카페 정보 setCafes에 저장하기
        displayPlaces(data);
      }
    };
    const displayPlaces = (data: any) => {
      const bounds = new kakao.maps.LatLngBounds();
      let markers: any = [];
      data.forEach((place: any) => {
        markers.push({
          position: {
            lat: place.y,
            lng: place.x,
          },
        });
      });
      setMarkers(markers); // 주변 카페 좌표 setMarkers에 저장하기
    };
    searchPlaces();
   });
  }, []);

  return (
    <S.Container>
      <S.SideContainer>
        <SearchContainer />
      </S.SideContainer>
      <main>
        <Map
          center={{ lat: state.center.lat, lng: state.center.lng }}
          style={{ width: "100%", height: "100vh" }}
          onCreate={setMap}
        >
          {markers?.map((marker: MarkersType) => {
            return (
              <MapMarker
                key={`marker-${marker.position.lat},${marker.position.lng}`}
                position={marker.position}
              />
            );
          })}
          {!state.isLoading && (
            <MapMarker
              position={state.center}
              image={{
                src: "/myPositionMarker.png",
                size: {
                  width: 50,
                  height: 50,
                },
              }}
            />
          )}
        </Map>
      </main>
    </S.Container>
  );
};

export default Home;

 

전체 코드를 살펴보면 위와 같다. 코드를 한번 해석해 보자.

1. 초기 좌표가 카카오맵에서 제공하는 제주도 본사인 state라는 변수를 생성해서 만약 내 위치를 불러오지 못할 시 초기값을 띄운다.

2. 만약 내 위치를 찾았다면 state값에 내 위치 좌표를 저장한다.

3. 저장된 내 위치 좌표를 기준으로 주변 카페의 정보와 마커를 화면에 표시한다.

내가 생각하고 작성한 코드는 이렇다. 하지만 생각처럼 동작하지 않았고, 내 위치가 아닌 초기값의 위치만을 띄워줬다. 그래서 어느 부분이 문제인지 확인하기 위해 console.log()를 활용해서 디버깅 해보았다.

 

내 위치가 잘 들어오는지 확인하기

if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition((position: any) => {
          console.log(position);  // 내 위치 확인
          const lat = position.coords.latitude; // 위도
          const lng = position.coords.longitude; // 경도
          setState((prev) => {
            return { ...prev, center: { lat, lng }, isLoading: false };
          });
        });
      }

 

latitude와 longitude의 값을 봐서는 내 위치가 잘 들어오고 있다. 그러면 들어온 값을 state에 저장해서 사용하는데 왜 내 위치의 주변 카페가 나오지 않는 걸까? 주변 카페의 데이터를 한번 확인해 보자.

 

주변 카페 잘 들어오는지 확인하기

const placesSearchCB = (data: any, status: string, pagination: any) => {
      if (status === kakao.maps.services.Status.OK) {
      	console.log(data) // 카페 정보 확인
        setCafes(data);  // 카페 정보 setCafes에 저장하기
        displayPlaces(data);
      }
    };

 

주변 카페는 잘 들어오지 않는다. 뭐가 문제일까 생각해 보다가 모든 코드를 useEffect에 사용중인걸 망각했다,, 내 위치 값을 state에 저장하고 그것을 사용하기 전에 placesSearchCB 함수가 먼저 실행되서 state의 초기값인 제주도 본사를 사용하고 있던 것 이였다.

NextJs에서 카카오맵을 사용할 때 왜 useEffect안에서만 사용해야 하는가?
kakao Map API는 클라이언트 측에서만 접근 가능한 브라우저 전용 객체와 API에 의존하기 때문이다. NextJs는 SSR(Server-Side-rendering)을 기본으로 하지만 Kakao Map API는 CSR(Client-Side_rendering)에서만 작동한다. 'window', 'document', 'navigator' 와 같은 객체는 클라이언트 측에서만 접근 가능하기 때문에 useEffect 훅을 사용하여 브라우저 전용 객체와 API를 사용할 수 있다.

 

해결 방법

문제를 해결하기 위해 내 위치를 불러오고 state에 저장하는 부분과 state의 위치 값을 이용해서 내 주변 카페를 불러오는 부분의 useEffect()를 나눠서 위치 값을 저장하고, 그 위치 값이 존재 할 때 주변 카페를 불러오도록 했다. 두번째 useEffect()는 주변 카페를 컨트롤 하기 때문에 의존성 배열에 map과 state(내 위치)를 추가하여 두개의 값이 바뀔 때 마다 새로운 카페 데이터를 불러오도록 했다. 코드는 아래와 같다.

import React, { useEffect, useState } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";
import styled from "styled-components";
import SearchContainer from "@/components/SearchContainer";
import { useRecoilState } from "recoil";
import { cafesState } from "@/globalState/recoilState";

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

interface MarkersType {
  position: {
    lat: number;
    lng: number;
  };
}

const Home = () => {
  const [state, setState] = useState({
    center: {
      lat: 33.450701,
      lng: 126.570667,
    },
    errMsg: null,
    isLoading: true,
  });
  const [map, setMap] = useState<any>();
  const [markers, setMarkers] = useState<any>();
  const [, setCafes] = useRecoilState<any>(cafesState);

  useEffect(() => {
    kakao.maps.load(() => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position: GeolocationPosition) => {
            const lat = position.coords.latitude; // 위도
            const lng = position.coords.longitude; // 경도
            setState((prev) => {
              return { ...prev, center: { lat, lng }, isLoading: false };
            });
          }
        );
      }
    });
  }, []);

  useEffect(() => {
    if (map && state.center.lat && state.center.lng) {
      const searchPlaces = () => {
        const ps = new kakao.maps.services.Places();
        const category = "CE7";
        const options = {
          location: new kakao.maps.LatLng(state.center.lat, state.center.lng),
          sort: kakao.maps.services.SortBy.DISTANCE,
        };
        ps.categorySearch(category, placesSearchCB, options);
      };
      const placesSearchCB = (data: any, status: string, pagination: any) => {
        if (status === kakao.maps.services.Status.OK) {
          setCafes(data);
          displayPlaces(data);
        }
      };
      const displayPlaces = (data: any) => {
        const bounds = new kakao.maps.LatLngBounds();
        let markers: any = [];
        data.forEach((place: any) => {
          markers.push({
            position: {
              lat: place.y,
              lng: place.x,
            },
          });
        });
        setMarkers(markers);
        // map.setBounds(bounds);
      };
      searchPlaces();
    }
  }, [map, state]);

  return (
    <S.Container>
      <S.SideContainer>
        <SearchContainer />
      </S.SideContainer>
      <main>
        <Map
          center={{ lat: state.center.lat, lng: state.center.lng }}
          style={{ width: "100%", height: "100vh" }}
          onCreate={setMap}
        >
          {markers?.map((marker: MarkersType) => {
            return (
              <MapMarker
                key={`marker-${marker.position.lat},${marker.position.lng}`}
                position={marker.position}
              />
            );
          })}
          {!state.isLoading && (
            <MapMarker
              position={state.center}
              image={{
                src: "/myPositionMarker.png",
                size: {
                  width: 50,
                  height: 50,
                },
              }}
            />
          )}
        </Map>
      </main>
    </S.Container>
  );
};

export default Home;

 

마무리

해결을 하고 나서는 너무 간단한 것이였지만 해결하는 과정에서는 너무 어려운 것처럼 느껴졌다. NextJs의 문제일까, 카카오맵의 문제일까, 코드의 문제일까 어떤 것이 문제일지 경우의 수를 좁히는게 역시 간단한 일이 아니다. 하지만 여러 문제를 해결 하다 보면 역시 비슷한 문제들을 겪게 되고, 그것들이 점점 더 적은 문제의 경우의 수를 만들지 않을까 한다.