2026. 7. 8. 16:00ㆍAI 기반 프론트엔드 웹개발자 양성과정

[미니 프로젝트] 트래블틱(Travel-Tick)
여행 소비 기록장 — 기획부터 정산 알고리즘까지
2026.07.08 · HTML + CSS + JS · 멀티페이지 4개 (index · create · trip · report)
📋 오늘 한 일
지난 이틀간 배운 이벤트·반복문·배열/객체·localStorage·fetch를 전부 모아 자유 주제 미니 프로젝트를 만들었다. 주제는 여행 소비 기록장. 여행 중 지출을 적으면 오늘 환율로 원화까지 자동 계산해주고, 여행별로 모아 보고, 일행과 나눠 낸 돈을 최소 송금 루트로 정산까지 해주는 앱이다. 기획서 작성 → 화면 스케치 → HTML 뼈대 4개 → CSS 레트로 캐릭터 테마 → JS 기능 구현 → F12 디버깅 → Netlify 배포까지 기획서에 적어둔 순서를 전부 끝냈다. 보너스로 잡아뒀던 실시간 환율 fetch도 붙였다.
1. 기획 — 왜 만들었나
여행지에서 "이거 우리 돈으로 얼마지?"를 매번 계산하는 게 귀찮고, 영수증은 버려지고 그날의 기분·맛집 평가는 금방 잊힌다. 게다가 일행과 나눠 낸 돈은 누가 누구에게 얼마 줘야 하는지 정산이 늘 복잡했다. 이 세 가지 — 기록 + 환율 + 정산을 한 번에 해결하는 걸 목표로 잡았다.
2. 화면 구성 — 멀티페이지 4개
파일 하나에 다 우겨넣지 않고, 화면마다 파일을 나눴다. 페이지끼리는 서버 없이 localStorage로 데이터를 공유하는데, 이 저장/조회 로직만 store.js 하나에 모아 4개 페이지가 공용으로 불러 쓰게 만들었다.
| 페이지 | 파일 | 내용 |
|---|---|---|
| 🏠 홈 / 여행 리스트 | index.html | 랜딩 히어로(마스코트) + 총 여행·지출 통계 + 진행중/지난 여행 카드 |
| ✍️ 여정 생성/수정 | create.html | 나라 선택 → 통화·환율 자동 · 이름 · 기간 · 예산 · 멤버 (?id=로 수정 겸용) |
| 🧾 여행 타임라인 | trip.html | 예산 대시보드 + 지출 기록 폼 + 일차별 리스트 (카드 클릭 시 수정) |
| 💸 최종 결산 | report.html | 공동지출 총액 + 최적 송금 루트 + 인원별 내역 |
store.js에 loadState()·saveState()·getTrip(id)·putTrip(trip) 네 함수만 모아두고, 나머지 4개 스크립트는 전부 이걸 불러 쓴다. 여러 페이지가 같은 저장 로직을 중복해서 짜지 않아도 됐다.3. 핵심 기능 뜯어보기
① 여행 상태 자동 판별 (진행중 / 예정 / 지난)
여행마다 상태 값을 직접 저장하지 않고, 오늘 날짜와 시작일·종료일을 비교해서 그때그때 계산하도록 만들었다. 데이터가 하나 줄어드는 대신 날짜만 맞으면 항상 최신 상태가 보장된다.
function tripStatus(trip) { const today = new Date().toISOString().slice(0, 10); if (trip.endDate && trip.endDate < today) return "past"; if (trip.startDate && trip.startDate > today) return "upcoming"; return "ongoing"; }
<·>로 바로 비교할 수 있는 건 toISOString().slice(0,10)이 항상 "YYYY-MM-DD" 형태로 나오기 때문. 자릿수가 고정된 문자열은 사전순 비교가 곧 날짜순 비교와 같다.② 나라 선택 → 통화·환율 자동 + 실시간 환율 fetch
나라 타일을 클릭하면 통화(JPY·THB·USD 등)와 참고 환율이 자동으로 채워진다. 여기에 더해 "🔄 실시간 환율" 버튼을 누르면 무료 환율 API(open.er-api.com)에서 그날 실제 환율을 fetch로 받아와 덮어쓴다 — 기획서의 보너스 항목이었는데 오늘 붙이는 데 성공했다.
rateFetchBtn.addEventListener("click", async () => { const cur = currencyInp.value; try { const res = await fetch(`https://open.er-api.com/v6/latest/${cur}`); const data = await res.json(); if (data.result !== "success") throw new Error("응답 오류"); rateInp.value = Math.round(data.rates.KRW * 10000) / 10000; } catch (err) { rateStatus.textContent = "⚠️ 불러오기 실패, 직접 입력해주세요"; } });
try/catch를 fetch 실패 대비로 처음 실제 프로젝트에 써봤다. 네트워크가 끊기거나 API가 응답이 이상해도 앱이 멈추지 않고 "직접 입력해주세요"로 안내하게 만든 부분이 마음에 든다.③ 지출 기록 + 공동지출 참여자 칩 선택
지출은 개인 아니면 공동인데, 공동이라고 항상 전원이 나눠 내는 건 아니다. 그래서 "공동"을 고르면 멤버 칩이 나오고, 탭해서 빠질 사람만 빼는 방식으로 만들었다(기본은 전원 참여).
let splitParticipants = [...trip.members]; // 기본은 전원 참여 chip.onclick = () => { splitParticipants = on ? splitParticipants.filter((m) => m !== name) // 있으면 빼기 : [...splitParticipants, name]; // 없으면 추가 renderSplitChips(); };
participants 배열만 지출 객체에 함께 넣는다. 결산(report.html) 단계에서 "이 지출에 실제로 참여한 사람끼리만" 금액을 나누도록, 전원이 아니라 participants 길이로 share를 계산했다.④ 예산 대시보드 — 막대그래프 + 응원 문구
지금까지 쓴 돈을 원화로 합산해 예산 대비 퍼센트로 막대를 채우고, 사용률 구간에 따라 다른 색·문구를 보여준다. 조건문 하나로 앱에 "감정"을 붙인 셈이다.
const pct = budget > 0 ? Math.min((totalKrw / budget) * 100, 100) : 0; bar.style.width = pct + "%"; if (trip.expenses.length === 0) { cheer.textContent = "😊 여행 시작! 첫 지출을 기록해보세요"; } else if (pct < 70) { cheer.textContent = `😊 편안한 페이스예요! ${Math.round(pct)}% 사용`; } else if (pct < 100) { cheer.textContent = "😮 예산의 70%를 넘었어요. 슬슬 아껴볼까요?"; } else { cheer.textContent = "🚨 예산을 다 썼어요! 지출에 주의하세요"; }
⑤ 최적 정산 — 최소 송금 루트 (그리디 알고리즘)
가장 신경 쓴 기능. 사람마다 "낸 돈(paid)"과 "실제 부담해야 할 몫(owed)"을 구해 balance = paid - owed를 계산한 뒤, 가장 많이 받을 사람과 가장 많이 줄 사람을 매번 짝짓는 방식으로 송금 횟수를 최소화했다.
while (true) { const credit = bal.reduce((a, b) => (b.amt > a.amt ? b : a), { amt: -Infinity }); const debit = bal.reduce((a, b) => (b.amt < a.amt ? b : a), { amt: Infinity }); if (credit.amt < EPS || debit.amt > -EPS) break; const move = Math.min(credit.amt, -debit.amt); transfers.push({ from: debit.name, to: credit.name, amt: move }); credit.amt -= move; debit.amt += move; }
EPS = 0.01은 소수점 계산 오차 때문에 완전히 0이 안 되는 경우를 걸러내는 안전장치다.⑥ 여행 · 지출 수정 — 만든 뒤에도 고칠 수 있게
주소창의 ?id= 쿼리 하나로 "새로 만들기"와 "수정하기" 폼을 겸용으로 썼다. id가 있으면 기존 데이터를 불러와 폼에 채우고, 제출할 때 새로 만들지 덮어쓸지를 분기했다.
const editId = param("id"); // URLSearchParams로 ?id= 값 읽기 const editingTrip = editId ? getTrip(editId) : null; if (editingTrip) { // 기존 값으로 폼 채우기, id·expenses는 그대로 유지 editingTrip.name = name; editingTrip.members = members; putTrip(editingTrip); } else { const trip = { id: Date.now(), name, members, expenses: [] }; putTrip(trip); }
editingTrip.expenses.forEach(...)). 데이터 하나를 바꿀 때 그걸 참조하는 다른 데이터까지 챙겨야 한다는 걸 직접 겪으며 배웠다.4. 배운 것을 어떻게 썼나
| 배운 것 (STEP) | 이 프로젝트에서 쓴 곳 |
|---|---|
| 이벤트 (08) | 나라 타일·칩·버튼 클릭 전부 |
| localStorage (10) | store.js로 4개 페이지 간 데이터 공유 |
| 반복문 (11) | forEach·map·filter·reduce로 목록·합계 계산 |
| 배열·객체 (13·14) | 여행(trip)·지출(expense) 데이터 구조 전체 |
| 디버깅 (16) | 정산 계산이 안 맞을 때 console.log로 balance 값 확인 |
| fetch (17) | 실시간 환율 API 호출 (보너스 목표 달성) |
🔁 회고
✍️ 마치며
이틀 동안 배운 문법 하나하나는 작았는데, 그걸 모아 실제로 쓸모 있어 보이는 앱 하나를 완성하고 나니 "이래서 배우는구나"가 실감났다. 특히 render()로 데이터가 바뀔 때마다 화면을 다시 그리는 패턴과 store.js 같은 공용 모듈로 코드를 정리하는 습관은 이번 프로젝트에서 가장 크게 남았다. 다음엔 시간이 되면 영수증 OCR을 붙여보고, 클래스로 Trip·Expense 구조를 다시 짜보고 싶다.
🏷️ JavaScript | 미니프로젝트 | localStorage | fetch | 알고리즘 | 학습일지
'AI 기반 프론트엔드 웹개발자 양성과정' 카테고리의 다른 글
| [260710 10일차] React (0) | 2026.07.10 |
|---|---|
| [260709 09일차] React (0) | 2026.07.09 |
| [260707화 07일차] JavaScript (0) | 2026.07.08 |
| [260706월 06일차] Mini-Project, JavaScript (0) | 2026.07.06 |
| [260703금 05일차] CSS (0) | 2026.07.03 |