카카오 개발자 페이지에 접속!
카카오 맵 뿐만 아니라 다양한 API가 적용된다. 로그인 후 애플리케이션 추가(프로젝트 폴더 생성과 비슷)
애플리케이션 생성된거 클릭해보면 해당 애플리케이션의 앱 키 목록을 볼 수 있습니다.
그 중 자바스크립트 키 이용하기!!
[지도를 띄우는 코드 작성] 부분에서 카카오 맵을 설저아는 가이드라인을 찾을 수 있습니다.
❗️ 위의 가이드 라인은 바닐라 (Original) JavaScript와 HTML 기준으로 만들어진 코드입니다.
React 에서, 특히 Next JS
에서는 HTML에 접근할 수 있는 방법이 한정적
이기 때문에
위의 가이드라인 만으로는 구현할 수 없습니다.
무엇보다 가이드라인에서 사용하고 있는 document 객체는 서버사이드 렌더링을 지원하는 Next.js 에서는
화면을 렌더하기 전까지 생성되지 않은 값 (=undefiend)
을 가집니다.
그렇기 때문에 가이드라인의 코드를 그대로 입력하면 오류가 발생하게 됩니다.
먼저 설정해야 되는 부분이 있습니다.
바로 카카오 맵에서 제공하는 스크립트를 내 프로젝트 안에 적용
시켜줘야 합니다.
아래 코드를 프로젝트 최상위에 호출해줍시다.
import Head from 'next/head';
Head 컴포넌트는 Next.js 에서 기본적으로 제공하는 기능입니다.
HTML 에서 Head 태그 안으로 다른 기능들을 호출해서 사용할 수 있는 것처럼 Next.js 의Head 컴포넌트를 이용해 동일한 기능을 사용
할 수 있습니다.Head 컴포넌트를 불러오셨다면,
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> </> )
카카오 맵을 출력해줄 수 있는 영역이되는 div 태그와, 호출할 수 있는 JS 코드를 만들어줍니다.
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 태그를 가져와 담아준 후,
new kakao.maps.Map
이라는 카카오 스크립트의 기능을 이용해container 상수에 담아놓은 id 태그에 지도를 생성
해줍니다.
options
상수는 카카오 맵을 호출할 때의 초기값을 설정해주는 역할을 합니다. 위 JavaScript 코드는 'map' 이라는 id 태그에 지도를 생성해주는 역할을 하므로,return 부분에도 'map' 이라는 id를 가지는 div 태그도 하나 생성해주어야 합니다.
document가 정의되지 않았다는 에러가 발생할텐데, 프론트엔드 서버에서 페이지가 그려지는 시점에는 document 가 undefined 한 값
을 가지고 있기 때문입니다. 이것은 서버사이드 렌더링의 특징 중 하나 입니다.
💡
useEffect
를 이용해서 document를 사용하는 시점을 document가 생성된 시점 이후로 변경해줍시다!!
글로벌 스코프에 위치한 kakao 라는 객체의 타입은 다음과 같이 지정할 수 있습니다.declare const window: typeof globalThis & { kakao: any; };
내 서비스에서 카카오 스크립트가 동작하게 하기 위해서는 개발자페이지에서 사이트의 도메인 등럭을 해야합니다. 이런 식으로 제공되는 API들은 대부분 보안을 위해 해당 기능을 끌어다 사용할 도메인을 등록해야만 이용할 수 있는 구조로 되어있습니다.
키 발급 > 내 애플리케이션 > 플랫폼 페이지에서 Web 사이트 도메인
을 등록하면 됩니다.
여기까지 완료했으면 가이드라인에서 지시하는 카카오 맵 출력 방법은 전부 따라한 것입니다.
이제 페이지를 실행시켜보고, 지도가 잘 나오는지 확인해봅시다!!
신기하게도 카카오 지도는 버튼을 눌러 페이지이동을 하면 이동된 페이지에서 지도가 보이지 않고 에러를 뱉습니다.
하지만 해당 주소를 주소창에 입력해서 접속을 하게되면 언제 그랬냐는 듯 다시 지도를 보여줍니다.
먼저 페이지를 새로 만들고, 맵으로 이동하기 버튼을 만들어줍니다.
그리고 버튼을 누르면 지도 스크립트가 입력되어있는 페이지로 이동하도록 함수를 연결해줍니다.
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>
);
}
서버를 실행한 뒤,
버튼을 클릭해서 지도가 있는 페이지로 이동해봅시다.
kakao를 찾을 수 없다며 에러가 뜹니다.
이번에는 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 Application)
라고 합니다.
SPA에서는 서비스에 처음 접속할 때 모든 페이지의 데이터를 다 받아옵니다.
그리고 router를 통해 페이지를 이동할 때, 실제로는 페이지의 일부에 해당하는 컴포넌트만 교체한 뒤 페이지를 다시 그려오게 됩니다. (re-rendering)
SPA의 경우 최초 로딩에는 시간이 다소 걸릴 수 있으나 페이지를 이동할 때 걸리는 시간이 MPA에 비하여 압도적으로 짧습니다.
MPA는
전통적인 의미의 홈페이지
이고,SPA는 홈페이지보다는
애플리케이션
의 느낌이 강합니다.
그렇다면 a태그를 이용하면 오류는 해결되지만, 페이지를 이동했을 때 페이지 자체가 새로 로딩되기때문에 SPA 프레임워크인 Next.js를 사용하는 의미가 없어집니다.
해결하기 위해, 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 태그를 사용하는 것이 좋습니다.
💡 시맨틱 태그란, 그 자체만으로도 의미를 담고 있는 html 태그입니다.
많이 사용되는 시맨틱 태그에는 header, footer, nav, section, aside 등이 있습니다.
Head 태그 안에 넣어 놓은 script 태그로 카카오 맵 script를 불러올 경우 script 다운로드와 지도를 로드하는 작업이 동시로 진행되기 때문에, 사용자 PC의 다운로드 속도가 느릴 경우 지도가 정상적으로 보이지 않는 문제가 발생할 수 있습니다.
이러한 경우에는 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);
}
}
}
카카오 맵도 원하는 기능을 추가해서 커스텀할 수 있습니다.
카카오 맵 페이지를 실행시키면 카카오 본사의 위치가 초기위치로 잡혀있습니다.
options 라는 이름으로 선언된 객체 안에
center
라는 키 값으로 주소값이 지정되어 있는 부분이 있습니다.
첫번째 인자는 초기 위치에 대한 경도값
, 두번째 인자는 초기 위치에 대한 위도값
을 받아옵니다.
설정된 경도와 위도값에 따라서 처음 카카오 맵을 호출할 때 초기 위치 값을 변경
시킬 수 있습니다.
💡 위치값 확인은 구글지도에서 해당 장소 오른쪽 클릭 후 확인 가능!!
지도에서 표시를 고정하고 싶을 때는 마커를 이용합니다. 우리가 추가했던 카카오 맵에서도 마커 기능을 제공하고 있습니다. 마커생성하기
const marker = new kakao.maps.Marker({
position: markerPosition
});
마커를 생성할 때, 옵션으로 받는 position 에 마커의 위치값
을 넣어주면 설정된 초기 위치 값에 마커를 생성하게 됩니다.
marker.setMap(map);
생성된 마커의 .setMap
메소드를 이용하는데 어떠한 맵에 해당 마커를 생성할 건지에 대한 지도 정보를 넣어줘야 합니다. 그렇다면 지도에 대한 정보는 어디서 받아올 수 있을까요?
이미 우리는 앞의 과정에서 만들어 놓은 게 있습니다.
kakao.maps.Map
을 이용해서 지도를 담을 영역인 container 와 초기 위치값을 설정하는 options 로 카카오 맵을 화면에 출력했던 걸 기억하시나요?
이 정보 값을 map 이라는 상수에 할당 하고생성할 마커에게 '이 지도에 마커를 생성해 줘'
라는 의미로 map 상수를 setMap 메소드의 인자값으로 넣어줄 수 있습니다.
전체코드!
import { useEffect } from "react";
import Head from "next/head";
declare const window: typeof globalThis & {
kakao: any;
};
export default function KakaoMap(): JSX.Element {
useEffect(() => {
const script = document.createElement("script");
script.src =
"//dapi.kakao.com/v2/maps/sdk.js?autoload=false&appkey=a36053254ce187748fe8cd545456ef97";
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.264685, 127.047441), //지도의 중심좌표.
level: 3, //지도의 레벨(확대, 축소 정도)
};
const map = new window.kakao.maps.Map(container, options); //지도 생성 및 객체 리턴
// 마커가 표시될 위치입니다
const markerPosition = new window.kakao.maps.LatLng(
37.264685,
127.047441
);
// 마커를 생성합니다
const marker = new window.kakao.maps.Marker({
position: markerPosition,
});
// 마커가 지도 위에 표시되도록 설정합니다
marker.setMap(map);
});
};
}, []);
return (
<>
<Head>
{/* <script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=a36053254ce187748fe8cd545456ef97"
></script> */}
</Head>
<div id="map" style={{ width: 500, height: 300 }}></div>
</>
);
}
우리는 등록, 삭제 이후 refetch를 통해 목록을 업데이트 했습니다. 그렇지만 useQuery()는 수행 후 cache-state에 저장되는데 refetch를 사용하게 되면 새롭게 다시 받아오기때문에 비효율 적인 구조가 되기 때문에 좋은 방법은 아닙니다.
이제는 refetch를 하지 않고, apollo-cache-state를 직접 업데이트하는 방법을 사용해봅시다.
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을 요청한 이후 업데이트 할때는 위와 같이 refetchQueries를 해줬습니다.
하지만 refetchQueries를 이용하면 api요청이 한번 더 일어나게 되므로 우리는 더이상 refetchQueries를 사용하지 않겠습니다!
💡 하지만!
작은 서비스에서는 오히려 refetchQueries를 쓰시는게 좋습니다.
코드의 가독성 면에 있어서는 refetchQueries가 훨씬 깔끔하고 좋기 때문에 성능을 크게 따질 필요없는 작은 서비스에서는 오히려 refetchQueries를 쓰시는게 좋습니다.
하지만 규모가 커지게 되면 서버의 부하를 초래할 수 있으므로 그때는 cache를 업데이트 하시는게 좋습니다.
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값
- fields는 캐시에 있는 어떤 데이터를 수정할 것 인지를 알려줍니다.
- fields 의 prev : 이전데이터를 불러옵니다.
- fields의 return값 : 이전 데이터를 return 값으로 바꿔줍니다. 업데이트 된 결과가 {data} 이므로 결국 {data} 내부의 값이 됩니다.