2023. 4. 18

Junghan Lee·2023년 4월 18일
0

TIL Diary

목록 보기
37/52

INDEX

카카오 맵 API, 구현, script 태그의 비동기 작동 이슈, 카카오 맵 커스텀(초기 위치 설정, 마커 추가), refetch의 문제점과 개선 방법

카카오 맵 API

검색창에 주소지를 입력하면? 보통 지도가 나타나고 지도를 통해 경로, 주소를 파악할 수 있다. 또한 현재 내 위치를 알려주기도 좋고 맵을 내 프로젝트 안으로 적용한다면 편의성이 향상된다.

지도 API의 종류에는 구글, 네이버, 카카오 등이 있는데 지도 API 간에는 제공하는 기능의 종류, 비용 등의 차이가 있으므로 이를 고려해 API를 결정하면 된다.

카카오 개발자(Kakao Developers)
페이지 : https://developers.kakao.com/

모든 API가 무료는 아니며 일정 사용량 초과시 유료로 전환되는 경우도 있다.

카카오에서 제공하는 개발자 API 사용을 위해서는 어플리케이션을 추가해야 한다.

앱 이름, 사업자명(필수 항목)을 입력 후 저장 버튼을 누르면 새 어플리케이션이 생성된다.

카카오 맵 구현

1) 카카오 맵 API 받아오기

링크 https://apis.map.kakao.com/

페이지 접속 후, 어떤 브라우저에서 이 API를 사용할 것인지 선택하면 됨
ex. 웹 서비스 : Web

2) 왼쪽 하단의 메뉴 바에서 키 발급!

참고) 로그인을 해야 하며, 1일 30만회 요청까지 무료 이용 가능

키 발급 메뉴에 들어가면 생성했던 어플리케이션 목록 확인 가능,
생성한 어플리케이션 클릭 시 앱 키 목록 확인 가능

다양한 종류의 key중 용도에 맞는 키 사용!

3) 맵 구현하기

상세 가이드 : https://apis.map.kakao.com/web/guide/
지도를 띄우는 코드 작성 가이드라인: https://apis.map.kakao.com/web/guide/#:~:text=%EB%A8%BC%EC%A0%80%20%EC%84%A0%EC%96%B8%EB%90%98%EC%96%B4%EC%95%BC%20%ED%95%A9%EB%8B%88%EB%8B%A4.-,%EC%A7%80%EB%8F%84%EB%A5%BC%20%EB%9D%84%EC%9A%B0%EB%8A%94%20%EC%BD%94%EB%93%9C%20%EC%9E%91%EC%84%B1,-var%20container%20%3D%20document

위 가이드라인은 바닐라 JavaScript와 HTML 기준으로 만들어진 코드로 React, 특히 Next JS에서는 HTML에 접근할 수 있는 방법이 한정적이기 때문에 가이드라인 만으로 구현하기 힘들다.

가이드라인에서 사용하고 있는 document 객체는 서버사이드 렌더링을 지원하는 Next.js에서는 화면을 렌더하기 전까지 생성되지 않은 값(undefined)를 가지므로 가이드라인대로 입력하게 되면 오류가 발생한다.

그렇다면 어떻게 불러와야 할까?

1) Head 설정
카카오 맵에서 제공하는 스크립트를 내 프로젝트 안에 적용시켜라.

import Head from 'next/head';

위의 코드를 프로젝트 최상위에 호출한다.

Head컴포넌트는 Next.js에서 기본적으로 제공하는 기능으로 HTML에서 Head태그 안으로 다른 기능들을 호출해 사용하는 것처럼 Next.js의 헤드 컴포넌트를 이용해 동일한 기능을 사용할 수 있다.

Head컴포넌트를 불러온 후 컴포넌트 안에 카카오 맵 스크립트를 호출할 수 있는 코드를 추가한다.( 이 때, JavaScript 앱 키가 필요하다. script태그의 src속성 값 안의 'appkey=' 부분에 자신의 Javascript App Key 값을 넣으면 된다.

 return(
	 <>
			<Head>
				<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey='JavaScript 앱 키 입력'"></script>
			</Head>
	 </>
)

2) 카카오 맵 그리기

앱 키를 넣은 후 내 프로젝트에서 카카오 맵의 스크립트 파일을 호출할 수 있게 되는데 호출한 스크립트와 HTML을 결합해서 화면에 카카오 맵을 출력시켜 보자.

카카오 맵을 호출할 수 있는 JavaScript 코드와 카카오 맵을 호출받아 출력할 영역이 되는 div태그를 생성해준다.

const container = document.getElementById('map'); 
//지도를 담을 영역의 DOM 레퍼런스

const options = { 
//지도를 생성할 때 필요한 기본 옵션
    center: new kakao.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
    level: 3 //지도의 레벨(확대, 축소 정도)
};

new kakao.maps.Map(container, options); 
//지도 생성 및 객체 리턴

// return(
	// ...
		<div id='map' style={{ "width" : "500px", "height" : "400px" }}></div>
// )

container라는 상수 값에 'map'의 이름을 갖는 id tag를 가져와 담아준 후 new kakao.maps.Map이라는 카카오 스크립트의 기능을 이용해 container 상수에 담아놓은 id 태그에 지도를 생성해준다. 참고로 options 상수는 카카오 맵을 호출할 때의 초기값을 설정해주는 역할이다.

위 JavaScript 코드는 'map'이라는 id 태그에 지도를 생성해주는 역할을 하기 때문에 return 부분에도 'map'이라는 id를 가지는 div태그를 하나 생성해주어야 한다.

이 에러가 발생하는 이유는, 프론트엔드 서버에서 페이지가 그려지는 시점에는 document가 undefined한 값을 가지고 있기 때문이다. 이는 서버사이드 렌더링의 특징으로 document를 사용하는 시점을 document가 생성된 시점 이후로 변경해주면 해결된다.

또 글로벌 스코프에 위치한 카카오라는 객체의 타입은 다음과 같이 지정할 수 있다.

declare const window: typeof globalThis & {
  kakao: any;
};

3) 내 애플리케이션에 사이트 도메인 등록하기

내 서비스에서 카카오 스크립트가 동작하게 하기 위해서는 개발자페이지에서 사이트 도메인 등록을 해주어야 한다. 이런 식으로 제공되는 API들은 대부분 보안을 위해 해당 기능을 끌어다 사용할 도메인을 등록해야만 이용할 수 있는 구조로 되어 있다.

tip) JavaScript API Key와 같은 민감 정보는 github에 올려선 안된다. env를 이용환 환경변수 설정 등의 방법으로 최대한 숨겨놓고 절대 노출되면 안되는 중요 민감 정보의 경우에는 프론트 서버보다는 백에 놓고 사용하는 편이 권장된다.

4) 페이지 이동 후 지도 출력(SPA&CSR)

버튼을 눌러 페이지 이동 시 이동된 페이지에서 지도가 보이지 않고 에러를 뱉는다. 그러나 해당 주소를 주소창에 입력해서 접속하면 다시 지도를 보여준다. 이 부분은 SPA 와 CSR에 대한 이해가 필요하다.

import { useRouter } from "next/router";

export default function KakaoMapRoutingPage() {
  const router = useRouter();
  const onClickMoveToMap = () => {
    router.push("/29-03-kakao-map-routed");
  };

  return (
    <div>
      <button onClick={onClickMoveToMap}>맵으로 이동하기 !</button>
    </div>
  );
}

간단한 페이지를 만들어 버튼을 누르면 지도 스크립트가 입력된 페이지로 이동하도록 만든다.

서버 실행 후 버튼을 클릭해 지도가 있는 페이지로 이동하면..

에러가 뜬다.
router를 사용하지 않고 a 태그를 이용해 페이지를 이동해보자.

import { useRouter } from "next/router";

export default function KakaoMapRoutingPage() {
  // const router = useRouter();
  // const onClickMoveToMap = () => {
  //   router.push("/29-03-kakao-map-routed");
  // };

  return (
    <div>
      {/* <button onClick={onClickMoveToMap}>맵으로 이동하기 !</button> */}
      <a href="/29-03-kakao-map-routed">맵으로 이동하기 !!</a>
    </div>
  );
}

문제 없이 출력된다!

router와 a의 다른 점을 알아보자.

a태그를 이용해 페이지를 이동하는 방식의 웹 서비스를 MPA(Multi Page Application)이라고 하며 MPA에서 서로 다른 url을 가진 페이지들은 각각 독립적으로 존재하기 때문에 프론트 서버에서 페이지를 그린 뒤 브라우저로 HTML/CSS/JS를 보내주는 작업을 매 페이지 이동 시마다 거치게 된다.

이 경우 주소를 직접 입력해 들어가는 것과 a태그를 통해 페이지를 이동하는 것은 본질적으로 같다. 하지만 MPA의 경우 페이지 이동 시마다 서버에 요청해 데이터를 받아와야 하기 때문에 성능이 좋지는 않다.

반면 router를 이용하는 경우를 SPA(Single Page App)라고 한다.

SPA에서는 서비스에 처음 접속할 때 모든 페이지의 데이터를 다 받아온다. 그리고 라우터를 통해 페이지를 이동할 때 실제로는 페이지의 일부에 해당하는 컴포넌트만 교체한 뒤 페이지를 다시 그려온다.(리렌더) SPA의 경우 최초 로딩에는 다소 시간이 걸릴 수 있으나 이동 시에는 MPA에 비해 시간이 압도적으로 덜 걸린다.

MPA는 전통적 의미의 홈페이지, SPA는 홈페이지보다는 어플리케이션의 느낌이 강하다!

이를 바탕으로 정리해보면, a태그를 이요하면 오류는 해결되나 페이지 이동 시 페이지 자체가 새로 로딩되기 때문에 SPA프레임워크인 Next.js를 사용하는 의믹 없어진다. 그럼 어떻게 해야 할까?

Next.js에서 제공하는 Link태그를 이용하면 된다.

참조: https://nextjs.org/docs/api-reference/next/link

router로 연결해둔 기존 코드를 다음과 같이 변경한다.

import { useRouter } from "next/router";
import Link from "next/link";

export default function KakaoMapRoutingPage() {
  // const router = useRouter();
  // const onClickMoveToMap = () => {
  //   router.push("/29-03-kakao-map-routed");
  // };

  return (
    <div>
      {/* <button onClick={onClickMoveToMap}>맵으로 이동하기 !</button> */}
      <Link href="/29-03-kakao-map-routed">
        <a>맵으로 이동하기 !!</a>
      </Link>
    </div>
  );
}

Link 안에 a 태그를 넣으면 시멘틱 요소를 가지고 있는 Html태그로 렌더링이 되기 때문에 웹 표준이나 검색 엔진 최적화 차원에서도 이점을 가지고 있다. 따라서 가능한 Link태그를 사용하는 것이 좋다.

script 태그의 비동기 작동 이슈 해결

script 다운로드와 지도 로드 작업이 동시에 진행되어 지도가 정상적으로 출력되지 않는 문제 방지를 위해서는...
스크립트 다운로드 오나료 후 카카오 맵 로드도 완료된 후 지도를 불러오도록 코드를 작성하면 된다.

useEffect(() => {
  // 여기서 직접 다운로드 받고, 다 받을때까지 기다렸다가 그려주기!!
  const script = document.createElement("script"); // html에 script라는 태그(Element)를 만든다.
  script.src =
    "//dapi.kakao.com/v2/maps/sdk.js?appkey='JavaScript API Key'&autoload=false";
  document.head.appendChild(script);

  script.onload = () => {
		window.kakao.maps.load(function () {
			const container = document.getElementById("map");
      const options = {
        center: new window.kakao.maps.LatLng(33.450701, 126.570667),
        level: 3,
      };
      const map = new window.kakao.maps.Map(container, options);
		}
	}
}

카카오 맵 커스텀

참고 https://apis.map.kakao.com/web/sample/

카카오 맵 초기 위치 설정

참고 https://apis.map.kakao.com/web/sample/basicMap/

해당 좌표를 바꿔주면 된다.

options라는 이름으로 선언된 객체 안에 center라는 키 값으로 주소값이 지정된 부분의 첫번째 인자는 초기 위치에 대한 경도값, 두번째 인자는 위도값이다.

설정된 경도, 위도값에 따라 초기 위치를 변경할 수 있다.

지도에 마커를 추가하려면
참고 https://apis.map.kakao.com/web/sample/basicMarker/

추가할 마커의 초기 위치 값을 먼저 잡아준다.

const markerPosition  = new kakao.maps.LatLng(37.4848929702844, 126.89537799629241);

이후 카카오 맵을 호출해주는 useEffect 함수 안으로 추가한다.

초기 위치 값을 설정했다면 화면에 출력할 마커의 정보를 설정한다.

const marker = new kakao.maps.Marker({
position: markerPosition
});

마커 생성 시, 옵션으로 받는 position에 마커의 위치값을 넣어 주면 설정된 초기 위치 값에 마커를 생성하게 된다.

설정한 마커를 화면에 출력시키려면..

marker.setMap(map);

생성된 마커의 .setMap 메소드를 이용하는데 어떠한 맵에 해당 마커를 생성할 건지에 대한 지도 정보를 넣어줄 필요가 있다. 그렇다면 지도에 대한 정보는 어디서 받아올까?

kakao.maps.Map을 이용해 지도를 담을 영역인 container와 초기 위치값을 설정하는 options로 카카오 맵을 화면에 출력했었는데 이 정보 값을 map이라는 상수에 할당하고 생성할 마커에게 이 지도에 마크를 생성해 달라는 의미로 map 상수를 setMap 메소드의 인자값으로 넣어줄 수 있다!

refetch의 문제점과 개선 방법

refetch의 문제점을 알아보자. useQuery()는 수행 후 cache-state에 저장되는데 refetch를 사용하게 되면 새롭게 다시 받아오기 때문에 비효율적인 구조가 된다. 따라서 apollo-cache-state를 직접 업데이트 하는 방법이 권장된다.

refetchqueries가 적용된 코드

import { useQuery, gql, useMutation } from "@apollo/client";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

// 캐시에 저장되는 데이터와 요청 후 받아오는 값이 일치되어야 합니다.
const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
      writer
      title
      contents
    }
  }
`;

const DELETE_BOARD = gql`
  mutation deleteBoard($boardId: ID!) {
    deleteBoard(boardId: $boardId)
  }
`;

export default function StaticRoutedPage() {
  const { data } = useQuery<Pick<IQuery, "fetchBoards">, IQueryFetchBoardsArgs>(
    FETCH_BOARDS
  );
  const [deleteBoard] = useMutation(DELETE_BOARD);
  const [createBoard] = useMutation(CREATE_BOARD);

//삭제 함수
  const onClickDelete = (boardId: string) => () => {
    void deleteBoard({
      variables: { boardId },
      refetchQueries: [{ query: FETCH_BOARDS }]
    });
  };

//등록 함수
  const onClickCreate = () => {
    void createBoard({
      variables: {
        createBoardInput: {
          writer: "영희",
          password: "1234",
          title: "제목입니다~~",
          contents: "내용입니다@@@",
        },
      },
      refetchQueries: [{ query: FETCH_BOARDS }],
    });
  };

  return (
    <>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.writer}</span>
          <span style={{ margin: "10px" }}>{el.title}</span>
          <button onClick={onClickDelete(el._id)}>삭제하기</button>
        </div>
      ))}
      <button onClick={onClickCreate}>등록하기</button>
    </>
  );
}

mutation 요청 후 refetch를 해줬다. 그러나 이를 이용하면 api요청이 한번 더 일어난다.

그럼 어디에 쓰나? 작은 서비스에서 쓴다!(코드의 가독성 면에서 좋음)

cache-state 업데이트 해보기

import { useQuery, gql, useMutation } from "@apollo/client";
import { MouseEvent } from "react";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

// 캐시에 저장되는 데이터와 요청 후 받아오는 값이 일치되어야 합니다.
const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
      writer
      title
      contents
    }
  }
`;

const DELETE_BOARD = gql`
  mutation deleteBoard($boardId: ID!) {
    deleteBoard(boardId: $boardId)
  }
`;

export default function StaticRoutedPage() {
  const { data } = useQuery<Pick<IQuery, "fetchBoards">, IQueryFetchBoardsArgs>(
    FETCH_BOARDS
  );
  const [deleteBoard] = useMutation(DELETE_BOARD);
  const [createBoard] = useMutation(CREATE_BOARD);

//삭제 함수
  const onClickDelete = (boardId: string) => () => {
    void deleteBoard({
      variables: { boardId },
      update(cache, { data }) {
				// 캐시를 수정한다는 뜻의 cache.modify
        cache.modify({
				// 캐시에있는 어떤 필드를 수정할 것 인지 key-value 형태로 적어줍니다.
          fields: {
            fetchBoards: (prev, { readField }) => {
              const deletedId = data.deleteBoard; // 삭제된ID
              const filteredPrev = prev.filter(
                (el) => readField("_id", el) !== deletedId // el._id가 안되므로, readField를 사용해서 꺼내오기
              );
              return [...filteredPrev]; // 삭제된ID를 제외한 나머지 9개만 리턴
            },
          },
        });
      },
    });
  };

//등록 함수
  const onClickCreate = () => {
    void createBoard({
      variables: {
        createBoardInput: {
          writer: "영희",
          password: "1234",
          title: "제목입니다~~",
          contents: "내용입니다@@@",
        },
      },
      update(cache, { data }) {
				// 캐시를 수정한다는 뜻의 cache.modify
        cache.modify({
				// 캐시에있는 어떤 필드를 수정할 것 인지 key-value 형태로 적어줍니다.
          fields: {
            fetchBoards: (prev) => {
              return [data.createBoard, ...prev];
            },
          },
        });
      },
    });
  };

  return (
    <>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.writer}</span>
          <span style={{ margin: "10px" }}>{el.title}</span>
          <button onClick={onClickDelete(el._id)}>삭제하기</button>
        </div>
      ))}
      <button onClick={onClickCreate}>등록하기</button>
    </>
  );
}

캐시의 직접 수정을 위해서는 update(){}라는 기능을 이용한다. update 기능을 사용할 때 파라미터로 데리고 온 cache는 apollo-cache-state에 있는 global state다. 그리고 업데이트된 결과는 파라미터의 {data} 안에 들어오게 된다.

참고) fields의 prev와 return 값
1) fields는 캐시에 있는 어떤 데이터를 수정할 것인지 알려줌
2) prev는 이전 데이터 불러옴
3) return 이전 데이터를 리턴값으로 바꿔줌. 업데이트 결과가 {data}이므로 결국 데이터 내부의 값이 됨

profile
Strive for greatness

0개의 댓글