[TIL] 카카오 맵 API / 태그 / refetch 개선

우기·2023년 4월 19일
1
post-thumbnail

📒 오늘 공부한 내용

🔍수업목차

[26-1] 카카오 맵 API (Kakao Map API)
[26-2] 태그 이용
[26-3] refetch의 문제점과 개선 방법

✅ 카카오 맵 API (Kakao Map API)


💡 구글 vs 네이버 vs 카카오

우리나라에서 가장 많이 사용하는 것은 구글, 네이버, 카카오에서 제공하는 지도 API다.
세 가지 API 간에는 제공하는 기능의 종류, 비용 문제 등의 차이가 있다.
차후 서비스를 직접 개발하게 되면, 이러한 차이점을 고려하여 어떤 API를 사용할 지 선택하면 된다.

📂 카카오 개발자 (Kakao Developers)

  • 구글, 네이버, 카카오 등의 대형 포털은 보통 개발자 사이트를 따로 가지고 있다.
  • 카카오 개발자 페이지
  • 카카오에서 제공하는 개발자 API를 사용하기 위해서는 애플리케이션 추가가 필요하다.

  • KaKao 지도 Web API

📂 카카오 맵 그리기

  • 프론트엔드 서버에서 페이지가 그려지는 시점에는 document 가 undefined 한 값을 가지고 있다.
  • 이것은 서버사이드 렌더링의 특징 중 하나다.
  • document 가 생성된 시점 이후로 변경해주는 코드가 필요하다.
  • useEffect : 페이지가 마운트되고 document 객체가 생성된 이후에 카카오맵을 호출할 수 있도록 변경해준다.
  • 글로벌 스코프에 위치한 kakao라는 객체의 타입 지정
    declare const window: typeof globalThis & {
      kakao: any;
    };
import { useEffect } from "react";

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

export default function KaKaoMapPage(): JSX.Element {
  useEffect(() => {
    const script = document.createElement("script");
    script.src = JavaScript 앱 키
    document.head.appendChild(script);

    script.onload = () => {
      window.kakao.maps.load(function () {
        const container = document.getElementById("map"); // 지도를 담을 영역의 DOM 레퍼런스
        const options = {
          // 지도를 생성할 때 필요한 기본 옵션
          center: new window.kakao.maps.LatLng(37.241547, 131.864797), // 지도의 중심좌표.
          level: 3, // 지도의 레벨(확대, 축소 정도)
        };

        const map = new window.kakao.maps.Map(container, options); // 지도 생성 및 객체 리턴
        console.log(map);

        const imageSrc = "/images/15-19-47.png", // 마커이미지의 주소입니다
          imageSize = new window.kakao.maps.Size(100, 100), // 마커이미지의 크기입니다
          imageOption = { offset: new window.kakao.maps.Point(27, 69) }; // 마커이미지의 옵션입니다. 마커의 좌표와 일치시킬 이미지 안에서의 좌표를 설정합니다.

        const markerImage = new window.kakao.maps.MarkerImage(
            imageSrc,
            imageSize,
            imageOption
          ),
          markerPosition = new window.kakao.maps.LatLng(37.241547, 131.864797); // 마커가 표시될 위치입니다

        // 마커를 생성합니다
        var marker = new window.kakao.maps.Marker({
          position: markerPosition,
          image: markerImage, // 마커이미지 설정
        });

        // 마커가 지도 위에 표시되도록 설정합니다
        marker.setMap(map);
      });
    };
  }, []);
  return (
    <>
      <div id="map" style={{ width: 1000, height: 900 }}></div>
    </>
  );
}

🎯 JavaScript API Key와 같은 민감 정보를 github에 올려도 될까?

  • 안 된다. env를 이용한 환경변수 설정 등의 방법 등으로 최대한 숨기고, 절대 노출되면 안되는 중요 민감 정보의 경우에는 프론트엔드 서버에 두지 말고 백엔드 서버에 놓고 사용하는 편이 안전하다.

✅ 태그 이용


📂 a 태그

  • a 태그를 이용해서 페이지를 이동하는 것과 같은 방식을 채택하는 웹 서비스를 MPA(Multi Page Application)라고 한다.
  • MPA에서 서로 다른 url을 가진 페이지들은 각각 독립적으로 존재한다.
  • 그렇기 때문에 프론트엔드 서버에서 페이지를 그린 뒤 브라우저로 HTML/CSS/JS를 보내주는 작업을 매 페이지 이동 시마다 거치게 된다.
  • 주소를 직접 입력해서 들어가는 것과 a태그를 통해 페이지를 이동하는 것은 본질적으로 동일하다.
  • 페이지 이동 시마다 서버에 요청해서 데이터를 받아와야 하기 때문에 성능은 좋지 않다.
  • MPA는 전통적인 의미의 홈페이지

📂 router

  • router를 이용해서 페이지를 이동하는 것과 같은 방식을 채택하는 웹 서비스를 SPA(Single Page Application)라고 한다.
  • SPA에서는 서비스에 처음 접속할 때 모든 페이지의 데이터를 다 받아온다.
  • SPA의 경우 최초 로딩에는 시간이 다소 걸릴 수 있으나 페이지를 이동할 때 걸리는 시간이 MPA에 비하여 압도적으로 짧다.
  • SPA는 홈페이지보다는 애플리케이션

📂 Next.js의 Link 태그

  • Next.js Link 태그

    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 태그를 사용하는 것이 좋다.

✅ refetch의 문제점과 개선 방법


  • 지금까지 등록/삭제 이후 refetch를 통해 목록을 업데이트 해왔다.
  • useQuery()는 수행 후 cache-state에 저장되는데 refetch를 사용하게 되면 새롭게 다시 받아오기때문에 비효율 적인 구조가 되기 때문에 좋은 방법은 아니다.

💡 efetchQueries 언제 쓰는걸까?

  • 작은 서비스에서는 오히려 refetchQueries를 쓰시는게 좋다.
  • 코드의 가독성 면에 있어서는 refetchQueries가 훨씬 깔끔하고 좋기 때문에 성능을 크게 따질 필요없는 작은 서비스에서는 오히려 refetchQueries를 쓰시는게 좋다.
  • 하지만 규모가 커지게 되면 서버의 부하를 초래할 수 있으므로 그때는 cache를 업데이트 하시는게 좋다.

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>
    </>
  );
}

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>
    </>
  );
}
profile
개발 블로그

0개의 댓글