본문 바로가기
프로젝트/풀스택

Trip Planner - 지도 기반 여행계획 풀스택 앱

by 루까(Luka) 2026. 2. 25.
반응형

왜 만들었나

여행 계획을 짤 때 구글 지도, 메모 앱, 엑셀을 동시에 열어두고 작업하는 게 불편했다. 지도를 보면서 순서를 정하고, 이동수단을 선택하고, 체크리스트처럼 관리하는 도구가 필요했다. 포트폴리오 목적도 있었고, 실제로 쓸 수 있는 앱을 만들고 싶었다.


기술 스택

레이어 선택 이유
Frontend React + Vite + Tailwind CSS v4 빠른 개발, 최신 Tailwind
Backend Node.js + Express 심플한 REST API
DB MongoDB 유연한 스키마 (장소 데이터 구조가 자주 바뀜)
지도 Leaflet.js + OpenStreetMap 완전 무료
도로 경로 Valhalla 공개 서버 무료, 5개 교통수단 프로파일
대중교통 ODsay LAB 버스+지하철 최적 혼합 경로
인증 JWT + bcrypt 기본기
배포 Vercel + MongoDB Atlas 무료 tier

핵심 제약: 예산 0원. Google Maps API 대신 Leaflet+OSM, 자체 라우팅 엔진으로 해결했다.


주요 기능

1. 지도 + 장소 관리

  • Google Places Text Search로 장소 검색 (백엔드 프록시)
  • 검색한 장소를 지도 마커로 추가
  • 드래그 앤 드롭으로 방문 순서 변경
  • 체크박스로 방문 완료 표시 + 진행률 바
  • 장소별 메모 + 예약번호 입력 (기차/배/비행기)
  • 장소 클릭 → 지도 flyTo 이동 (모바일: 자동으로 지도 탭 전환)

2. 8종 이동수단 + 실제 경로

도보      → Valhalla pedestrian  (보도 우선)
자전거    → Valhalla bicycle     (자전거도로 우선, 고속도로 회피)
바이크    → Valhalla motor_scooter (자동차전용도로 제한)
대중교통  → ODsay SearchPathType=0 (버스+지하철 최적 혼합)
기차      → 코레일 역 202개 DB → Haversine 최근접 역 탐색
자동차    → Valhalla auto        (유료도로 자동 감지)
배        → 직선 (해상)
비행기    → 직선 (항공)

이동수단별로 실제로 다른 경로를 반환한다. 서울역→강남역 기준으로 도보(125분)와 자동차(27분)가 명확히 다른 경로를 그린다.

3. SegmentCard (구간 카드)

장소 카드 사이에 구간 정보를 표시한다.

  • 이동수단 아이콘 + 예상 시간/거리
  • 대중교통: 환승 상세 (노선명, 승하차 정류장, 노선 컬러)
  • 기차: 출발역→도착역 + 코레일 예약 버튼 (새 탭)
  • 유료도로 포함 시 경고 배지
  • 지도 폴리라인 클릭 → 해당 SegmentCard 하이라이트 + 자동 스크롤

4. 공유 & PDF 내보내기

  • 공개 링크 생성 → /shared/:id 읽기 전용 뷰 (구간 카드 포함)
  • PDF 내보내기: 장소 목록 + 구간 정보 + 환승 상세 + 유료도로 안내
    • 지도 타일은 CORS 이슈로 캡처 불가 (html2canvas 구조적 한계)

삽질 기록

OSRM → Valhalla 교체

초기에는 OSRM 데모 서버(router.project-osrm.org)를 썼다. 근데 치명적인 문제가 있었다.

서울→부산 장거리에서 car/bike/foot 3개 프로파일이 완전히 동일한 경로(396.7km, 4321 포인트)를 반환했다. 데모 서버가 car 프로파일 하나만 서비스하고 있던 것. exclude=motorway 파라미터도 InvalidValue로 거부했다.

Valhalla 공개 서버(valhalla1.openstreetmap.de)로 교체 후 해결. API 키 없이 무료, 5개 프로파일이 실제로 다른 경로를 반환한다.

Valhalla 주의사항 두 가지:

1. 응답이 GeoJSON이 아닌 polyline6(precision=6) 인코딩
   → 직접 디코딩 함수 구현 필요

2. 한글 도로명의 이스케이프 문자로 JSON.parse() 실패
   → 정규식으로 summary 블록 먼저 추출 후 파싱

ODsay 인증 이슈

ODsay가 API 키뿐만 아니라 Origin 헤더 기반 도메인 인증을 사용한다. 백엔드에서 직접 호출하면 [ApiKeyAuthFailed]가 반환된다.

// 백엔드 fetch에 Origin 헤더 추가
headers: { 'Origin': 'http://localhost:5173' }

등록된 도메인의 Origin을 헤더에 직접 넣어야 한다.

Valhalla 200km+ 제한

pedestrian/bicycle costing은 200km 초과 시 Path distance exceeds the max distance limit 에러가 발생한다. 직선 폴백으로 처리하면 시간/거리가 null이 된다.

해결: 분할 라우팅

직선 거리 190km 초과 → 170km 단위로 구간 분할
→ 각 분할 구간 Valhalla 병렬 호출 (Promise.all)
→ coords/time/distance 합산 (중복 접점 slice(1) 제거)
→ 분할도 실패 시 Haversine 추정 + 경고 배지 표시

링크 공유 버그

공유 링크 활성화가 간헐적으로 실패했다. clipboard API 문제인 줄 알았는데 실제 원인은 다른 곳에 있었다.

원인: 레거시 데이터에 transport = 'transit' 값이 저장됨
      → plan.save() 시 Mongoose enum 불일치 → 500 에러

수정 1: Plan.js enum에 'transit' 추가 (하위 호환)
수정 2: plan.save() → Plan.updateOne({$set}) 교체
        (전체 validation 대신 isPublic 필드만 업데이트)

아쉬운 점

1. Google Places API 의존

검색은 무료 API가 없다. Google Places Text Search를 백엔드 프록시로 우회해서 쓰는데, 한 달 무료 크레딧 이후엔 유료다. 실제 서비스로 키울 계획이 있다면 다른 방법이 필요하다.

2. Valhalla 공개 서버 의존성

안정성 보장이 없다. 장애 시 경로를 못 그린다. 경로 새로고침 버튼을 추가해서 캐시 무효화 후 재시도하는 방법으로 부분 완화했지만, 근본 해결은 아니다.

3. PDF 지도 캡처 불가

html2canvas로 지도를 캡처하면 OpenStreetMap 타일이 CORS 정책으로 블록된다. PDF에 장소 목록과 구간 정보는 들어가지만 지도 이미지는 빠진다.


회고

포트폴리오 프로젝트 치고 기능이 많아졌다. 처음엔 "지도에서 장소 추가하고 순서 관리"만 하려고 했는데, 이동수단별 실제 경로, 대중교통 환승 상세, 기차 API 연동, VMS 이벤트 오버레이까지 붙다 보니 꽤 복잡해졌다.

제로 예산 제약 속에서 무료 API 조합을 찾는 과정이 가장 재밌었다. OSRM 데모 서버 버그를 발견하고 Valhalla로 교체한 것, ODsay Origin 인증 이슈를 파악한 것 등 문서에 없는 내용을 직접 파악하는 경험이 유익했다.


기술 스택: React, Vite, Tailwind CSS v4, Node.js, Express, MongoDB, Leaflet.js, Valhalla, ODsay
라이브 데모: https://trip-planner-woad-seven.vercel.app
소스 코드: GitHub

반응형