๐Ÿ“์˜จํ•(on-fit) kakao map api๋ฅผ ํ™œ์šฉํ•œ location modal ์ œ์ž‘

์กฐ์ค€ํ˜•ยท2025๋…„ 11์›” 10์ผ

์˜จํ•

๋ชฉ๋ก ๋ณด๊ธฐ
4/16

๐Ÿ—บ๏ธ Next.js + Kakao Map API๋กœ ํ˜„์žฌ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋™๋„ค ์„ ํƒ ๊ธฐ๋Šฅ ๋งŒ๋“ค๊ธฐ

์ด๋ฒˆ ํŒ€ ํ”„๋กœ์ ํŠธ์—์„œ โ€œํ˜„์žฌ ์œ„์น˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ๋™๋„ค๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๊ธฐ๋Šฅโ€์ด ํ•„์š”ํ–ˆ๋‹ค.

๋งˆ์นจ ์ง€๋„ ๊ธฐ๋Šฅ์ด ์žˆ์œผ๋ฉด UX์ ์œผ๋กœ๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์„œ, ์นด์นด์˜ค๋งต API๋ฅผ ์ง์ ‘ ๋ถ™์—ฌ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ถ€ํ„ฐ LocationModal, ๊ทธ๋ฆฌ๊ณ  ์นด์นด์˜ค ์ง€๋„ API ์—ฐ๋™๊นŒ์ง€ ์ „์ฒด ๊ณผ์ •์„

์ฝ”๋“œ ํ•œ ์ค„ ํ•œ ์ค„ ๋œฏ์–ด๊ฐ€๋ฉด์„œ ์ •๋ฆฌํ•œ๋‹ค.


๐Ÿ” ๊ตฌํ˜„ ์ˆœ์„œ

  1. ๊ณตํ†ต ๋ชจ๋‹ฌ (Modal.tsx) โ†’ ์–ด๋””์„œ๋“  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋‹ฌ ๋ฒ ์ด์Šค
  2. LocationModal.tsx โ†’ โ€œ๋™๋„ค ์„ค์ •โ€์šฉ ๋ชจ๋‹ฌ UI
  3. Kakao Map API ์ ์šฉ โ†’ ์ง€๋„ ๋„์šฐ๊ธฐ + ํด๋ฆญ์œผ๋กœ ์ง€์—ญ๋ช…(์„œ์šธ์‹œ ๋™์ž‘๊ตฌ) ํ‘œ์‹œ

1) ๊ณตํ†ต ๋ชจ๋‹ฌ (Common Modal) โ€” createPortal + SSR ๊ฐ€๋“œ

์–ด๋””์„œ๋‚˜ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ.

ํ•ต์‹ฌ ํฌ์ธํŠธ: SSR์—์„œ ์•ˆ์ „, ํฌํ„ธ๋กœ body ์ตœ์ƒ๋‹จ์— ๋ Œ๋”, ๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ๋งŒ ๋ Œ๋”

์ฝ”๋“œ

// components/common/Modal.tsx
'use client'

import { ComponentPropsWithRef, ReactNode, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/lib/utils'
import { Card, CardContent, CardFooter, CardHeader } from './Card'

interface ModalProps extends ComponentPropsWithRef<'div'> {
  open: boolean
  onClose: () => void
  children?: ReactNode
  size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'
  closeOnBackdrop?: boolean
}

export function Modal({
  open,
  onClose,
  children,
  size = 'md',
  closeOnBackdrop = false,
  className,
  ...props
}: ModalProps) {
  // โœ… SSR ๊ฐ€๋“œ: ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ๋ Œ๋”(์„œ๋ฒ„์—์„  document๊ฐ€ ์—†์Œ)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // ์‚ฌ์ด์ฆˆ ํ”„๋ฆฌ์…‹
  const sizeClasses = {
    sm: 'max-w-sm',
    md: 'max-w-md',
    lg: 'max-w-lg',
    xl: 'max-w-xl',
    '2xl': 'max-w-2xl',
    '3xl': 'max-w-3xl',
    full: 'max-w-full mx-4',
  } as const

  // โœ… ๋ฐฑ๋“œ๋กญ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ (์˜ต์…˜)
  const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (closeOnBackdrop && e.target === e.currentTarget) onClose()
  }

  // โœ… ๋‹ซํ˜€ ์žˆ์„ ๋•Œ/SSR์ผ ๋•Œ๋Š” ๋ Œ๋”ํ•˜์ง€ ์•Š์Œ โ†’ ์ดˆ๊ธฐ ํ™”๋ฉด ํ๋ ค์ง ๋ฐฉ์ง€
  if (!mounted || !open) return null

  const modalContent = (
    <>
      {/* ๋ฐฑ๋“œ๋กญ(๋’ท๋ฐฐ๊ฒฝ) */}
      <divclassName="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
        onClick={handleBackdropClick}
      />
      {/* ์ค‘์•™ ์ •๋ ฌ ์ปจํ…Œ์ด๋„ˆ. ์—ฌ๊ธฐ "์•ˆ"์— Card(๋ณธ๋ฌธ)๊ฐ€ ์žˆ์–ด์•ผ ์ค‘์•™์ •๋ ฌ์ด ๋จน์Œ */}
      <div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
        <CardclassName={cn(
            'relative w-full mx-4 my-8 max-h-[90vh] pointer-events-auto',
            sizeClasses[size],
            className
          )}
          {...props}
        >
          {children}
        </Card>
      </div>
    </>
  )

  // โœ… body ์•„๋ž˜๋กœ ํฌํ„ธ ๋ Œ๋” โ†’ z-index/overflow ๊ฐ„์„ญ ์ตœ์†Œํ™”
  return createPortal(modalContent, document.body)
}

export { CardHeader as ModalHeader }
export { CardContent as ModalContent }
export { CardFooter as ModalFooter }

์™œ ์ด๋ ‡๊ฒŒ?

  • SSR ๊ฐ€๋“œ: Next.js(App Router)๋Š” ์„œ๋ฒ„์—์„œ ๋จผ์ € ๋ Œ๋” โ†’ document๊ฐ€ ์—†์œผ๋ฏ€๋กœ mounted ์ดํ›„์—๋งŒ ๋ Œ๋”.
  • Portal: ๋ชจ๋‹ฌ์€ ๋Œ€๊ฐœ ์ตœ์ƒ๋‹จ(z-50 ์ด์ƒ)์œผ๋กœ ์˜ฌ๋ผ์™€์•ผ ํ•˜๊ณ , ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ๊ณผ ๋…๋ฆฝ๋˜์–ด์•ผ ํ•จ.
  • ๋‹ซํž ๋• ๋ Œ๋” X: open={false} ๋•Œ ๋ฐฑ๋“œ๋กญ/๋ชจ๋‹ฌ DOM ์ž์ฒด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋„๋ก ํ•ด์„œ, ์ดˆ๊ธฐ ํ™”๋ฉด์ด ํ๋ ค์ง€๋Š” ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ ์ฐจ๋‹จ.

2) LocationModal โ€” ๊ณตํ†ต ๋ชจ๋‹ฌ์— โ€œ๋™๋„ค ์„ค์ •โ€ UI ์–น๊ธฐ

๊ณตํ†ต ๋ชจ๋‹ฌ์€ โ€œ๊ทธ๋ฆ‡โ€, LocationModal์€ โ€œ๋‚ด์šฉ๋ฌผโ€.

์—ฌ๊ธฐ์„œ๋Š” ํ—ค๋” + Kakao ์ง€๋„ ์„ ํƒ๊ธฐ(LocationPicker)๋ฅผ ์กฐ๋ฆฝํ•˜๊ณ , env ํ‚ค๋ฅผ ํ•˜์œ„๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ

// components/header/LocationModal.tsx
'use client'

import { MapPinIcon } from 'lucide-react'
import { Modal, ModalHeader, ModalContent } from '@/components/common/Modal'
import LocationPicker from '@/components/location/LocationPicker'

type Props = {
  open: boolean
  onClose: () => void
  onSelect?: (region: string, lat: number, lng: number) => void
}

export default function LocationModal({ open, onClose, onSelect }: Props) {
  // โœ… env์—์„œ ์นด์นด์˜ค JS ํ‚ค ์ฝ์–ด์„œ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์— ๋„˜๊น€
  const appKey = process.env.NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY as string

  return (
    <Modal open={open} onClose={onClose} size="md" closeOnBackdrop>
      <ModalHeader className="flex flex-row items-center gap-2 space-y-0">
        <MapPinIcon className="w-5 h-5" />
        <h3 className="text-base leading-none">๋™๋„ค ์„ค์ •</h3>
      </ModalHeader>

      <ModalContent>
        <LocationPickerappKey={appKey}
          open={open}
          onPick={({ lat, lng, region }) => {
            // ์ง€์—ญ๋ช…/์ขŒํ‘œ๋ฅผ ์ƒ์œ„๋กœ ์ „๋‹ฌ (ex. ์ƒํƒœ ์ €์žฅ, DB ๋ฐ˜์˜)
            onSelect?.(region, lat, lng)
            onClose()
          }}
        />
      </ModalContent>
    </Modal>
  )
}

์™œ ์ด๋ ‡๊ฒŒ?

  • env๋Š” ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์ฃผ์ž…: ํ›…/์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ process.env๋ฅผ ์ง์ ‘ ์ฝ๋Š” ๋Œ€์‹ , prop์œผ๋กœ ์ฃผ์ž…ํ•˜๋ฉด ํ…Œ์ŠคํŠธ/ํ™˜๊ฒฝ ์ „ํ™˜์ด ์‰ฌ์›Œ์ง.
  • UI ๋ ˆ๋ฒจ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ: ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ(๊ณตํ†ต) โ†”๏ธ ์œ„์น˜ ์„ค์ • ๋„๋ฉ”์ธ(ํ—ค๋”/์ง€๋„) ๊ตฌ๋ถ„.

3) Kakao Map API ์ ์šฉ โ€” SDK ๋กœ๋” ํ›… + ์ง€๋„/์—ญ์ง€์˜ค์ฝ”๋”ฉ

์ด ํŒŒํŠธ๋Š” 2๊ฐœ ํŒŒ์ผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

  1. useKakaoLoader.ts: ์นด์นด์˜ค ์ง€๋„ SDK ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋™์ ์œผ๋กœ ๋กœ๋“œํ•˜๊ณ  ์ค€๋น„ ์™„๋ฃŒ ์ƒํƒœ(ready)๋ฅผ ์ œ๊ณต.
  2. LocationPicker.tsx: ์‹ค์ œ ์ง€๋„๋ฅผ ๋„์šฐ๊ณ , ์ง€๋„ ํด๋ฆญ/ํ˜„์žฌ ์œ„์น˜๋กœ ์ขŒํ‘œ๋ฅผ ๋ฐ›์•„ โ€œ์„œ์šธ์‹œ ๋™์ž‘๊ตฌโ€ ํ˜•์‹์˜ ๋ผ๋ฒจ์„ ์ƒ์„ฑ.

3-1) SDK ๋กœ๋” ํ›…

// hooks/useKakaoLoader.ts
'use client'

import { useEffect, useState } from 'react'

const KAKAO_SDK_ID = 'kakao-maps-sdk'

export function useKakaoLoader(appKey: string) {
  const [ready, setReady] = useState(false)

  useEffect(() => {
    if (typeof window === 'undefined') return

    // ์ด๋ฏธ ๋กœ๋“œ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ฆ‰์‹œ OK
    if ((window as any).kakao?.maps) {
      setReady(true)
      return
    }

    // ๊ฐ™์€ ID์˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์ด๋ฒคํŠธ๋งŒ ์—ฐ๊ฒฐ (์ค‘๋ณต ์ฃผ์ž… ๋ฐฉ์ง€)
    const prev = document.getElementById(KAKAO_SDK_ID) as HTMLScriptElement | null
    if (prev) {
      const onLoad = () => (window as any).kakao.maps.load(() => setReady(true))
      prev.addEventListener('load', onLoad)
      return () => prev.removeEventListener('load', onLoad)
    }

    // ์‹ ๊ทœ ์ฃผ์ž…
    const script = document.createElement('script')
    script.id = KAKAO_SDK_ID
    script.async = true
    script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${appKey}&libraries=services&autoload=false`
    script.onload = () => (window as any).kakao.maps.load(() => setReady(true))
    script.onerror = (e) => console.error('[KakaoLoader] script error', e)
    document.head.appendChild(script)
  }, [appKey])

  return ready
}

์ค€๋น„๋ฌผ

  • ์นด์นด์˜ค ๊ฐœ๋ฐœ์ž ์ฝ˜์†”์—์„œ โ€œ์นด์นด์˜ค๋งต(์ง€๋„/๋กœ์ปฌ)โ€ ์ œํ’ˆ ํ™œ์„ฑํ™”
  • JavaScript ํ‚ค ์‚ฌ์šฉ (REST ํ‚ค X)
  • ํ”Œ๋žซํผ ๋„๋ฉ”์ธ ๋“ฑ๋ก: http://localhost:3000 ๋“ฑ

ํƒ€์ž… ๊ฒฝ๊ณ ๊ฐ€ ๋œจ๋ฉด ํ•œ๋ฒˆ๋งŒ ์ถ”๊ฐ€:

// types/kakao.maps.d.ts
export {}
declare global { interface Window { kakao: any } }

tsconfig.json โ†’ "include": ["next-env.d.ts","src/types/**/*.d.ts","**/*.ts","**/*.tsx"]


3-2) LocationPicker โ€” ์ง€๋„/ํ˜„์žฌ ์œ„์น˜/โ€œ์„œ์šธ์‹œ ๋™์ž‘๊ตฌโ€ ๋ผ๋ฒจ

// components/location/LocationPicker.tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import { useKakaoLoader } from '@/hooks/useKakaoLoader'

type Props = {
  appKey: string
  open: boolean
  onPick: (payload: { lat: number; lng: number; region: string }) => void
}

/** ์‹œ/๋„ ๋ณด์ •: '์„œ์šธ' โ†’ '์„œ์šธ์‹œ', '๊ฒฝ๊ธฐ' โ†’ '๊ฒฝ๊ธฐ๋„', '์„ธ์ข…' โ†’ '์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ' */
function formatSido(sido: string) {
  if (/(ํŠน๋ณ„์‹œ|๊ด‘์—ญ์‹œ|ํŠน๋ณ„์ž์น˜์‹œ|ํŠน๋ณ„์ž์น˜๋„|๋„|์‹œ)$/.test(sido)) return sido
  const metro = ['์„œ์šธ', '๋ถ€์‚ฐ', '๋Œ€๊ตฌ', '์ธ์ฒœ', '๊ด‘์ฃผ', '๋Œ€์ „', '์šธ์‚ฐ', '์„ธ์ข…']
  return metro.includes(sido) ? (sido === '์„ธ์ข…' ? '์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ' : `${sido}์‹œ`) : `${sido}๋„`
}

/** H(ํ–‰์ •๋™) ์šฐ์„  โ†’ "์„œ์šธ์‹œ ๋™์ž‘๊ตฌ" ํ˜•์‹ ๋ผ๋ฒจ ์ƒ์„ฑ */
function pickGuLabel(res: any[]) {
  const target = res.find((r: any) => r.region_type === 'H') ?? res[0]
  const { region_1depth_name: sidoRaw, region_2depth_name: guRaw } = target
  const sido = formatSido(sidoRaw || '')
  const gu = guRaw || ''
  return gu ? `${sido} ${gu}` : `${sido}`
}

export default function LocationPicker({ appKey, open, onPick }: Props) {
  const ready = useKakaoLoader(appKey)                       // โœ… SDK ์ค€๋น„ ์ƒํƒœ
  const boxRef = useRef<HTMLDivElement | null>(null)         // โœ… ์ง€๋„ ์ปจํ…Œ์ด๋„ˆ
  const [map, setMap] = useState<any>(null)
  const [marker, setMarker] = useState<any>(null)
  const [label, setLabel] = useState<string>('')

  // 1) ๋ชจ๋‹ฌ ์—ด๋ฆฌ๊ณ  + SDK ์ค€๋น„๋˜๋ฉด ์ง€๋„ ์ƒ์„ฑ
  useEffect(() => {
    if (!open || !ready || !boxRef.current || map) return
    const kakao = (window as any).kakao
    const center = new kakao.maps.LatLng(37.5665, 126.9780) // ์„œ์šธ์‹œ์ฒญ
    const _map = new kakao.maps.Map(boxRef.current, { center, level: 5 })
    const _marker = new kakao.maps.Marker({ position: center })
    _marker.setMap(_map)
    setMap(_map)
    setMarker(_marker)
  }, [open, ready, map])

  // 2) ๋ชจ๋‹ฌ์ด "์—ด๋ฆฐ ํ›„" ๋ ˆ์ด์•„์›ƒ ๋ณด์ • (๋ชจ๋‹ฌ ์ˆจ๊น€ ์ƒํƒœ์—์„œ ์ดˆ๊ธฐํ™”ํ•˜๋ฉด ํƒ€์ผ์ด ์•ˆ ๊ทธ๋ ค์ง)
  useEffect(() => {
    if (!open || !map) return
    const id = setTimeout(() => {
      map.relayout()
      map.setCenter(map.getCenter())
    }, 0)
    return () => clearTimeout(id)
  }, [open, map])

  // 3) ์ง€๋„ ํด๋ฆญ โ†’ ์ขŒํ‘œ โ†’ ์—ญ์ง€์˜ค์ฝ”๋”ฉ โ†’ "์„œ์šธ์‹œ ๋™์ž‘๊ตฌ"
  useEffect(() => {
    if (!map || !marker) return
    const kakao = (window as any).kakao
    const geocoder = new kakao.maps.services.Geocoder()

    const clickHandler = (e: any) => {
      const latlng = e.latLng
      marker.setPosition(latlng)
      map.panTo(latlng)
      geocoder.coord2RegionCode(
        latlng.getLng(),
        latlng.getLat(),
        (res: any, status: any) => {
          if (status === kakao.maps.services.Status.OK && res[0]) {
            setLabel(pickGuLabel(res))
          }
        }
      )
    }

    kakao.maps.event.addListener(map, 'click', clickHandler)
    return () => kakao.maps.event.removeListener(map, 'click', clickHandler)
  }, [map, marker])

  // 4) ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ (HTTPS ํ•„์š” / ๋กœ์ปฌ ๊ฐœ๋ฐœ์€ ์˜ˆ์™ธ์ ์œผ๋กœ ํ—ˆ์šฉ)
  const useCurrentLocation = () => {
    if (!map || !marker) return
    if (!navigator.geolocation) return alert('๋ธŒ๋ผ์šฐ์ €์—์„œ ์œ„์น˜๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์•„์š”.')
    navigator.geolocation.getCurrentPosition(
      ({ coords }) => {
        const kakao = (window as any).kakao
        const geocoder = new kakao.maps.services.Geocoder()
        const latlng = new kakao.maps.LatLng(coords.latitude, coords.longitude)
        marker.setPosition(latlng)
        map.setCenter(latlng)
        geocoder.coord2RegionCode(
          latlng.getLng(),
          latlng.getLat(),
          (res: any, status: any) => {
            if (status === kakao.maps.services.Status.OK && res[0]) {
              setLabel(pickGuLabel(res))
            }
          }
        )
      },
      () => alert('ํ˜„์žฌ ์œ„์น˜ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด ์ฃผ์„ธ์š”.')
    )
  }

  // 5) ์ƒ์œ„๋กœ ์„ ํƒ ๊ฒฐ๊ณผ ์ „๋‹ฌ
  const confirm = () => {
    if (!marker) return
    const pos = marker.getPosition()
    onPick({ lat: pos.getLat(), lng: pos.getLng(), region: label })
  }

  return (
    <div className="space-y-3">
      {/* ์ง€๋„๋Š” ๊ณ ์ • ๋†’์ด๊ฐ€ ๊ผญ ์žˆ์–ด์•ผ ํ•จ. (์—†์œผ๋ฉด ๋ณด์ด์ง€ ์•Š์Œ) */}
      <div ref={boxRef} className="h-72 w-full rounded-xl border overflow-hidden" />
      <div className="flex items-center justify-between text-sm">
        <div className="text-muted-foreground">
          {label ? `์„ ํƒ๋œ ์ง€์—ญ: ${label}` : '์ง€๋„๋ฅผ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”'}
        </div>
        <div className="flex gap-2">
          <button className="h-9 rounded-md border px-3" onClick={useCurrentLocation}>
            ํ˜„์žฌ ์œ„์น˜
          </button>
          <buttonclassName="h-9 rounded-md bg-primary px-3 text-primary-foreground disabled:opacity-50"
            disabled={!label}
            onClick={confirm}
          >
            ์„ ํƒ
          </button>
        </div>
      </div>
    </div>
  )
}

์™œ ์ด๋ ‡๊ฒŒ?

  • ๋ชจ๋‹ฌ์ด ์—ด๋ฆฐ ๋’ค ์ดˆ๊ธฐํ™” + relayout: ์ˆจ๊ฒจ์ง„ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ง€๋„๋ฅผ ๋งŒ๋“ค๋ฉด ํƒ€์ผ์ด ์•ˆ ๊ทธ๋ ค์ง€๋Š” ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐ.
  • ๊ตฌ/๊ตฐ ๋‹จ์œ„ ๋ผ๋ฒจ: coord2RegionCode ์‘๋‹ต์—์„œ region_2depth_name(๊ตฌ/๊ตฐ) + region_1depth_name(์‹œ/๋„) ์กฐํ•ฉ.
  • ์‹œ/๋„ ๋ณด์ •: โ€˜์„œ์šธโ€™ โ†’ โ€˜์„œ์šธ์‹œโ€™, โ€˜๊ฒฝ๊ธฐโ€™ โ†’ โ€˜๊ฒฝ๊ธฐ๋„โ€™ ๋“ฑ ๊ฐ€๋…์„ฑ ๊ฐœ์„ .

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ / ์ฝ˜์†” ์„ค์ • (ํ•œ ๋ฒˆ๋งŒ)

.env.local

NEXT_PUBLIC_KAKAO_JAVASCRIPT_KEY=์นด์นด์˜ค_JavaScript_ํ‚ค
  • dev ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ•„์ˆ˜.
  • ์นด์นด์˜ค ๊ฐœ๋ฐœ์ž ์ฝ˜์†”
    • ์ œํ’ˆ ์„ค์ • โ†’ ์นด์นด์˜ค๋งต(์ง€๋„/๋กœ์ปฌ) ํ™œ์„ฑํ™”(OPEN_MAP_AND_LOCAL)
    • ์•ฑ ์„ค์ • โ†’ ํ”Œ๋žซํผ โ†’ Web โ†’ ์‚ฌ์ดํŠธ ๋„๋ฉ”์ธ ๋“ฑ๋ก (http://localhost:3000 ๋“ฑ)
    • JavaScript ํ‚ค ์‚ฌ์šฉ (REST ํ‚ค X)

์ „์—ญ ํƒ€์ž… ์„ ์–ธ(์„ ํƒ):

// types/kakao.maps.d.ts
export {}
declare global { interface Window { kakao: any } }

๋งˆ๋ฌด๋ฆฌ โ€” ํ๋ฆ„ ์š”์•ฝ

  1. ๊ณตํ†ต ๋ชจ๋‹ฌ: createPortal๊ณผ SSR ๊ฐ€๋“œ๋กœ ์–ด๋””์„œ๋“  ์•ˆ์ „ํ•˜๊ฒŒ ๋„์›€
  2. LocationModal: ๊ณตํ†ต ๋ชจ๋‹ฌ์— โ€œ๋™๋„ค ์„ค์ •โ€ UI๋ฅผ ์–น๊ณ , env ํ‚ค๋ฅผ ํ•˜์œ„๋กœ ์ „๋‹ฌ
  3. Kakao Map API:
    • ๋กœ๋” ํ›…์œผ๋กœ SDK ํ•œ ๋ฒˆ๋งŒ ๋กœ๋“œ
    • ๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ ์ง€๋„ ์ดˆ๊ธฐํ™” + relayout()
    • ์ง€๋„ ํด๋ฆญ/ํ˜„์žฌ ์œ„์น˜ โ†’ ์—ญ์ง€์˜ค์ฝ”๋”ฉ โ†’ โ€œ์„œ์šธ์‹œ ๋™์ž‘๊ตฌโ€ ๋ผ๋ฒจ โ†’ ์ƒ์œ„๋กœ ์ „๋‹ฌ
profile
์ฝ”๋ฆฐ์ด

0๊ฐœ์˜ ๋Œ“๊ธ€