← 블로그 · Projects
[케이스 스터디] love-trip — 커플 여행 계획을 한 화면으로 모은 제품 설계기
#Next.js #TypeScript #Supabase #케이스 스터디 #외부 API #모노레포
love-trip — 커플 여행 계획을 한 화면으로 모은 제품 설계기
토리스(Toris Inc.)가 설계·개발·운영하는 커플 맞춤 여행 서비스 love-trip의 기술 케이스 스터디다. 제품 가설 수립부터 아키텍처 결정, 외부 API 의존성 관리, 검증 루프까지 1인 스튜디오 관점에서 정리한다.
제품 스냅샷
| 항목 | 내용 |
|---|---|
| 제품명 | love-trip |
| 개발 착수 | 2025.08 |
| 기술 스택 | Next.js, TypeScript, Supabase, Tour API, Naver Maps API, pnpm workspace |
| 상태 | 개발 진행 중 (베타 운영) |
| 핵심 가치 | 교통편·숙소·데이트 장소·경비를 하나의 추천 플로우로 통합 |
1. 어떤 사용자의 어떤 문제인가 — 제품 가설
커플이 여행을 계획하는 과정은 정보가 심하게 파편화되어 있다. 장소 탐색은 SNS와 블로그, 이동 경로는 지도 앱, 숙소는 예약 플랫폼, 예산은 메모 앱이나 스프레드시트로 흩어져 있고, 사용자는 이 도구들 사이를 오가며 정보를 직접 이어 붙여야 한다. “여기 좋은 카페가 어디 있지?”, “이 코스는 어떻게 이동하지?”, “예산은 얼마나 필요하지?”라는 세 가지 질문이 서로 다른 앱에서 각각 답을 요구하는 구조다.
love-trip의 제품 가설은 단순하다.
출발지, 목적지, 예산 — 이 세 가지 입력만으로 이동·장소·비용이 함께 계산된 여행 코스를 제안할 수 있다면, 커플의 여행 계획 비용(시간·인지 부하)을 크게 줄일 수 있다.
이 가설의 첫 사용자는 개발자 본인이 속한 커플이었다. 도구를 만드는 사람과 도구를 가장 자주 쓰는 사람이 같다는 점은 1인 스튜디오 제품에서 가장 빠른 피드백 루프를 확보하는 조건이 된다.
2. 제약 조건
1인 스튜디오의 제품은 제약을 먼저 명확히 하고 설계에 들어간다.
- 인력: 기획·개발·운영 전부 1인. 서버 운영 부담을 최소화하는 매니지드 서비스 중심 구성이 필수였다.
- 예산: 인프라 비용을 사실상 0에 수렴시켜야 했다. Vercel 무료 티어와 Supabase 무료 티어, 공공 데이터 API(무료)로 스택을 구성한 이유다.
- 데이터: 여행 장소 데이터를 직접 구축할 수 없다. 한국관광공사 Tour API라는 공공 데이터에 의존하되, 공공 데이터가 커버하지 못하는 영역은 크롤러로 보완하는 이원화 전략을 택했다.
- 일정: 완성 후 출시가 아니라, 동작하는 최소 코스 추천 플로우를 먼저 배포하고 실제 여행 계획에 써 보며 반복 개선하는 방식으로 진행했다.
3. 기술 설계와 트레이드오프
3.1 모노레포 — 웹·크롤러·공용 로직의 경계 설정
이 제품은 성격이 다른 두 개의 실행 단위를 갖는다. 사용자가 접하는 Next.js 웹 앱과, 데이터를 주기적으로 수집하는 크롤러다. 둘은 배포 주기와 실행 환경이 다르지만 데이터 모델과 유틸리티를 공유해야 한다.
선택지는 세 가지였다.
| 대안 | 장점 | 버린 이유 |
|---|---|---|
| 단일 저장소·단일 앱 | 가장 단순 | 크롤러가 웹 앱 빌드·배포에 끌려 들어감 |
| 저장소 분리 (multi-repo) | 완전한 독립 | 공용 타입·유틸을 패키지로 발행·버전 관리하는 비용이 1인 체제에 과함 |
| pnpm workspace 모노레포 | 코드 공유 + 독립 실행 | — (채택) |
love-trip/
├── web/ # Next.js 프론트엔드 (Vercel 배포)
├── crawler/ # Puppeteer 기반 데이터 수집기 (스케줄 실행)
└── shared/ # 공용 타입·데이터 가공 유틸리티
# pnpm-workspace.yaml
packages:
- 'web'
- 'crawler'
- 'shared'
핵심 판단 기준은 **“공용 코드의 변경이 두 실행 단위에 동시에, 별도 배포 절차 없이 반영되어야 한다”**였다. 장소 데이터의 스키마가 바뀌면 크롤러의 저장 로직과 웹의 렌더링 로직이 함께 바뀌어야 하는데, 모노레포에서는 이것이 하나의 커밋으로 원자적으로 처리된다. multi-repo였다면 shared 패키지 버전 발행 → 양쪽 저장소 업데이트라는 절차가 매번 끼어들었을 것이다.
3.2 데이터 파이프라인 — 공공 API와 크롤러의 이원화
Tour API (1차 소스): 한국관광공사 Tour API에서 지역 코드 기반으로 관광지 데이터를 수집한다.
// Tour API 호출
const fetchTouristSpots = async (areaCode: string) => {
const response = await fetch(
`http://apis.data.go.kr/B551011/KorService1/areaBasedList1?serviceKey=${API_KEY}&numOfRows=10&pageNo=1&MobileOS=ETC&MobileApp=love-trip&arrange=A&areaCode=${areaCode}&contentTypeId=12`
);
const data = await response.json();
return processTourData(data);
};
공공 API 응답은 그대로 쓰기 어렵다. 필드 명명이 도메인과 맞지 않고 형식이 일관되지 않아, processTourData에서 내부 도메인 모델로 변환하는 계층을 반드시 거치게 했다. 외부 데이터가 애플리케이션 내부 타입으로 직접 흘러 들어오지 않게 하는 변환 경계는 이후 API 스펙 변경 시 수정 범위를 이 함수 하나로 좁혀 주었다.
크롤러 (2차 소스): 공공 데이터가 커버하지 못하는 장소 정보는 Puppeteer 기반 크롤러로 보완했다. 크롤러 운영에서 확립한 원칙은 세 가지다.
- 법적·윤리적 경계 준수 — 수집 대상과 방식을 보수적으로 제한했다.
- 수집과 서비스의 분리 — 크롤링 실패가 사용자 서비스 장애로 전이되지 않도록, 크롤러는 DB에 적재만 하고 웹 앱은 DB만 읽는다.
- 검증 후 적재 — 대상 사이트의 구조 변경으로 오염된 데이터가 들어오는 것을 막기 위해 저장 전 데이터 검증 로직을 둔다.
3.3 외부 API 의존성 관리
이 제품은 Tour API, Naver Maps API라는 외부 의존성 위에 서 있다. 외부 API는 언제든 스펙이 바뀌고, 실패하고, 호출 제한에 걸린다. 대응 전략은 세 겹이다.
- 캐싱: 동일 지역·조건의 조회 결과를 캐싱해 API 호출량 자체를 줄인다. 호출 제한 대응이면서 동시에 응답 속도 개선이다.
- 철저한 에러 처리: API 실패가 화면 전체의 실패로 번지지 않도록 실패 지점을 격리한다.
- 대체 데이터 소스: 1차 소스 실패 시 크롤러가 적재해 둔 자체 DB 데이터로 폴백한다. 3.2의 이원화 전략이 여기서 가용성 장치로도 기능한다.
3.4 지도 통합 — Naver Maps
추천 코스는 목록이 아니라 지도 위의 동선으로 보여야 설득력이 있다. 국내 장소 데이터 정합성을 기준으로 Naver Maps API를 선택했고, 마커·경로 렌더링과 모바일 반응형 대응을 구현했다.
// 네이버 지도 초기화
useEffect(() => {
const map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(37.5665, 126.978),
zoom: 13
});
spots.forEach((spot) => {
const marker = new naver.maps.Marker({
position: new naver.maps.LatLng(spot.lat, spot.lng),
map: map
});
});
}, []);
지도는 마커 수가 늘수록 성능이 급격히 나빠지는 영역이라, 마커·경로 객체의 생성과 해제를 명시적으로 관리하는 것을 초기부터 규칙으로 삼았다.
3.5 코스 생성 로직 — 규칙 기반 점수 시스템
“출발지·목적지·예산 → 코스”의 자동 생성은 이 제품에서 가장 복잡한 로직이다. 고려 변수는 출발지-목적지 거리, 교통편과 소요 시간, 예산 범위 내 장소 선택, 사용자 선호도다.
초기부터 ML 추천을 도입하는 대안도 있었지만 버렸다. 학습에 쓸 사용자 행동 데이터가 없는 단계에서 ML은 비용만 크고 설명 불가능한 결과를 낸다. 대신 단계별로 분리된 규칙 기반 점수 시스템을 택했다 — 후보 장소를 수집하고, 거리·비용·선호 축으로 점수화하고, 예산 제약 안에서 상위 조합을 코스로 구성하는 파이프라인이다. 규칙 기반의 장점은 “왜 이 코스가 나왔는가”를 개발자가 즉시 설명하고 조정할 수 있다는 것으로, 피드백을 반영해 가중치를 손보는 초기 반복 단계에 적합했다.
3.6 예산 관리와 1/N 정산
커플 여행의 실질적 마찰 지점 중 하나가 비용 분담이다. 항목별 예산 입력, 실시간 예산 계산, 1/N 정산을 구현하면서 확립한 원칙은 금액 계산의 정확성은 협상 불가라는 것이다. 부동소수점 오차가 정산 금액에 나타나는 순간 제품 신뢰가 무너지는 기능이므로, 계산 로직은 가장 보수적으로 작성하고 예산 초과 시 사용자에게 즉시 알리는 UX를 함께 설계했다.
4. 사용자와의 검증 — 피드백 루프
love-trip의 검증 대상은 명확했다. 실제 여행을 계획하는 커플, 그 첫 표본은 개발자 자신의 커플이었다.
피드백 루프는 다음과 같이 돌렸다.
- 실사용 투입: 실제 여행 계획에 love-trip을 직접 사용한다. 데모가 아니라 진짜 여행의 의사결정 도구로 쓴다.
- 마찰 기록: 추천 코스가 어색한 지점(“이 동선은 실제로는 못 다닌다”, “예산 계산이 체감과 다르다”)을 기록한다.
- 로직 반영: 기록된 마찰을 점수 시스템의 가중치와 규칙으로 번역해 반영한다.
이 루프에서 실제로 바뀐 것들이 있다. 코스 생성 로직이 단순 필터링에서 점수 기반 추천으로 재설계된 것, 예산 초과 알림이 추가된 것, 데이터 출처를 사용자에게 명시하게 된 것이 모두 사용 중 발견된 마찰에서 나온 변경이다. 특히 데이터 출처 명시는 “이 정보가 최신인가?”라는 사용자 불신에 대한 대응으로, 여러 소스의 데이터 갱신 주기가 다른 제품에서는 신뢰를 유지하는 최소 장치라고 판단했다.
베타 단계의 검증은 정량 지표보다 **“다음 여행에도 이 도구를 다시 열게 되는가”**라는 질적 기준으로 판단하고 있다. 재사용이 일어나는 기능(코스 지도, 예산 정산)과 그렇지 않은 기능을 구분하는 것이 현재 로드맵의 우선순위 근거다.
5. 운영과 현재 상태
- 배포: Vercel 기반 자동 배포. 크롤러는 스케줄 실행으로 데이터를 주기 갱신한다.
- 데이터 정합성: 여러 소스의 갱신 주기 차이에서 오는 불일치는 정기 업데이트 스케줄링 + 저장 전 검증 + 출처 명시라는 3단 대응으로 관리한다.
- 장애 격리: 크롤러 실패, 외부 API 실패가 각각 서비스 중단으로 전이되지 않는 구조를 유지한다.
제품은 개발 진행 중이며, 로드맵의 다음 항목은 우선순위 순으로 다음과 같다.
- 실시간 정보 연동 — 교통·날씨 데이터를 코스 추천에 반영해 추천의 시의성을 높인다.
- 사용자 리뷰 수집 — 자체 리뷰 데이터로 크롤링 의존도를 낮추고 신뢰 가능한 1차 데이터를 확보한다.
- AI 기반 추천 — 규칙 기반 시스템으로 축적한 사용 데이터가 임계치를 넘으면, 그때 학습 기반 추천으로 전환을 검토한다. 순서가 반대가 되면 안 된다는 것이 현재 판단이다.
6. 다음 제품에 재사용할 교훈
love-trip은 토리스가 가장 오래 다듬고 있는 제품이고, 이후 제품들에 그대로 이식된 원칙 몇 가지를 남겼다.
- 외부 데이터는 반드시 변환 경계를 거친다. 외부 API 응답이 내부 타입으로 직접 흐르게 두면, 외부 스펙 변경이 코드베이스 전체의 수정으로 번진다. 변환 함수 한 겹이 유지보수 범위를 결정한다.
- 수집과 서비스는 실행 단위부터 분리한다. 크롤러 장애가 서비스 장애가 되지 않는 구조는 아키텍처 단계에서만 확보할 수 있다. 모노레포는 이 분리를 유지하면서 코드 공유 비용을 없애는 1인 체제 최적해였다.
- 추천은 규칙 기반으로 시작한다. 데이터가 없는 단계의 ML은 허세다. 설명 가능하고 즉시 조정 가능한 규칙 시스템으로 피드백 루프를 먼저 돌리고, 데이터가 쌓인 뒤 학습 기반을 검토한다.
- 개발자 자신이 타깃 사용자인 제품은 가장 싼 검증 환경이다. 단, 자기 사용 경험을 일반화하지 않도록 마찰을 기록으로 남기고 기록 기반으로 변경하는 규율이 필요하다.
- 돈 계산 기능은 신뢰의 마지노선이다. 정산·예산처럼 금액을 다루는 기능의 오차는 다른 기능 열 개의 완성도를 무의미하게 만든다.