[기능] Next.js App router 기반 카카오맵 기능 구현

짜장킴·2025년 10월 15일

프로젝트

목록 보기
33/38

0. 개발자 키 받기 & 환경변수 설정

  • 카카오 개발자 센터 접속
  • 내 애플리케이션 → 새 앱 만들기
  • 플랫폼 등록에서 “웹” 추가 (도메인: 개발 중이면 http://localhost:3000)
  • 앱 키에서 JavaScript 키 복사
  • 프로젝트에 .env.local 생성:
NEXT_PUBLIC_KAKAO_MAP_KEY=여기에_자바스크립트키_붙여넣기

NEXTPUBLIC로 시작해야 클라이언트에서 접근 가능

1. Kakao Maps SDK를 Next.js에서 한번만 로드하기

  • Next는 public/index.html이 없어서 next/script를 이용해 SDK를 클라이언트에서 한 번만 로드해야 함
// /components/KakaoScript.tsx
"use client";

import Script from "next/script";

export default function KakaoScript() {
  const appKey = process.env.NEXT_PUBLIC_KAKAO_MAP_KEY;

  return (
    <Script
      id="kakao-maps-sdk"
      src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${appKey}&autoload=false&libraries=services`}
      strategy="afterInteractive"
      onLoad={() => {
        if (window.kakao && window.kakao.maps) {
          window.kakao.maps.load(() => {
            window.dispatchEvent(new Event("kakao:loaded"));
          });
        }
      }}
    />
  );
}
  • id : Next.js가 이 스크립트를 구분할 수 있도록 고유 ID를 붙임.
  • src : 카카오맵 SDK URL
    - autoload=false : 자동 실행 막고, onLoad에서 수동으로 kakao.maps.load() 호출
    - libraries=services : 주소 → 좌표 변환(Geocoder) 기능 사용
    - strategy="afterInteractive" : 페이지가 인터랙티브해진 뒤(클라이언트에서 준비된 후) 이 스크립트를 로드
  • 로드 완료 시 → window.dispatchEvent(new Event("kakao:loaded")) 발생
    → 다른 컴포넌트에서 “SDK 준비 완료” 이벤트를 받을 수 있음

2. 타입 설정

// types/global.d.ts
declare global {
  interface Window {
    kakao: any;
  }
}
  • tsconfig.json의 include에 types/*/.d.ts 추가
{
  "compilerOptions": {
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"]
}
  • 카카오맵 타입 설치(@types/kakao.maps.d)
npm install --save-dev @types/kakao.maps.d

3. 루트 레이아웃에 SDK 연결

// app/layout.tsx 
import KakaoScript from "@/components/KakaoScript";
// ...생략

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={pretendard.variable}>
      <body className="font-sans">
        <KakaoScript />
        <AuthBootstrap />
        <Providers>{children}</Providers>
        <ToastProvider />
      </body>
    </html>
  );
}

4. 재사용 가능한 지도 컴포넌트 만들기

  • 카카오 지도 SDK를 이용해 특정 주소(address)를 지도 위에 표시
  • 그 위치에 마커 띄우는 React 클라이언트 컴포넌트
// /components/map/KakaoMap.tsx
"use client";

import { useEffect, useRef } from "react";
import { toast } from "react-toastify";

type KakaoMapProps = {
  address: string;
  className?: string;
  level?: number;
};

export function KakaoMap({ address, className, level = 3 }: KakaoMapProps) {
  const mapRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!address) return;

    let cleanup = () => {};

    const init = () => {
      if (!mapRef.current || !window.kakao?.maps) return;

      const { maps } = window.kakao;
      const map = new maps.Map(mapRef.current, {
        center: new maps.LatLng(37.5665, 126.978), // 기본: 서울시청
        level,
      });

		// 주소 -> 좌표 변환
      const geocoder = new maps.services.Geocoder();
      /* eslint-disable @typescript-eslint/no-explicit-any */
      geocoder.addressSearch(address, (result: any[], status: string) => {
        if (status === maps.services.Status.OK && result[0]) {
          const { x, y } = result[0];
          const coords = new maps.LatLng(Number(y), Number(x));

          new maps.Marker({ position: coords, map });

          map.setCenter(coords);
        } else {
          if (process.env.NODE_ENV !== "production") {
            toast.error("[KakaoMap] 주소 검색 실패:");
          }
        }
      });
    };

	// SDK가 이미 로드된 경우 → 바로 init()
    if (window.kakao?.maps) {
      init();
    } else {
    // 아직 로드되지 않았다면 → "kakao:loaded" 이벤트 대기
      const onLoaded = () => init();
      window.addEventListener("kakao:loaded", onLoaded, { once: true });
      cleanup = () => window.removeEventListener("kakao:loaded", onLoaded);
    }

    return () => cleanup();
  }, [address, level]);

  return <div ref={mapRef} className={className} aria-label="카카오 지도" />;
}
  1. mapRef로 지도 DOM 참조
    • div 안에 실제 지도 인스턴스가 들어감
  2. SDK 로드 여부 확인
    • 이미 로드됨 → init() 바로 실행
    • 아직이면 → "kakao:loaded" 이벤트 기다림
  3. init() 실행 시
    • new kakao.maps.Map()으로 지도 생성
    • Geocoder로 주소 → 위도·경도 변환
    • 변환된 좌표로 Marker 표시 및 지도 중심 이동
  4. 클린업
    • 컴포넌트 언마운트 시 이벤트 리스너 제거

5. 실제 페이지에 지도 삽입하기

import Image from "next/image";

// Icons
import LocationIcon from "@/assets/svgs/location_icon.svg";
import { GetActivityDetailResponse } from "@/lib/types/activities";
import { KakaoMap } from "./KakaoMap";

type ActivityDescriptionProps = {
  activity: GetActivityDetailResponse;
};

export function ActivityDescription({ activity }: ActivityDescriptionProps) {
  return (
    <article
      className="flex flex-col gap-y-[30px]"
      aria-labelledby="activity-description"
    >
      <hr className="border-nomadBlack/20" />

      <section className="flex flex-col gap-y-[16px]">
        <h3 className="text-xl font-bold text-nomadBlack">체험 설명</h3>
        <p className="text-lg text-nomadBlack">{activity.description}</p>
      </section>

      <hr className="border-nomadBlack/20" />

      <section className="flex flex-col gap-y-[8px]">
        <KakaoMap
          address={activity.address}
          className="aspect-[5/2] w-full rounded-[16px] border border-gray-200"
        />

        <div className="flex gap-x-[2px]">
          <Image src={LocationIcon} alt="위치" width={18} height={18} />
          <span className="text-lg text-nomadBlack">{activity.address}</span>
        </div>
      </section>
      <hr className="border-nomadBlack/20" />
    </article>
  );
}

주의해야할 점!!

  • Kakao Developers → “지도(Map)” API 활성화 필수
  • 도메인 등록 시 http://localhost:3000 또는 실제 배포 도메인 반드시 추가
profile
프론트엔드 취준생입니다.

0개의 댓글