빈방
운영 중인기 숙소의 빈방과 가격 하락을 대신 지켜보는 알림 서비스
원하는 숙소를 등록해 두면 빈방이 다시 나오거나 가격이 내려갈 때 이메일로 알려주는 웹 서비스입니다. 웹과 확인 워커를 나눠 설계하고, 배포와 운영까지 혼자 맡고 있습니다.

왜 만들었나
인기 숙소는 몇 달 전에 이미 매진되지만, 체크인이 가까워지면 취소된 방이 하나둘 다시 나옵니다. 문제는 그 순간을 잡으려면 하루에도 몇 번씩 예약 페이지를 새로고침해야 한다는 점이었습니다.
그래서 원하는 숙소를 등록해 두면 빈방이 나거나 가격이 내려갈 때 대신 알려주는 서비스를 만들었습니다. 첫 버전을 하루 만에 출시하고, 지금까지 직접 운영하며 고쳐 나가고 있습니다.
할 수 있는 것
- 로그인 없이 검색하고 알림 등록: Agoda 숙소를 바로 검색하고, 이메일 인증 코드만으로 알림을 걸면 가입까지 함께 끝납니다. 등록한 숙소는 30분마다 자동으로 확인합니다.
- 빈방·가격 하락 감지: 매진이던 방이 다시 나오거나 이전보다 가격이 내려가면 알림을 보냅니다. 같은 알림이 반복되지 않도록 빈방은 24시간, 가격 하락은 6시간 동안 다시 보내지 않습니다.
- 이메일 알림과 바로 예약: 알림 메일에서 바로 예약 페이지로 이동할 수 있고, 메일 하나로 수신을 거부할 수 있습니다.
- 5개 언어 지원: 한국어, 영어, 일본어, 중국어, 스페인어로 볼 수 있고, 언어마다 검색 엔진에 맞는 페이지를 만듭니다.
- 운영자 화면: 워커 생존 상태, 처리량, 설정 변경 이력을 확인하고, 코드 배포 없이 워커 설정을 바꾸거나 재시작할 수 있습니다.
구조
사용자가 보는 웹(apps/web)과 숙소를 주기적으로 확인하는 워커(apps/worker)를 따로 배포하는 앱으로 나누었습니다. 웹은 요청에 빠르게 응답하는 데만 집중하고, 오래 걸리는 확인 작업은 워커가 큐에서 꺼내 처리합니다.
데이터베이스는 packages/db 한 곳에서만 다루도록 모으고, 앱끼리 공유하는 코드는 packages/shared를 거치게 했습니다. 이 경계를 규칙 문서로 정해 두고 지키면서, 같은 저장소에 AI 여행 도우미(apps/travel)를 더할 때도 기존 코드를 흔들지 않았습니다.
고민한 점
스크래핑에서 제휴 API로
처음에는 Airbnb와 Agoda 숙소 페이지를 Playwright로 직접 열어 10분마다 새로고침하며 빈방을 읽어 왔습니다. 브라우저를 매번 새로 띄우면 느려서 Chromium 인스턴스를 풀로 묶어 재사용하고, 사이트 화면이 바뀔 때마다 배포하지 않도록 값을 읽는 CSS 셀렉터를 데이터베이스에서 관리했습니다.
그런데 운영하면서, 허락 없이 남의 사이트를 반복해서 긁어 오는 방식은 사이트 이용약관에 어긋날 수 있다는 걸 뒤늦게 알게 됐습니다. 화면이 바뀌면 언제든 멈출 수 있다는 점도 계속 불안했습니다.
그래서 Agoda와 제휴(어필리에이트)를 맺고 공식 API로 빈방과 가격을 확인하도록 구조를 바꿨습니다. 이제 정해진 방식으로 데이터를 받으니 화면 변경에 흔들리지 않고, 사용자가 알림을 눌러 예약하면 제휴 수익으로도 이어집니다. 잘 돌아가는 것보다 떳떳하게 돌아가는 것이 먼저라는 걸 배운 일이었습니다.
로그인 없이 바로 시작하게
사용자 데이터를 살펴보니 로그인 단계에서 떠나는 사용자가 많았습니다. 숙소를 찾아보기도 전에 카카오나 구글 계정 연결부터 요구하다 보니, 가입을 하고도 정작 알림은 걸지 않고 떠나는 경우가 대부분이었습니다.
그래서 순서를 바꿨습니다. 로그인 없이 먼저 숙소를 검색하고, 알림을 걸 때 이메일로 받은 6자리 코드만 입력하면 알림 등록과 회원가입이 한 번에 끝나도록 했습니다. 비밀번호도, 별도의 가입 화면도 없습니다. 첫 화면도 서비스 소개 대신 숙소 검색창을 맨 위에 두고, 검색 → 날짜 → 이메일 → 인증 코드 → 완료가 첫 화면 안에서 끝나게 다시 설계했습니다. 300만 개가 넘는 호텔은 외부 API가 아니라 자체 DB에서 검색하기 때문에, 로그인 전에도 검색을 열어 둘 수 있었습니다.
화면을 다듬으며 신경 쓴 부분은 이렇습니다. 덜어낼 것을 덜어낸 결과 첫 화면 길이는 데스크톱 2,473px → 1,953px, 모바일 3,717px → 2,502px로 줄었습니다.
- 보이는 위계 맞추기: 검색 영역의 제목보다 아래 보조 문구가 더 커서 검색창이 곁다리처럼 보였습니다. 페이지의
h1을 하나로 정리해 크기를 바로잡고, 숙소를 고르기 전에도 날짜 칸과 버튼을 보여 무엇을 입력할지 한눈에 들어오게 했습니다. - 한글 줄바꿈:
text-balance는 한글 어절 중간을 끊어서 제목이 "방 나 / 면"처럼 잘렸습니다.break-keep과 명시적 줄바꿈으로 바꿨습니다. - 서버와 사용자의 시간대 차이: 날짜 칸의 최솟값을 렌더링 중에 계산하다 보니, UTC 서버와 KST 브라우저의 날짜가 서로 다른 오전 0~9시에 하이드레이션 오류가 났습니다. 계산을 마운트 이후로 옮겨 해결했습니다.
- 데이터가 없으면 단계를 뺀다: 검색한 숙소의 지난 빈방 기록을 보여주는 단계를 넣었다가, 기록이 있는 숙소가 약 300만 개 중 2개뿐이라는 걸 확인하고 뺐습니다. 이메일을 받기 직전에 빈 화면을 한 번 더 보여주는 셈이었기 때문입니다.
바꾸기 전 30일 동안은 7명이 가입했지만 알림을 등록한 사람은 1명뿐이었습니다. 바꾼 뒤 30일 동안은 8명이 가입해 8명 모두 알림을 등록했고, 새로 생긴 알림도 1개에서 12개로 늘었습니다. 아직 사용자가 많지 않은 서비스라 큰 숫자는 아니지만, 사용자가 가치를 먼저 확인하게 하고 필요한 정보는 꼭 필요한 순간에만 받는 것이 왜 중요한지 확인한 일이었습니다.
보여주는 숫자는 사실만
랜딩을 처음 만들 때 채워 둔 "실시간 운영 현황"이 그대로 남아 있었습니다. 활성 모니터 3,421개, 가동률 99.99% 같은 숫자는 하드코딩된 값이었고, 당시 실제 모니터는 8개였습니다. "기존보다 4배 빠르게"처럼 근거 없는 문구도 있었습니다.
처음 보는 사용자의 신뢰를 얻어야 하는 서비스에서, 확인하면 바로 드러나는 숫자는 오히려 독이라고 판단했습니다. 모두 지우고 "30분마다 확인합니다", "무료입니다", "언제든 그만둘 수 있습니다"처럼 코드로 확인할 수 있는 사실로 바꿨습니다.
검색으로 찾아오게 만들기
광고비를 쓸 수 없는 1인 서비스라, 사용자가 검색으로 직접 찾아오게 하는 것이 가장 현실적인 홍보였습니다. 그래서 검색 엔진이 읽을 수 있는 페이지를 늘리고, 읽기 쉽게 만드는 데 공을 들였습니다.
- 숙소마다 공개 페이지 만들기: 워커가 모은 빈방·가격 기록을 숙소별, 지역별 공개 페이지로 만들었습니다. "OO 호텔 빈방"처럼 구체적으로 검색하는 사람도 들어올 수 있는 입구가 숙소 수만큼 생깁니다.
- 사이트맵 나누기: 페이지가 많아도 검색 엔진이 빠짐없이 가져가도록 고정 페이지, 지역, 숙소 사이트맵을 나누고, 숙소는 1만 개 단위로 잘라 한 시간마다 새로 만듭니다.
- 구조화 데이터: 서비스, 숙소, 자주 묻는 질문, 이동 경로(breadcrumb)를 schema.org 형식으로 넣어, 검색 결과에 더 풍부하게 표시될 수 있게 했습니다.
- 다국어 주소 정리: 5개 언어 페이지마다
canonical과hreflang을 달아 같은 내용이 중복으로 취급되지 않게 하고, 기본 언어는 접두어 없는 주소로 통일했습니다. - Google과 네이버에 함께 등록: 국내 사용자가 많은 만큼 Google Search Console과 네이버 서치어드바이저에 모두 사이트를 등록했습니다.
그 결과 Google에서 "빈방알림"으로 검색하면 첫 번째 결과로 노출됩니다. "빈방"이라는 일반 단어로만 검색해도 첫 페이지에 나오고, 네이버에서도 "빈방알림"으로 검색하면 첫 페이지에 나옵니다(2026년 10월 기준).


워커를 멈추지 않게 운영하기
숙소를 확인하는 일은 느리고 실패도 잦습니다. 웹 요청 안에서 처리하면 화면이 같이 느려지기 때문에, BullMQ와 Redis로 작업 큐를 만들어 30분마다 확인하고 5분마다 알림을 보내는 일을 워커에 맡겼습니다.
알림 서비스는 조용히 멈추는 게 가장 위험합니다. 워커가 주기적으로 하트비트를 남기게 하고, 끊기면 운영자에게 알림이 오도록 했습니다. 확인 주기마다 성공·실패·소요 시간을 모아 처리량 대시보드로 볼 수 있게 했습니다.
혼자서도 팀처럼 일하기
혼자 만드는 서비스지만 팀에서 일하듯 작업했습니다. 기능마다 브랜치를 나눠 PR로 올리고, Codex·CodeRabbit 같은 AI 리뷰어의 지적을 반영한 뒤 병합했습니다. 2026년 1월부터 9월까지 커밋 1,000여 개, PR 117개, 릴리스 40번이 쌓였고, 테스트 파일은 73개입니다.
AI 코딩 도구를 많이 쓰는 만큼, 도구가 구조를 무너뜨리지 않도록 규칙 문서(`rules.md`)와 에이전트 지침(`AGENTS.md`, `CLAUDE.md`)에 패키지 경계와 작업 원칙을 적어 두었습니다. 사람이든 AI든 같은 규칙을 따르고, 린트·타입 검사·테스트·빌드를 pnpm ci:check 하나로 묶어 커밋 전에 돌립니다. GitHub Actions는 같은 검증에 CodeQL 보안 검사, Docker 배포, 릴리스 태그까지 자동으로 반복합니다.
기술 스택
- 프런트엔드: Next.js 16, React 19, TypeScript, Tailwind CSS 4, shadcn/ui, TanStack Query, next-intl
- 워커: BullMQ, Redis, Agoda 제휴 API (초기에는 Playwright 스크래핑)
- 백엔드·데이터: Oracle Autonomous Database, TypeORM, NextAuth, Zod
- 운영: Docker, GitHub Actions, Sentry