26일차 카카오 지도 및 웹에디터

osdsoonhyun·2023년 3월 15일
0

코드캠프

목록 보기
18/22
  1. 지도 연동 -> Map
  2. 싱글 페이지 어플리케이션? 멀티 페이지 어플리케이션, -> SPA-CSR / MPA-SSR

카카오 지도

  • 지도 API의 종류는 크게 구글 지도, 네이버 지도, 카카오 지도 등이 있다. 이들간에는 비용적인 차이, 세부적인 기능 차이 등이 있다
  • 지도를 적용시키기 전 개발자 등록을 진행하고 애플리케이션을 추가하여 web플랫폼과 도메인을 등록해줘야 한다.

카카오 지도 연동하기
1. 지도 담을 영역 만들기
2. 실제 지도를 그리는 API 불러오기(라이브러리 다운 받기)
3. 코드 작성하기

Head 설정

  • 카카오 맵에서 제공하는 스크립트를 내 프로젝트 안에 적용시켜줘야 한다.
  • jsx에서 스크립트를 적용시키기 위해서 HTML의 Head 안에 넣어주어야 하는데, Next.js에서 제공하는 Head 컴포넌트를 사용하면 된다.
  • 미리 만들어둔 JavaScript 앱 키가 필요하다. script 태그의 src 속성 값 안의 appkey= 부분에 계정의 JavaScript App Key 값을 넣어주면 된다.
import Head from 'next/head';

export default function index() {
  return (
    <>
      <Head>
        <script 
          type="text/javascript" 
          src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY"
        ></script>
      </Head>
      <div id="map" style={{width:500, height:400}}></div> // 지도 담을 영역
    </>
  );
}

  • API를 로딩하는 스크립트 태그의 위치는 상관 없으나 반드시 실행 코드보다 먼저 선언되어야 한다.
  • 따라서 먼저 다운로드 받아놓고 렌더링이 되고 나서 실행되어야 하므로 (componentDidMount) useEffect를 이용해서 페이지가 마운트되고 document 객체가 생성된 이후에 카카오맵을 호출할 수 있도록 변경해준다.

카카오맵 구현시 문제 상황

window 타입지정 - declare 사용

  • window 안에 kakao type이 있다는 것을 명시해주어야 한다. (kakao 타입은 알지 못함)
  • 또한 글로벌 스코프에 위치한 kakao라는 객체의 타입은 다음과 같이 지정해줄 수 있습니다.
declare const window: typeof globalThis & {
  kakao: any;
};

appKey

  • 지도를 넣은 후 Github에 우리의 코드를 그대로 올리면 발급받은 appkey까지 다 그대로 올라가버린다.
  • env를 이용한 환경변수 설정 등의 방법 등으로 최대한 숨기고, 절대 노출되면 안되는 중요 민감 정보의 경우에는 프론트엔드 서버에 두지 말고 백엔드 서버에 놓고 사용하는 편이 안전하다.
  • 프론트엔드에서 사용되는 appkey는 숨길 수 없어 appKey 요청을 받아줄 도메인을 적어줘야 한다.
  • 즉, 우리가 지정한 도메인이 아닌 곳에서는 appkey가 작동하지 않도록 설정해주었다
  • 이후에 프로젝트 배포를 진행하고 도메인이 만들어지면, 카카오맵에도 해당 도메인을 적용해 주어야 한다.

페이지 이동

  • 브라우저에서 router.push로 이동할 때, 카카오 기능이 로딩되기도 전에 실행되어 문제가 발생한다.
  • 이를 해결하기 위해 SPA,CSR에 대한 이해가 필요하다.

SPA와 CSR에 대한 이해

  • 신기하게도 카카오 지도는 버튼을 눌러 페이지이동을 하면 이동된 페이지에서 지도가 보이지 않고 에러를 뱉는다. 하지만 해당 주소를 주소창에 입력해서 접속을 하게되면 언제 그랬냐는 듯 다시 지도를 보여준다.
    왜그럴까?

SPA와 MPA

  • 버튼을 눌러 이동하는 것은 이미 받아가지고 온 페이지에서 페이지 이동만 하기 때문에 자바스크립트 안에서 화면만 바꿔주는 것으로 서버에 추가적인 요청을 보내지 않아 훨씬 빠르다.

  • 이미 완성된 페이지를 받아가지고 왔다.SPA는 하나의 완성된 페이지 앱이다

  • 지금 문제는 버튼을 눌러 페이지 이동이 워낙 빠른데 카카오 맵 라이브러리를 받아오기 전에 그릴려고 시도하다가 에러가 발생한 것이다.

  • 그렇기 때문에 버튼으로 페이지 이동시 카카오 라이브러리를 받아오고 화면을 그려줌으로써 에러를 해결할 수 있다.

  • MPA(Multi-Page-Application) 옛날 페이지로 이것은 페이지 이동이 매우 느리다. 버튼 클릭할 때마다 새로고침이 되어 새롭게 페이지를 만들어 보내준다.

  • SPA(Single-Page-Application) Next.js로 만들고 있다.

  • 첫 랜딩시 frontend서버에서 페이지에 필요한 모든 정보를 넘겨받았고, 페이지 이동 시마다 Browser자체에서 페이지를 나타내거나 감추는 형태로 작동한다 (또한, 이러한 작동방식을 Client Side Rendering:CSR이라 한다..)

  • react, angular, vue는 SPA이다.

a태그 vs link태그 vs router.push 비교

a 태그 사용하여 페이지 이동시 CSR(Client-Side-Rendering)이 안되고 페이지 접속을 새로하여 새로운 html을 받아온 것이다.

버튼을 클릭하여 router.push를 하여 CSR이 되어 페이지 이동이 빨라진다.
a태그는 클릭하면 바로 이동이 되지만 문제는 SPA 상태에서 CSR이 안된다.
Link 태그는 a태그와 마찬가지로 클릭하면 페이지 이동이 CSR로 이뤄진다.

Link태그 vs router.push

  • a 태그를 이용하면 오류는 해결되지만, 페이지를 이동했을 때 페이지 자체가 새로 로딩되기 때문에 SPA 프레임워크인 Next.js를 사용하는 의미가 없어진다.

그렇다면 어떻게 해야 할까?

  • Next.js에서 제공하는 **Link 태그**를 이용하면 된다.
  • docs를 보면 link태그 안에 a태그를 달아서 쓰라고 나와있다.
  • Link 안에 a 태그를 넣으면 **시맨틱 요소**를 가지고 있는 html 태그로 렌더링이 되기 때문에 웹 표준이나 검색 엔진 최적화 차원에서도 이점이 있다.
  • 비록 a태그를 달아놨지만 Link 태그에 의해 움직이기 때문에 CSR이다. a태그는 작동은 하지 않고 검색엔진을 위해 보여주기 위함이다.
  • 이거 또한 Next.js 13버전 이후부터는 지원하지 않고 a태그 없이 Link 태그만 사용한다.

그럼 왜 Link 태그 대신 router.push를 사용할까?
1. 버튼을 클릭해서 이동하는게 아닌 경우
- ex) 게시글 등록 버튼 클릭 이후 게시글 등록 함수가 이뤄지고 페이지가 이동되어야 하는데 게시글 등록을 link 태그로 만들게 되면 함수 실행은 되지 않고 페이지 이동만 된다.
2. 페이지 이동시 추가 로직을 실행시켜야 하는 경우
- ex) 마지막 이동 페이지를 globalState에 저장해야 하는 경우

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

Script 위치 (+ Head 태그)에 따라 발생하는 성능 이슈

  • 현재는 페이지 이동할때 라이브러리를 다운받으려 하니 느려서 에러가 발생한다.
  • 따라서, kakao 기능이 로딩이 완료될 때까지 기다려주는 작업이 필요하다.

두 가지 해결책이 있다.
1. 모든 페이지에서 카카오를 받아올 수 있도록 app.tsx에서 script를 통해 kakao맵을 받아오는 방식
2. useEffect를 사용해 직접 다운로드받아 다 받을때까지 기다린 후 그려주는 방식이 있었습니다!

Script 위치를 app.tsx로

  • 모든 페이지에서 카카오맵을 다운로드 받도록 app.tsx 파일에 script가 있는 Head 태그를 둔다.
  • 전체적인 페이지를 느리게 하여 비효율적인 방법이다.

카카오지도 CSR 이슈 보완

  • 카카오맵 그리기 전에 script가 모두 다운로드 될 떄까지 기달렸다가 하는 것이다.
  • Head 안에 스크립트 태그가 들어가 있는데, 이 부분을 직접 만들어 컨트롤 한다.
  • return 부분에 선언해두면 조작할 수 없기 때문에 직접 만들어 변수에 할당하여 컨트롤 한다.
useEffect(()=>{
    const script = document.createElement('script'); // <script></script>
    script.src="//dapi.kakao.com/v2/maps/sdk.js?appkey=kkkeyy"
    document.head.appendChild(script);

    script.onload = () => {
      const container = document.getElementById('map'); // 지도를 담을 영역의 DOM 레퍼런스
      const options = { // 지도를 생성할 때 필요한 기본 옵션
        center: new window.kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표.
        level: 3 //지도의 레벨(확대, 축소 정도)
      };
      
      const map = new window.kakao.maps.Map(container, options); // 지도 생성 및 객체 리턴 
      console.log(map);
    }
  },[]);
  • createElement를 통해 script 태그를 직접 만들어 주고, document.head.appendChild(script)를 사용하여 head 태그에 만들어둔 script를 자식으로 추가한다.
  • 이후, script.onload 내부에서 window.kakao.maps.load()를 통해 카카오 맵 스크립트가 전부 다 받아진 후 화면을 그리도록 해준다.

  • 추가적으로 카카오 docs에 따라 실행해준다.
  • 카카오맵 script의 말미에 autoload=false를 붙여 자동으로 불러오는 기능을 꺼주어야 한다.

//전체코드
/ 26-02

카카오지도 마커 추가하기

refetch의 문제점과 개선 방법(writeQuery, readQuery, modify)

  • refetch의 장점은 쉽지만 모든 데이터를 다시 받아와야 하므로 성능적으로 좋지 않다.

  • useQuery에서 요청이 나갈 때 globalState(현재 사용중인 것은 apollo cache)에 있는지 확인을 하고(fetchPolish) 없으면 백엔드로 요청 하는 것을 cache-first라 하고 무조건 새로운 요청 받아오는 것은 network-only이다.그렇게 받아온 것은 apollo-cache에 먼저 저장된다.

  • recoilState를 변경하는 것과 마찬가지로 globalState인 apollo-cache-state를 직접 변경할 수 있다면 추가적인 api 요청(refetch)를 보내지 않고 globalState를 직접 바꾸어 수정할 수 있다.

  • cache 직접수정 : refetch 보다는 어렵고 fetchMore(무한스크롤)와 비슷한 맥락이다. 해야할게 많지만 성능은 더 좋다.

그렇다면 cache 직접 수정만 사용해야하나?

  • 그렇지 않다.서비스 규모에 따라 달라진다.
    서비스 규모가 작은데 cache 직접 수정하면 복잡하기만 하고 성능 개선에도 도움이 되지 않는다. 하지만 코드 길이가 짧기 때문에 유지보수가 용이해져 효율적이다.
    서비스 규모가 크면 refetch되는 것이 많아 api요청이 많이 날아가므로 백엔드에 서버 부하가 걸리는데 이때 cache를 직접 수정하여 성능 개선을 한다.
    규모 작을 때 - refetch , 규모 클 때 - cache 직접 수정

정리
게시물/상품 목록과 같은 많은 데이터를 담은 부분에 수정이나 삭제가 발생하여 refetch를 요청하면, 수정/삭제가 발생한 부분만이 아닌 전체 데이터를 refetch하기 때문에 불필요한 네트워크 비용을 소모한다는 단점이 있다.
이를 Apollo의 데이터가 저장되는 cacheState를 직접 수정하여 좀 더 효율적으로 만든다.

apollo-cache 업데이트로 목록 추가, 삭제하기

  • 매칭 시켜주기 위해 return 받는 값을 맞춰주어야 추가적인 refetch가 일어나지 않는다.

  • 추가적인 api 요청인 refetchQueries를 하지 않고 deleteBoard, createBoard에 대한 api 요청 결과 받아와서 이미 저장되어 있는 apollo-cache state(globalState)를 직접 수정한다.

  • update(cache,{data})에서 cache는 apollo-cache에 있는 값을 로 가지고 오는 것이고 삭제하고 받은 결과값은 {data}로 들어온다. 가지고온 cache를 수정해준다.

  • update(cache,{data}){}를 통해, 기존 데이터가 저장되어있는 cache를 수정한다.(삭제와 등록 로직은 다르다.)

  • 데이터를 추가 또는 삭제 시 update함수의 바디에서 cache.modify({})를 사용해 cache에 있는 어떤 필드를 수정할지 정한다.

  • 캐시를 직접 수정하기 위해서는 update(){} 라는 기능을 이용하게 된다.

  • update 기능을 사용할때 파라미터로 데리고 온 cacheapollo-cache-state에 있는 global state 이다. 그리고 업데이트 된 결과 는 파리미터의 {data} 에 들어오게 된다.

fields의 prev와 return값

  • fields는 캐시에 있는 어떤 데이터를 수정할 것 인지를 알려준다.
  • fields 의 prev : 이전데이터를 불러온다.
  • fields의 return값 : 이전 데이터를 return 값으로 바꿔준다. 업데이트 된 결과가 {data} 이므로 결국 {data} 내부의 값이 된다.

삭제

  • prev 안에는 객체가 하나하나 들어있지는 않다. 객체를 꺼낼 수 있는 주소가 들어가 있다. docs에 객체에서 key값을 읽고 싶을 때에는 {readField}를 사용하라고 한다 그렇게 _idel._id로 꺼내지 말고 readFiled('_id',el)로 꺼내온다.
  • 삭제된 id를 받아와 기존에서 삭제된 id를 제외한 나머지 id를 반환한다.
...
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개만 리턴
  },
},
...

등록

  • update(cache,{data}){}를 통해, 기존 데이터가 저장되어있는 cache와 쿼리 실행 이후 받은 데이터가 담겨있는 data를 합쳐준다.
...
fields: {
  fetchBoards: (prev) => {
    return [data.createBoard, ...prev];
  },
},
...
  • 등록하기 버튼 누르게 되면 실제 백엔드에 요청은 들어가고 원래 백엔드에서 refetch를 하기 때문에 api가 두 번 요청이 되는데 위와 cache-state를 조작하여 create 한 번만 요청하고 받아온 값으로 cache-state를 변경하였다.

  • 유의미한 곳 : 무한스크롤 형태의 페이지네이션을 포함한 대부분

  • 무의미한 곳 : 게시판 (10개씩 끊어서 보여줘야하는 곳)

cache-state -> 최적화 방법

소소한 꿀팁

return style에서 중괄호 두 개 사용하는 이유


검색엔진

  • 네이버와 구글과 같이 검색 엔진을 가지고 있는 회사들을 프로그램을 만드는데, 페이지를 점수를 매기고 검색시 점수 높은것을 보여주는데 검색점수를 높이기 위한 검색엔진 최적화 SEO(SearchEngineOptimization)라 한다.
  • router.push 보다는 Link에 a태그를 사용하는 것이 검색 엔진 최적화에 유리하다.

MPA vs SPA

MPA(Multi Page Application)

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

SPA(Single Page Application)

  • router를 이용해서 페이지를 이동하는 것과 같은 방식의 웹서비스
  • SPA에서는 서비스에 처음 접속할 때 모든 페이지의 데이터를 다 받아온다.
  • 그리고 router를 통해 페이지를 이동할 때, 실제로는 페이지의 일부에 해당하는 컴포넌트만 교체한 뒤 페이지를 다시 그려온다. (re-rendering)
  • SPA의 경우 최초 로딩에는 시간이 다소 걸릴 수 있으나 페이지를 이동할 때 걸리는 시간이 MPA에 비하여 압도적으로 짧다.

MPA전통적인 의미의 홈페이지이고,
SPA는 홈페이지보다는 애플리케이션의 느낌이 강하다.

query string

  • 백엔드 api에 post 방식으로 writer='철수' 이런 식으로도 할 수 있지만, 주소 안에다가 key value를 포함해서 보낼 수도 있다. 이것을 쿼리스트링(query string)이라 한다.
  • 두 개 이상의 값이 있다면 &로 구분한다.
script.src="//dapi.kakao.com/v2/maps/sdk.js?autoload=false&appkey=bb4afdb0f945dc3fc4ed6038257441cb"
  • 이렇게하면 페이지 이동시 CSR의 빠른 페이지 이동시에도 kakao map을 받아올 수 있다.

게시글 삭제 시 id

  • 게시글 삭제시 어떤 게시글을 삭제할지 게시글의 id를 보내주어야 한다.
  • id를 가져오는 3가지 방법이 있다.
    1. button 태그에 id해서 document.getElement('id') 가져오는 방법
    2. event.target.id해서 가져오는 방법
    3. hof를 이용하여 el._id에서 직접 넘겨주는 방법

인자에 객체

  • 인자에 객체로 값을 넣어줌으로써 구조분해할당으로 데이터를 넣은 것이다.
  • {data}를 구조분해할당 하지 않고 넣었다면, 예를 들어 update(cache,datas) {const deletedId = datas.data.deletedBoard} 이렇게 써줬어야 한다.

0개의 댓글