Canada Mortgage Compare
Project Case Study
Canada Mortgage Compare
캐나다 주요 5개 은행이 공시한 모기지 금리를 하루 두 번 모아 비교해 주는 독립 레퍼런스 사이트입니다. DB도 백엔드도 없는 순수 정적 Next.js 사이트로, 금리는 GitHub Actions에서 TypeScript 수집기가 긁어와 JSON으로 커밋합니다.
라이브 프로젝트 보기 →Overview
Canada Mortgage Compare는 캐나다 주요 은행이 공시한 모기지 금리를 모아 나란히 보여 줍니다. 첫 화면에는 부풀리지 않고 딱 하는 일만 적어 두었습니다. "Compare advertised Canadian mortgage rates."
지금은 은행 5곳의 상품 64개를 추적하고 있습니다. CIBC, National Bank of Canada, RBC, Scotiabank, TD이며, 하루 두 번 갱신합니다.
이 프로젝트를 설명하는 가장 좋은 방법은 무엇을 두지 않았는지 말하는 것입니다. DB도, 백엔드도, 유료 서비스도 없습니다. 사이트 전체가 Cloudflare Pages에 올라간 정적 Next.js 빌드이고, 금리 데이터는 저장소에 커밋된 JSON으로 존재합니다.
정직성 문제
금리 비교 사이트에는 구조적인 신뢰 문제가 있습니다. 레퍼런스인 척하지만 실은 대출 상담을 넘기기 위한 유입 퍼널인 경우가 대부분이기 때문입니다. 이 프로젝트는 반대편에 서기로 했고, 그 입장을 코드로 강제했습니다.
공시 금리는 제안이 아닙니다. 실제로 받게 될 금리는 신용도와 소득, 계약금, 물건에 따라 달라집니다. 이 사실을 흐리지 않고 그대로 밝힙니다.
`Offer`나 `Product` 구조화 데이터를 넣지 않습니다. SEO 마크업은 Organization, WebSite, BreadcrumbList까지만 씁니다. 보장되지도 않는 금리에 Offer 마크업을 붙이는 것은 코드 주석에 적어 둔 대로 기계가 읽는 형식으로 거짓말을 하는 일이고, 이는 글로 하는 거짓말보다 나쁩니다.
BMO는 등록해 두었지만 꺼 두었습니다. 켜려면 허가를 받아야 하기 때문이며, 그래서 BMO 금리는 공개하지 않습니다.
Architecture
DB를 두지 않은 이유
금리는 하루에 두 번 바뀌고 데이터의 양도 적습니다. DB를 두면 운영할 것만 늘어나고 얻는 것이 없습니다. 그래서 이런 흐름으로 만들었습니다. CI에서 수집기가 돌고, 검증을 거쳐, JSON을 커밋하면, 그 커밋이 Cloudflare Pages 배포를 일으킵니다.
next.config.ts에는 output: "export"와 trailingSlash: true를 설정해 두었습니다. 정적 빌드에서는 SSR과 ISR, 라우트 핸들러, 서버 액션을 쓸 수 없기 때문에 누군가 이런 것을 추가하면 빌드가 그 자리에서 실패합니다. 제약이 스스로를 지키는 셈입니다.
수집기
은행별 수집기를 TypeScript로 직접 작성해 collectors/에 두고 한곳의 레지스트리에 등록했습니다. HTML 파싱은 Cheerio가 맡습니다. Playwright도 준비해 뒀지만 필요할 때만 설치하도록 해 두었고, 지금 켜져 있는 수집기가 전부 평범한 fetch로 동작하기 때문에 실제로는 한 번도 실행되지 않았습니다.
가드레일은 실제로 일어나는 사고를 막기 위한 것입니다. 은행이 페이지를 개편하는 바람에 셀렉터가 엉뚱한 칸을 집어 오는 경우입니다.
금리는 0.5~15% 범위 안에 들어와야 합니다
1.5%p가 넘게 튀면 이상치로 봅니다
타임아웃 30초, 요청 간격 1.5초, 이상치는 한 번만 재시도합니다
타임존을 따진 스케줄
GitHub Actions 크론은 0 12,21 * * *입니다. 하루 두 번이지만 일부러 12시간씩 나누지 않고 9시간과 15시간 간격으로 두었습니다. 12:00 UTC는 토론토 오전 8시라 밤사이 바뀐 금리를 개장 시점에 잡을 수 있고, 21:00 UTC는 토론토 오후 5시로 마감 뒤이면서 밴쿠버로는 오후 2시입니다. 앞서 쓰던 0 6,18은 06:00 UTC가 토론토 새벽 2시여서 한 번을 버리는 셈이라 접었습니다.
파이프라인은 데이터가 실제로 바뀌었을 때만 커밋합니다. 커밋 한 번이 곧 배포 한 번이기 때문입니다.
Technical Implementation
캐나다식 모기지 계산
대부분의 계산기가 틀리는 부분입니다. 캐나다의 고정 모기지는 이자법에 따라 미국처럼 월 복리가 아니라 반기 복리로 계산합니다. 그래서 주기 이율을 이렇게 구합니다.
i = (1 + j/2)^(2/n) − 1부동소수점 오차가 쌓이지 않도록 decimal.js로 계산합니다. 납입 주기는 6가지를 지원하고, 가속 격주와 주간 납입은 월 납입액을 2나 4로 나눠서 구합니다. 정확한 상환식을 푸는 대신, 은행이 실제로 하는 방식을 그대로 따라간 것입니다. 어떤 은행도 그 정확한 식대로 받지는 않기 때문입니다. 기간 안에 상환이 끝나는 경우도 처리해서 총비용이 부풀려지지 않게 했습니다. 근사치가 하나 있는데, 변동금리에도 같은 반기 복리를 적용한다는 점이며 이는 문서에 밝혀 두었습니다.
히스토리는 공적인 기록입니다
금리 히스토리도 커밋된 JSON이고 최대 2,000건까지 남기며 product_added, rate_changed, terms_changed, product_removed를 기록합니다. 무결성 규칙 두 가지가 핵심입니다.
수집에 실패했거나 꺼져 있는 수집기는 아무 기록도 남기지 않습니다. 파서가 실패한 것을 두고 상품이 사라졌다고 적으면 공적인 기록에 거짓을 넣는 일이 됩니다.
새 상품은 수집한 시각이 아니라 은행이 밝힌 유효일자로 기록합니다. 그래야 첫 수집이 "하루 만에 모든 게 바뀐 날"처럼 보이지 않습니다.
스스로 숨는 필터
지금 있는 상품은 전부 전국 단위입니다. 데이터가 뒷받침하지 못하는 구분을 있는 것처럼 보여 주느니, 주별 필터가 스스로 숨도록 했습니다. 그리고 특정 주에서만 파는 상품을 가진 은행이 들어오는 순간 자동으로 다시 나타납니다.
차트 라이브러리를 쓰지 않았습니다
금리 히스토리는 직접 그린 SVG로 보여 줍니다. 컴포넌트 주석에 적어 둔 대로, 선 그래프 하나 때문에 차트 번들을 통째로 싣는 것은 남는 장사가 아닙니다.
실제 페이지로 테스트합니다
Vitest 스위트 11개가 은행별로 실제 받아 둔 HTML을 가지고 돕니다. 은행이 페이지를 바꾸면 라이브 사이트에 이상한 숫자가 뜨기 전에 테스트가 먼저 깨집니다.
What This Case Study Shows
문제에 맞는 가장 작은 구조를 고르고, 그 선택을 빌드가 지키게 만든 사례
데이터 무결성을 기술 문제가 아니라 편집 책임으로 다룬 관점
당연해 보이는 구현이 오히려 틀리는 영역에서 도메인 계산을 제대로 맞춘 경험
가드레일과 테스트, 정직한 실패 방식을 갖춘 스크래핑 파이프라인
인프라 비용을 사실상 들이지 않고 실제 데이터 제품을 운영하는 방법
Live: https://canadamortgage.servith.com/
Role: 1인 개발 (수집기 → 모기지 계산 → 무결성 규칙 → UI → CI)