독일의 임대 시장은 수요가 공급을 한참 넘습니다. 인기 지역의 괜찮은 집은 포털에 올라오자마자 문의가 수십 통씩 몰리고, 늦게 본 사람은 연락조차 못 합니다. 그래서 사람들은 ImmoScout24 같은 사이트를 하루에도 수십 번 새로고침합니다. Fredy는 그 새로고침 노동을 기계에게 떠넘기는 도구입니다.
당신이 "베를린, 방 2개, 월세 1200유로 이하" 조건을 한 번 알려주면, Fredy라는 알바생이 ImmoScout·Immowelt·당근마켓(독일판 eBay Kleinanzeigen) 등을 5분마다 돌며 새 글을 확인합니다. 조건에 맞는 새 매물을 발견하면 그 즉시 당신의 카카오톡(Slack·Telegram)으로 "이거 떴어요!" 하고 링크를 보냅니다. 한 번 본 매물은 다시 안 알려주고, 같은 집이 여러 사이트에 중복으로 올라와도 한 번만 알려주죠.
핵심은 내 서버에서 직접 돌린다(self-hosted)는 점입니다. 남의 유료 알림 서비스에 가입하는 게 아니라, Docker로 명령어 한 줄이면 내 PC나 NAS에 Fredy가 깔립니다. 검색 조건·알림 채널은 브라우저로 열리는 웹 UI에서 클릭 몇 번으로 설정합니다. 데이터는 전부 내 손 안에 있고, 비용은 0원입니다.
Fredy 자체는 오래된 프로젝트지만, 2026년에 다시 떠오른 데에는 몇 가지 분명한 이유가 있습니다. 첫째, 독일·유럽의 주거난이 심해지면서 "매물 알림 자동화" 수요가 커졌고, 둘째로 최근 버전(V20+)에서 MCP 서버를 내장해 Claude·ChatGPT 같은 LLM과 연결되는 길을 열었기 때문입니다. 셋째, 부동산 포털들이 봇 차단을 강화하자 Fredy가 봇 탐지 회피 기술로 정면 대응한 점이 개발자들의 흥미를 끌었습니다.
비슷한 도구·접근과 비교하면 Fredy의 위치가 또렷해집니다.
| 방식 | 커버 범위 | 비용 / 내 데이터 |
|---|---|---|
| Fredy | 독일 5대 포털 + 추가 (스크래핑·역설계 API) | 무료 / 내 서버에 보관 |
| 포털 자체 알림 | 그 포털 1곳만 | 무료지만 여러 곳을 따로 관리 |
| 유료 매물 알림 서비스 | 업체가 정한 범위 | 구독료 / 업체 서버에 보관 |
| 직접 새로고침 | 내 인내심만큼 | 무료지만 사람 노동·놓침 발생 |
요즘 대형 포털(특히 ImmoScout24)은 봇 탐지 시스템을 깔아둡니다. 평범한 스크래퍼로 접근하면 "당신은 봇입니다 :-/" 화면이 뜨고 차단당하죠. 단순 fetch나 기본 헤드리스 브라우저로는 몇 번 긁다가 막힙니다.
브라우저 기반 포털은 CloakBrowser(지문을 진짜 크롬과 구별 불가능하게 만든 하드닝 헤드리스 브라우저)로 긁고, ImmoScout24는 아예 그들의 모바일 앱 API를 역설계해 우회합니다. 서버(데이터센터 IP)에서 막힐 땐 "주거용 프록시(residential proxy)"를 꽂아 일반 가정집 인터넷처럼 보이게 합니다. 봇 탐지와의 싸움을 정면으로 다룬다는 게 차별점입니다.
마지막 한 방은 설치 장벽이 낮다는 점입니다. docker run 한 줄이면 9998 포트에 웹 UI가 뜨고, Unraid(가정용 NAS OS) 커뮤니티 스토어에서도 클릭 설치됩니다. 거기에 JetBrains의 오픈소스 후원까지 받는 꾸준히 관리되는 프로젝트라는 신뢰가 더해집니다.
Fredy는 거창한 마이크로서비스가 아니라, 하나의 Node.js 프로세스가 백엔드·스케줄러·스크래퍼를 모두 돌리고, 별도의 React 앱이 그 위에 웹 UI를 얹는 구조입니다. 무겁지 않게 "개인이 자기 서버에 올려 돌리는" 목적에 딱 맞춘 설계입니다.
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| 웹 서버 | Fastify ^5.8 | API·웹 UI 서빙. Express보다 빠른 경량 프레임워크 |
| 데이터베이스 | better-sqlite3 ^12 | 매물·검색·설정 저장. 파일 한 개로 끝나는 내장 DB |
| 스케줄러 | node-cron ^4.2 | "5분마다 검색 실행" 같은 주기 작업 타이머 |
| 스크래핑 | puppeteer-core + cloakbrowser + cheerio | 헤드리스 브라우저로 페이지 열고 HTML 파싱 |
| 세션·보안 | @fastify/session·cookie·helmet | 로그인 세션, 보안 헤더 |
| LLM 연동 | @modelcontextprotocol/sdk ^1.29 | MCP 서버 — Claude 등이 매물 데이터를 도구로 조회 |
Fredy의 강점 중 하나는 알림 채널이 15종이라는 것. 각각이 독립된 어댑터 파일로, 메시지 SDK를 끌어옵니다.
| 분류 | 채널 / 패키지 |
|---|---|
| 채팅 | Slack(slack·webhook)·Telegram·Discord·Mattermost |
| 이메일 | SendGrid·Mailjet·Nodemailer(SMTP)·Resend |
| 푸시·기타 | ntfy·Pushover·Apprise·HTTP(웹훅)·Console·SQLite |
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| UI 프레임워크 | React 19.2 + react-router 7 | 검색·매물·설정 화면 |
| 컴포넌트 키트 | @douyinfe/semi-ui 2.99 | ByteDance(틱톡)의 디자인 시스템. 버튼·폼·테이블 |
| 상태 관리 | zustand ^5 | 가볍고 단순한 전역 상태 저장소 |
| 지도 | maplibre-gl + mapbox-gl-draw + @turf | 지도에서 다각형을 그려 "이 동네 안만" 필터 |
| 차트·빌드 | chart.js + Vite 8 | 통계 시각화 / 초고속 번들러 |
fetch가 아니라 이게 필요합니다. puppeteer-core가 이 브라우저를 코드로 조종하는 리모컨입니다.Dockerfile과 docker-compose.yml을 제공하고, 공식 이미지는 ghcr.io/orangecoding/fredy(GitHub Container Registry)로 배포됩니다. 설정은 /conf, 데이터베이스는 /db 볼륨에 보관해, 컨테이너를 지워도 검색·매물 기록이 살아남습니다. 9998 포트 하나만 열면 웹 UI에 접속됩니다.
Fredy를 이해하는 열쇠는 딱 세 단어입니다. 이 셋의 조합이 Fredy의 전부라고 해도 됩니다.
| 개념 | 무엇인가 |
|---|---|
| Provider(공급자) | 매물을 가져오는 출처 사이트. ImmoScout24·Immowelt·Kleinanzeigen 등. 포털의 검색 URL(날짜순 정렬!)을 붙여넣어 등록 |
| Adapter(어댑터) | 새 매물을 알려주는 통로. Slack·Telegram·이메일 등. 여러 개를 동시에 쓸 수 있음 |
| Job(작업) | Provider들 + Adapter들을 묶은 한 묶음. "ImmoScout+Immowelt에서 찾아 Slack+Telegram으로 보내라"를 정해진 간격마다 자동 실행 |
여러 Job이 타이머에 따라 깨어나면, 각 Job은 자기가 가진 Provider들을 긁고, 중복을 걸러낸 뒤, 살아남은 새 매물만 Adapter들로 흘려보냅니다.
Fredy의 심장은 FredyPipelineExecutioner.js입니다. 매물은 이 컨베이어 벨트를 한 단계씩 통과하며 걸러지고 다듬어집니다. 코드의 execute()는 아래 단계를 .then()으로 줄세운 약속(Promise) 체인입니다.
@turf 라이브러리로 계산해, 원하는 동네만 남깁니다.코드를 열어보면 몇 가지 일관된 약속이 보입니다. 첫째, 각 Provider는 "데이터를 어떻게 긁고 정규화하나"만 책임지고, 흐름·순서·필터링은 전부 파이프라인이 담당합니다(관심사 분리). 그래서 새 포털을 추가할 때 lib/provider/에 파일 하나만 더하면 됩니다. 둘째, "새 매물 없음"을 에러가 아니라 신호로 취급합니다 — NoNewListingsWarning을 일부러 던져 파이프라인을 조용히 멈추죠. 셋째, 알림 어댑터도 똑같이 lib/notification/adapter/에 파일 하나 = 채널 하나로 격리돼, 새 알림 채널을 붙이기 쉽습니다.
| 위치 | 역할 |
|---|---|
| lib/provider/ | 이 프로젝트의 입구. 각 파일이 한 포털의 "긁기+정규화" 규칙 |
| lib/FredyPipelineExecutioner.js | 모든 Provider 결과를 13단계로 거르는 컨베이어 |
| lib/notification/adapter/ | 이 프로젝트의 출구. 각 파일이 한 알림 채널 |
| lib/services/extractor/ | 봇 탐지를 뚫는 CloakBrowser 추출 엔진 |
| lib/services/storage/migrations/ | DB 스키마 버전 관리(17개+ 마이그레이션) |
| lib/mcp/ | LLM이 매물 데이터를 자연어로 묻게 하는 다리 |
Fredy는 Provider도 Adapter도 폴더에 파일을 하나 더 넣으면 끝나도록 설계됐습니다. 핵심 파이프라인은 "어떤 Provider인지" 신경 쓰지 않고 정해진 인터페이스(normalize·filter·crawlFields 등)만 호출하죠. 새 포털·새 알림 채널을 추가할 때 기존 코드를 안 건드린다는 게 핵심입니다. 이게 "개방-폐쇄 원칙(OCP)"의 실전 예시입니다.
📖 한 줄 풀이: lib/provider/immowelt.js 하나를 열어보면, 한 포털을 Fredy에 가르치는 데 필요한 최소 규칙이 무엇인지 한눈에 보입니다.
13단계나 되는 처리를 거대한 함수 하나에 욱여넣으면 읽을 수 없습니다. Fredy는 각 단계를 작은 메서드로 쪼개고 .then()으로 연결해, 코드만 봐도 처리 순서가 그대로 읽히게 했습니다.
// 💡 하는 일: 매물을 13단계 컨베이어에 한 번 태운다 return Promise.resolve(urlModifier(url, sortByDate)) .then(this._getListings) // ② 추출 .then(this._normalize) // ③ 정규화 .then(this._filter) // ④ 1차 필터 .then(this._findNew) // ⑤ 새것만 .then(this._filterBySimilarListings) // ⑩ 중복 제거 .then(this._notify) // ⑬ 알림 .catch(this._handleError); // 에러는 한 곳에서
안심하세요 — 각 _단계는 입력을 받아 다음으로 넘길 값을 돌려주는 작은 함수일 뿐입니다. "데이터가 파이프를 따라 흐른다"는 그림이 코드에 그대로 박혀 있습니다.
실전 스크래핑의 8할은 "안 막히기"입니다. Fredy는 세 가지 무기를 씁니다 — ① CloakBrowser로 브라우저 지문을 진짜 크롬처럼 위장, ② ImmoScout24는 웹 대신 모바일 앱 API를 역설계해 우회, ③ 서버에서 막힐 땐 주거용 프록시로 가정집 IP인 척. "기술적으로 가능한가"와 "현실에서 안 막히나"는 다른 문제임을 배우는 좋은 교재입니다.
📖 한 줄 풀이: reverse-engineered-immoscout.md 문서가 "공식 API가 없을 때 모바일 앱 트래픽을 분석해 비공식 통로를 찾는" 과정을 보여줍니다.
Fredy는 매물 주소를 지오코딩으로 좌표화하고, 웹 UI에서 지도에 직접 그린 다각형(polygon) 안에 그 좌표가 들어오는지 @turf/boolean-point-in-polygon으로 판정합니다. "점이 다각형 내부인가"는 게임·지도·물류 어디서나 쓰이는 고전 알고리즘이라, 한 번 익혀두면 두고두고 씁니다.
V20+의 MCP 서버는 Fredy가 모은 매물 데이터를 LLM이 자연어로 질의할 수 있게 엽니다. "이번 주 새로 뜬 방 3개짜리 중 역세권인 거 추려줘" 같은 요청을 Claude·ChatGPT가 처리하죠. 자기 앱에 LLM 인터페이스를 붙이는 요즘 표준(MCP)을 실제 프로젝트에서 어떻게 쓰는지 볼 수 있습니다.
| 항목 | 요구사항 |
|---|---|
| 실행 방식 | Docker(권장) 또는 Node.js 직접 실행 |
| Node.js | 22 이상 (직접 실행 시) · 패키지 매니저 yarn |
| 메모리 | 헤드리스 크롬(CloakBrowser)이 도는 만큼 권장 1GB+ RAM |
| 저장공간 | 매물 DB는 SQLite 파일 한 개 — 가벼움(수십 MB) |
| 포트 | 9998 (웹 UI) 하나만 열면 됨 |
| 네트워크 | 가정용 인터넷이면 충분 / 서버·VPS면 독일(DE) 주거용 프록시 권장 |
| 플랫폼 | Linux·macOS·Windows·NAS(Unraid 커뮤니티 스토어 지원) |
Fredy는 "항상 켜둬야 의미 있는" 도구라, 24시간 도는 작은 컴퓨터에 두는 게 이상적입니다. 매일 끄는 노트북보다 라즈베리파이·홈 NAS·저렴한 VPS가 제격이죠. 다만 클라우드 서버(데이터센터 IP)에 두면 봇으로 의심받기 쉬우니, 그때는 "가정집 인터넷처럼 보이게 하는" 주거용 프록시라는 변장 도구를 하나 더 챙겨야 합니다 — 집에서 돌리면 이 변장이 필요 없습니다.
VPS에서 We have been detected as a bot :-/ 메시지가 뜨면 IP 평판 문제입니다. 데이터센터 IP는 지문을 아무리 완벽히 위장해도 평판만으로 차단됩니다. 해결은 독일 주거용(또는 모바일/4G) 프록시를 설정에 꽂는 것 — 일반 VPN·데이터센터 프록시는 도움이 안 됩니다(같은 나쁜 평판).
아래 한 줄로 Fredy를 띄우고 http://localhost:9998에 접속해보세요(기본 로그인 admin/admin). 먼저 공개 데모로 UI를 둘러본 뒤 내 것으로 옮기면 길을 잃지 않습니다.
# 💡 하는 일: Fredy 컨테이너를 띄우고 설정·DB 볼륨을 붙인다 docker run -d --name fredy \ -v fredy_conf:/conf \ -v fredy_db:/db \ -p 9998:9998 \ ghcr.io/orangecoding/fredy:master
안심하세요 — 위 명령은 복사만 하면 동작합니다. docker logs fredy -f로 무슨 일이 벌어지는지 실시간으로 볼 수 있습니다.
포털(예: WG-Gesucht)에서 원하는 조건으로 검색하고 "날짜순 정렬"로 맞춘 뒤 그 URL을 복사하세요. Fredy 웹 UI에서 Provider로 붙여넣고, Adapter로 Telegram이나 ntfy(가장 설정이 쉬움)를 골라 Job을 만들면, 다음 실행 때 새 매물이 알림으로 옵니다. "정렬을 날짜순으로" 안 하면 최신 매물을 놓치니 꼭 확인하세요.
웹 UI의 지도에서 원하는 동네를 다각형으로 그려 지역 필터를 걸어보세요. 그런 다음 코드에서 lib/FredyPipelineExecutioner.js의 _filterByArea를 읽어, 매물 좌표가 그 다각형 안인지 @turf로 어떻게 판정하는지 따라가 보면 "UI 조작 → 코드 동작"의 연결이 보입니다.
lib/notification/adapter/ntfy.js 같은 기존 어댑터 하나를 본보기로, 새 채널(예: 카카오워크·자체 웹훅)을 보내는 어댑터 파일을 하나 만들어 등록해보세요. 플러그인 구조 덕에 핵심 코드를 안 건드리고도 기능이 늘어나는 경험을 직접 할 수 있습니다.
yarn run test:offline은 실제 포털 대신 저장된 픽스처(과거 응답 스냅샷)로 핵심 로직을 빠르게 검증합니다. 포털이 HTML 구조를 바꾸면 스크래퍼가 깨지는데, yarn run test:download-fixtures로 새 픽스처를 받아 테스트를 갱신하는 흐름을 따라가 보면 "스크래퍼 유지보수"의 현실을 배웁니다.
| 주차 | 주제 | 학습 자료 / 키워드 |
|---|---|---|
| 1주 | Node.js 백엔드 기초 + 스케줄링 | Fastify 라우팅, node-cron, better-sqlite3, 환경변수·세션 |
| 2주 | 웹 스크래핑 & 봇 탐지 회피 | puppeteer-core, cheerio, 헤드리스 지문, 주거용 프록시, robots·법적 경계 |
| 3주 | React 풀스택 UI + 지도 | React 19, Semi UI, zustand, MapLibre·Turf 다각형 필터 |
| 4주 | 에이전트 인프라 & LLM 연동 | MCP 서버, 알림 어댑터 패턴, Docker 배포, 마이그레이션 |
이 4주는 "남이 만든 봇을 쓰는 사람"에서 "봇을 만드는 사람"으로 넘어가는 다리입니다. Fredy는 그 모든 조각(긁기·거르기·저장·알림·UI)이 한 저장소에 모여 있어, 큰 그림을 한 번에 볼 수 있는 흔치 않은 실전 교재입니다.
| 용어 | 의미 |
|---|---|
| Provider | 매물을 가져오는 출처 사이트(ImmoScout24·Immowelt 등). lib/provider/의 파일 하나 = 포털 하나 |
| Adapter | 새 매물을 알리는 통로(Slack·Telegram·이메일 등). lib/notification/adapter/의 파일 하나 = 채널 하나 |
| Job | Provider들 + Adapter들을 묶어 정해진 간격마다 실행하는 단위 |
| FredyPipelineExecutioner | 매물을 추출→정규화→필터→중복제거→알림까지 13단계로 처리하는 핵심 컨베이어 |
| CloakBrowser | 브라우저 지문을 진짜 크롬과 구별 불가능하게 위장한 하드닝 헤드리스 브라우저(npm 패키지) |
| 주거용 프록시 (Residential Proxy) | 실제 가정집 인터넷을 거쳐 요청을 보내, 데이터센터 IP 차단을 우회하는 유료 서비스 |
| 지오코딩 (Geocoding) | 주소를 위도·경도 좌표로 변환 — 거리·지역 필터의 전제 |
| 유사도 체크 (Similarity) | 제목·주소·가격이 닮은 매물을 같은 집으로 보고 중복 알림을 막는 캐시 |
| 다각형 필터 (Polygon / Turf) | 지도에 그린 영역 안에 매물 좌표가 있는지 판정해 동네를 한정 |
| MCP (Model Context Protocol) | LLM이 외부 기능·데이터를 "도구"로 부르는 표준. Fredy V20+가 매물 조회 도구를 제공 |
| Semi UI | ByteDance(틱톡)가 만든 React 컴포넌트 디자인 시스템. Fredy 웹 UI의 외형 |