Smart Drop Point 개발기 - 하차 지점을 추천하는 모바일 웹앱 만들기

2026. 5. 15. 16:40·Develop/SmartDropPoint

Smart Drop Point는 같이 차를 탔을 때 “어디서 내리면 좋을지”를 추천하는 모바일 웹앱입니다.

처음에는 아이디어가 꽤 단순했습니다.

운전자는 자기 목적지로 가야 하고, 승객은 다른 곳으로 가야 할 때.
둘 사이에서 적당한 하차 지점을 찾아주면 되지 않을까?

그런데 막상 만들기 시작하니 단순한 길찾기 앱과는 조금 다른 문제였습니다.

이번 글은 Smart Drop Point를 만들면서 어떤 기준으로 구조를 잡았는지 정리한 개발기입니다.

사용한 기술 스택

기본 구조는 Next.js App Router 기반으로 잡았습니다.

전체적으로는 모바일 웹/PWA를 먼저 생각했고, 추천 계산과 UI 상태가 섞이지 않도록 도메인 로직을 분리하는 방향으로 구성했습니다.

  • Next.js App Router
  • React
  • TypeScript strict mode
  • Tailwind CSS
  • Zustand persist
  • TanStack Query
  • Yup
  • Kakao Map / Kakao Local
  • Tmap / ODsay 연동을 고려한 어댑터 구조
  • Vitest 기반 테스트 구조

프론트엔드 앱처럼 보이지만, 내부적으로는 입력 검증, 좌표 정규화, 후보 필터링, 추천 점수화, 외부 API fallback 같은 서버/도메인 로직이 꽤 들어갑니다.

그래서 화면 컴포넌트 안에 계산 로직을 넣지 않고, features, adapters, types, constants 영역으로 나눠서 관리했습니다.

대략 이런 식입니다.

src/
├── adapters/          지도, 경로, 대중교통, 장소 검색 어댑터
├── app/               Next.js App Router 페이지와 API 라우트
├── components/        홈/결과 화면 UI 컴포넌트
├── constants/         앱 상수와 역 데이터
├── features/          추천 계산, 입력 흐름, URL 직렬화 같은 도메인 로직
├── stores/            사용자 설정과 최근 검색 상태
└── types/             도메인 타입

이렇게 나눠두면 UI를 바꾸는 작업과 추천 품질을 바꾸는 작업이 서로 덜 엉킵니다.

문제를 좁게 잡기

처음부터 모든 카풀 문제를 풀 생각은 없었습니다.

낯선 사람을 매칭하거나, 결제를 붙이거나, 실시간 위치를 추적하는 쪽으로 가면 제품의 범위가 너무 커집니다.

제가 잡은 문제는 훨씬 좁았습니다.

  • 이미 같은 차를 탄 상태
  • 출발지는 같음
  • 운전자 목적지와 승객 목적지가 다름
  • 중간에 어디서 내리면 좋을지 정해야 함

이 네 가지 조건에만 집중했습니다.

이렇게 좁히고 나니 앱의 목적도 분명해졌습니다.

“새로운 이동을 만들어주는 앱”이 아니라,
“이미 같이 이동 중인 사람들이 어디서 자연스럽게 갈라질지 정하게 도와주는 앱”입니다.

기존 지도 앱과 다른 계산

일반 지도 앱은 보통 A에서 B까지의 최적 경로를 계산합니다.

하지만 이 앱은 조금 다릅니다.

A에서 출발해서 운전자는 B로 가고, 승객은 C로 가야 합니다.
여기서 궁금한 건 A → B 또는 A → C가 아니라, A → ? → B에서 ?가 어디면 좋을지입니다.

이 후보 지점은 운전자만 기준으로 봐도 안 되고, 승객만 기준으로 봐도 안 됩니다.

운전자 입장에서는 우회 시간이 짧아야 하고,
승객 입장에서는 하차 후 대중교통이나 도보 이동이 괜찮아야 합니다.

그래서 추천 엔진은 크게 이런 흐름으로 잡았습니다.

  1. 운전자 출발지, 운전자 목적지, 승객 목적지의 좌표를 확정한다
  2. 후보가 될 수 있는 전철역이나 환승 거점을 추린다
  3. 운전자가 해당 지점을 경유했을 때 얼마나 우회하는지 본다
  4. 승객이 그 지점에서 목적지까지 얼마나 걸리는지 본다
  5. 우회 시간, 도보 시간, 환승 횟수, 총 이동 시간을 기준으로 점수를 낸다
  6. 상위 후보를 카드로 보여준다

말로 쓰면 간단하지만, 실제로는 “후보를 얼마나 많이 계산할 것인가”가 중요했습니다.
전국 역을 모두 대상으로 매번 외부 API를 호출하면 비용도 크고 느려집니다.

그래서 먼저 공간적으로 후보를 줄이고, 그다음 필요한 후보에만 경로 계산을 붙이는 방향으로 설계했습니다.

추천 엔진을 파이프라인으로 나누기

추천 로직은 한 번에 크게 계산하는 함수로 만들기보다, 단계별 파이프라인으로 나누었습니다.

대략 흐름은 이렇습니다.

입력 정규화
→ 좌표 확정
→ 후보 하차지 prefilter
→ 운전자 우회 시간 계산
→ 승객 대중교통/도보 시간 계산
→ 정책별 점수화
→ 상위 후보 정렬
→ 결과 카드 모델로 변환

이렇게 나눈 이유는 각 단계의 책임이 다르기 때문입니다.

좌표를 확정하는 단계는 장소 검색과 관련이 있고,
후보를 줄이는 단계는 공간 계산과 관련이 있고,
우회 시간과 대중교통 시간은 외부 API나 mock 어댑터와 관련이 있습니다.

이걸 한 함수 안에 다 넣으면 처음엔 빠르게 만들 수 있지만, 나중에 추천 품질을 조정할 때 어디를 고쳐야 하는지 헷갈립니다.

그래서 도메인 로직은 최대한 pure TypeScript 모듈에 두고, 외부 API 호출은 어댑터 뒤로 밀었습니다.

점수 계산도 단순히 총 시간이 짧은 순서만 보지 않았습니다.

  • 운전자 우회 시간
  • 승객 총 이동 시간
  • 도보 시간
  • 환승 횟수
  • 사용자가 고른 우선순위
  • 필터 조건

이 값을 조합해서 후보를 정렬합니다.

예를 들어 “운전자 배려”를 고르면 운전자 우회 시간이 더 강하게 반영되고, “환승 최소”를 고르면 승객 이동 시간 중에서도 환승 횟수 쪽 가중치가 커지는 식입니다.

결국 추천 결과는 단순 최단 시간이 아니라, 사용자가 그 상황에서 어떤 기준을 더 중요하게 보는지 반영한 결과가 됩니다.

좌표를 먼저 믿기

이 프로젝트에서 계속 중요하게 본 건 좌표입니다.

장소 이름만 가지고 계산하면 애매한 경우가 많습니다.
동명이인처럼 같은 이름의 장소가 여러 개 있을 수도 있고, 역명이나 동 이름만으로는 실제 위치가 흔들릴 수도 있습니다.

그래서 입력 단계에서 가능한 한 좌표를 확정하고, 이후 추천 계산은 좌표를 기준으로 흐르게 했습니다.

장소 자동완성도 이 목적에 맞춰 붙였습니다.
사용자가 동 이름이나 역 이름을 입력하면 실제 좌표를 얻고, 그 좌표를 URL에도 실어 결과 페이지가 같은 입력을 재현할 수 있게 했습니다.

공유 링크를 열었을 때도 다시 검색 결과가 달라지면 안 되기 때문입니다.

URL을 상태 저장소처럼 쓰기

결과 페이지는 URL 쿼리만으로도 다시 열릴 수 있게 만들었습니다.

처음에는 상태를 전역 store에만 넣어도 될 것 같았는데, 이 앱은 공유가 중요했습니다.
상대에게 링크를 보냈을 때 같은 결과가 열려야 하고, 새로고침해도 같은 후보가 유지되어야 합니다.

그래서 입력값은 이런 형태로 URL에 직렬화했습니다.

driverOrigin
driverOriginLat
driverOriginLng
driverDestination
driverDestinationLat
driverDestinationLng
passengerDestination
passengerDestinationLat
passengerDestinationLng
maxDetourMinutes
maxWalkMinutes
priority
picked

장소명뿐 아니라 위도/경도를 같이 싣는 이유는 재현성 때문입니다.
장소명을 다시 검색하면 같은 이름의 다른 장소가 나올 수도 있고, 자동완성 결과가 시간이 지나며 바뀔 수도 있습니다.

반대로 좌표가 URL에 있으면 결과 페이지는 다시 장소 검색을 하지 않아도 같은 추천 계산을 시작할 수 있습니다.

폼 입력과 URL 경계에서는 Yup 스키마로 값을 검증하고, 내부 도메인에서는 TypeScript 타입을 기준으로 다루도록 나눴습니다.
폼 검증 스키마가 도메인 타입을 대체하지 않게 한 점도 의도한 부분입니다.

어댑터로 외부 의존성 분리

지도나 경로 계산, 대중교통 정보는 외부 API에 의존합니다.

현재 구조에서는 각각을 바로 코드에 박아 넣지 않고 어댑터 뒤에 두었습니다.

  • 운전 경로
  • 대중교통 경로
  • 지도 SDK
  • 장소 검색

이 네 영역을 분리해두면, 처음에는 mock으로 시작하고 나중에 Tmap, ODsay, Kakao 같은 실제 API로 바꾸기 쉽습니다.

이 방식이 마음에 들었던 이유는 개발 단계에서도 앱의 흐름을 먼저 만들 수 있다는 점이었습니다.
외부 키가 없거나 API가 잠깐 실패해도, 최소한 핵심 화면은 확인할 수 있어야 했습니다.

그래서 fallback 구조도 같이 넣었습니다.
키가 없으면 mock으로 돌아가고, 지도나 자동완성처럼 실제 키가 필요한 기능은 가능한 범위에서만 켜지게 했습니다.

실제 API와 mock을 같은 인터페이스로 맞추기

외부 API는 붙이는 것보다 바꾸기 쉽게 만드는 쪽이 더 중요하다고 봤습니다.

운전 경로는 Tmap, 대중교통은 ODsay, 지도는 Kakao Map, 장소 검색은 Kakao Local을 염두에 두었지만, 코드 곳곳에서 직접 호출하지는 않았습니다.

대신 각각을 이런 식의 역할로 분리했습니다.

  • IRoutingAdapter: 운전 경로와 우회 시간 계산
  • ITransitAdapter: 하차 후 대중교통/도보 이동 계산
  • IMapAdapter: 지도 렌더링과 마커/폴리라인 갱신
  • IPlaceSearchAdapter: 장소 자동완성과 좌표 확정

실제 구현체와 mock 구현체는 같은 인터페이스를 따릅니다.

덕분에 개발 중에는 API 키 없이도 화면과 추천 흐름을 계속 볼 수 있고, 실제 키가 들어오면 같은 도메인 로직을 유지한 채 어댑터만 바꿔서 정밀도를 올릴 수 있습니다.

또 하나 좋았던 점은 장애 대응입니다.

외부 API가 실패해도 앱 전체가 멈추는 대신, 가능한 범위에서 fallback 결과를 보여줄 수 있습니다.
지도나 자동완성은 비활성화될 수 있어도, 핵심 추천 화면은 mock 데이터로 확인할 수 있게 했습니다.

작은 사이드 프로젝트라도 이런 분리선을 미리 잡아두면, 나중에 실제 서비스를 붙일 때 훨씬 덜 흔들립니다.

모바일이 먼저인 UI

이 앱은 PC에서 오래 보는 서비스가 아닙니다.
대부분 차 안에서 잠깐 열어볼 가능성이 높습니다.

그래서 처음부터 모바일 웹을 기준으로 잡았습니다.

홈 화면에서는 운전자 출발지, 운전자 도착지, 승객 목적지를 하나의 여정 클러스터처럼 묶었습니다.
각 입력이 따로 흩어져 있으면 관계가 잘 안 보이기 때문입니다.

결과 화면에서는 1순위 후보를 크게 보여주고, 나머지 후보는 압축해서 비교할 수 있게 했습니다.
후보가 너무 많으면 오히려 결정이 늦어지니, 빠르게 고를 수 있는 카드 구성이 더 맞다고 봤습니다.

시간 비교도 숫자만 보여주기보다 가로 막대로 같이 표현했습니다.
차 안에서 작은 글씨를 오래 읽기보다, 대략 어느 후보가 더 유리한지 바로 보이게 하고 싶었습니다.

결과 UI는 “설명”보다 “비교”에 가깝게

결과 화면을 만들 때 가장 신경 쓴 건 후보 간 비교였습니다.

사용자는 이 앱을 오래 읽으려고 여는 게 아닙니다.
지금 차 안에서 빨리 정해야 하니까 여는 겁니다.

그래서 결과 카드에는 많은 설명을 넣기보다, 의사결정에 필요한 숫자를 최대한 같은 위치에 반복해서 배치했습니다.

  • 운전자 추가 시간
  • 승객 총 이동 시간
  • 도보 시간
  • 환승 횟수
  • 추천 사유
  • 여정 타임라인

시간 스택바도 이 맥락에서 넣었습니다.

승객 이동 시간을 대기, 대중교통, 도보처럼 나눠서 보여주면 “총 30분”이라는 숫자만 볼 때보다 더 판단하기 쉽습니다.
예를 들어 같은 30분이라도 도보가 긴 30분과 지하철 한 번 타는 30분은 체감이 다릅니다.

1순위 후보는 크게 보여주고, 2·3순위는 압축 카드로 두었습니다.
그리고 다른 후보가 더 마음에 들면 바로 promote해서 지도와 카드 상태가 같이 바뀌도록 했습니다.

이 부분은 단순 UI라기보다, 추천 결과를 사용자가 납득하는 과정이라고 생각했습니다.

결과 페이지는 공유 가능해야 한다

이 앱은 혼자만 보는 도구가 아닐 가능성이 큽니다.
운전자에게 보여주거나, 같이 탄 사람에게 링크를 보내야 할 수도 있습니다.

그래서 결과 상태가 URL에 담기도록 했습니다.
선택한 후보도 picked 값으로 직렬화해서, 링크를 다시 열었을 때 같은 후보를 볼 수 있게 했습니다.

나중에는 카카오톡이나 SNS에 공유했을 때 선택한 후보의 이름과 시간이 들어간 미리보기 이미지도 만들 수 있도록 구조를 잡았습니다.

결국 이 앱의 결과는 “내가 보는 화면”이면서 동시에 “상대에게 보여줄 근거”이기도 합니다.

성능과 호출 수 줄이기

추천 계산은 여러 외부 정보를 섞기 때문에 호출 수가 쉽게 늘어날 수 있습니다.

특히 개발 환경에서는 React StrictMode나 새로고침 때문에 같은 요청이 반복될 수 있습니다.
이 부분은 runRecommendation 쪽에 캐시를 두는 방식으로 줄였습니다.

같은 입력에 대해서는 일정 시간 동안 결과를 재사용하고,
이미 진행 중인 요청이 있으면 같은 Promise를 공유하도록 했습니다.

작은 앱이라도 이런 부분을 초기에 잡아두면, 나중에 실제 API를 붙였을 때 비용과 속도 면에서 훨씬 안정적입니다.

후보를 줄이는 방식

전국 전철역과 환승 거점을 모두 후보로 두면 보기에는 좋아 보이지만, 실제 계산에서는 부담이 큽니다.

그래서 처음부터 전체 후보에 대해 외부 API를 호출하지 않고, 먼저 가벼운 공간 계산으로 후보를 줄였습니다.

기본 아이디어는 이렇습니다.

  1. 운전자 출발지와 도착지 주변 영역을 잡는다
  2. 승객 목적지와 너무 동떨어진 후보를 제외한다
  3. 운전자 출발지와 너무 가까운 지점은 제외한다
  4. 남은 후보를 직선거리와 대략적인 위치 관계로 정렬한다
  5. 그중 상위 후보에 대해서만 routing/transit 계산을 수행한다

이때 haversine 거리 계산과 bounding box를 사용했습니다.
정밀 경로 계산은 비싸지만, 후보를 줄이는 단계에서는 직선거리 기반 계산만으로도 충분히 의미가 있습니다.

이렇게 prefilter를 먼저 거치면 실제 API 호출 수를 줄일 수 있고, 결과 응답도 빨라집니다.
사용자 입장에서는 그냥 빠르게 결과가 뜨는 것처럼 보이지만, 내부적으로는 “비싼 계산을 하기 전에 싼 계산으로 줄이는” 흐름입니다.

이런 구조는 나중에 후보 데이터가 1천 개, 1만 개로 늘어나도 그대로 확장할 수 있습니다.

개인정보는 최소로

이 앱은 위치를 다루기 때문에 개인정보에 대한 기준도 단순하게 잡았습니다.

초기에는 서버에 입력한 출발지나 목적지를 저장하지 않습니다.
최근 검색이나 즐겨찾기는 사용자 브라우저의 localStorage에만 둡니다.

서버 키가 필요한 API는 Next API 라우트 뒤로 숨기고, 브라우저에는 공개 가능한 키만 노출되도록 했습니다.

회원가입 없이 쓰는 도구이기 때문에, 데이터 저장을 최소화하는 쪽이 제품 성격에도 맞았습니다.

지금 상태와 다음 단계

현재는 모바일 웹앱 형태로 흐름을 볼 수 있는 상태입니다.

입력 화면, 추천 결과 카드, 지도 미리보기, 후보 비교, 공유를 고려한 URL 구조까지 잡아두었습니다.
전국 전철·도시철도역 데이터를 기반으로 후보를 만들고, 외부 API가 있으면 실제 경로 계산을 쓰고, 없으면 mock fallback으로 화면을 확인할 수 있게 했습니다.

다음에 더 다듬고 싶은 부분은 이런 것들입니다.

  • 시간대별 가중치 자동 조정
  • 막차나 심야 안전성을 반영한 추천
  • 결과 공유용 이미지 카드
  • 여러 명의 승객을 동시에 고려하는 모드
  • 추천이 좋았는지 피드백을 받는 UX

처음부터 다 넣기보다는, 지금은 “한 명의 운전자와 한 명의 승객”이라는 기본 문제를 제대로 푸는 데 집중하고 있습니다.

정리

Smart Drop Point는 거대한 이동 플랫폼을 만들려는 프로젝트는 아닙니다.

오히려 아주 작고 구체적인 순간을 보고 있습니다.

같이 차를 탔는데 서로 목적지가 다를 때,
어디서 내리면 운전자도 덜 부담스럽고 승객도 편할까?

이 질문에 좌표, 경로, 대중교통 정보를 얹어서 빠르게 답해보는 앱입니다.

만들면서 느낀 건, 작은 문제라도 실제 생활의 맥락이 들어가면 설계할 게 꽤 많다는 점이었습니다.
입력은 짧아야 하고, 결과는 바로 비교돼야 하고, 숫자는 신뢰할 수 있어야 하고, 모바일에서 흔들리는 손으로도 쓸 수 있어야 했습니다.

아직 더 다듬을 부분은 많지만, 문제를 좁게 잡고 구조를 나눠두니 다음 단계로 확장하기는 좋아졌습니다.

이제는 실제 사용 사례를 더 넣어보면서 추천 품질을 조금씩 올려봐야겠습니다.

저작자표시 비영리 변경금지 (새창열림)

'Develop > SmartDropPoint' 카테고리의 다른 글

같이 탄 차에서 어디서 내릴지 정해주는 앱  (0) 2026.05.15
'Develop/SmartDropPoint' 카테고리의 다른 글
  • 같이 탄 차에서 어디서 내릴지 정해주는 앱
상구너
상구너
개발, AI, 자동화, 일상 기록을 담는 상구너의 작업노트. 직접 해본 것, 써본 것, 정리한 것을 가볍고 실용적으로 남깁니다
  • 상구너
    상구너의 개발노트
    상구너
  • 전체
    오늘
    어제
    • 분류 전체보기 (56) N
      • Day by day (5)
        • Diary (2)
        • TV (0)
      • Challenge (6) N
      • Study (28)
        • AI (6)
        • NodeJS (0)
        • JavaScript (3)
        • Markup (1)
        • Linux (6)
        • Java (1)
        • BCI&BCEL&ASM (8)
        • WAS (1)
        • DB (1)
      • Develop (16) N
        • inKrKamus (3)
        • SmartDropPoint (2) N
      • Faith (0)
  • 블로그 메뉴

    • 홈
    • GITHUB
    • LinkedIn
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Agent
    oci
    openclaw
    html5
    Hermes
    Hermes Agent
    Canvas
    Ai
    tts
    그룹웨어 자동화
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
상구너
Smart Drop Point 개발기 - 하차 지점을 추천하는 모바일 웹앱 만들기
상단으로

티스토리툴바