| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 배포
- contextBridge
- 온디바이스AI
- github actions
- 마이그레이션
- REST API
- Kotlin
- typescript
- playwright
- php
- JWT
- 보안
- 백엔드
- React
- 크롤링
- 아키텍처
- sqlite
- 인증
- next.js
- electron
- 도메인설계
- Vite
- 상태관리
- javascript
- FastAPI
- Android
- 소셜로그인
- 메일자동화
- 프론트엔드
- python
- Today
- Total
HM소프트
크롤러 데이터 검증·검색·스케줄러·데스크톱앱화 — 공개 데이터 크롤러 (4) 본문

오번역을 막는 번역 필요 여부 가드와 스레드 로컬 번역기 격리, 429 분기 백오프, 원문·번역 칸을 모두 훑는 다필드 LIKE 검색과 이스케이프, 화면과 일치하는 내보내기, 국가별 시각 분산과 직렬화 락 스케줄러, APScheduler 잠자기 방어, UNIQUE 마이그레이션, Electron 포트 폴백·트레이 상주까지 데스크톱 앱화 기록.
4편: 검색·번역·스케줄러·데스크톱
시리즈 마지막이다. 1편 파이프라인, 2편 PDF 파싱, 3편 강건성에 이어, 이번엔 사용자가 실제로 만지는 부분과 운영 자동화를 한 번에 다룬다.
번역 — "되게"보다 "틀리지 않게"
수집한 외국어 원문은 googletrans로 번역했다. 네트워크 작업이라 실패가 잦아, 재시도와 백오프를 넣었다.
def translate_with_retry(text, retries=3):
for i in range(retries):
try:
return translator.translate(text, dest="ko").text
except Exception:
time.sleep(2 ** i) # 1s, 2s, 4s 백오프
return text # 끝내 실패하면 원문 유지
그런데 실무에서 진짜 문제는 번역이 안 되는 것이 아니라 엉뚱하게 되는 것이었다. 대표적으로 짧은 영문 브랜드명이 일반 단어로 오역됐다(예: 짧은 영문 토큰을 뜻 있는 단어로 바꿔버림). 고유명사가 번역되면 데이터가 오염된다.
그래서 번역 차단 가드를 넣었다. 짧은 영문 토큰·브랜드명으로 판단되는 값은 번역 대상에서 빼고 원형을 유지했다.
def should_skip_translation(s: str) -> bool:
t = s.strip()
if len(t) <= 3 and t.isascii(): # 짧은 영문 약어/브랜드
return True
if t.isupper() and t.isascii(): # 대문자 약어
return True
return False
19개 필드 검색
검색은 19개 항목을 대상으로 한다. 처음엔 제목 위주였는데, 고객 피드백을 받아 실제로 찾고 싶어 하는 항목들을 키워드 검색에 포함시켰다. 결함 원인(defect_cause), 영향(consequence), 조치(remedy), 부품(component), 리콜 번호(recall_number) 등이다.
검색은 선택된 필터(기간·국가 등) 조건과 결합되며, 결과는 화면 표시와 내보내기에서 동일하게 적용된다.
데이터 정합성: DB UNIQUE 마이그레이션
자동 수집을 매일 돌리면 같은 데이터가 반복 수집된다. 3편에서 "중복은 시각이 아닌 고유 키로 판정"한다고 했는데, 그 고유 키에 DB UNIQUE 제약을 걸어 중복 저장을 원천 차단했다. 기존 DB에는 마이그레이션으로 적용했고, 개발 중 데이터를 갈아엎을 수 있도록 reset 엔드포인트도 뒀다.
저장은 INSERT ... ON CONFLICT로 처리해, 새 데이터는 넣고 기존 데이터는 갱신하는 upsert가 되게 했다.
내보내기 — 필터를 그대로 반영
CSV/Excel 내보내기에서 중요한 건, 화면에서 좁혀 본 결과와 내려받은 파일이 같아야 한다는 점이다. 그래서 export가 현재 적용된 필터 조건을 그대로 반영하게 했다. 번역본이 export에서 누락되던 버그도 함께 잡았다.
자동 스케줄러 — APScheduler
운영은 매일 자동으로 돌아야 한다. APScheduler로 일일 수집을 걸되, 핵심은 국가별로 수집 시각을 분산한 것이다. 모든 나라를 같은 시각에 한꺼번에 긁으면 부하가 몰리고, 한 곳이 막히면 줄줄이 밀린다.
scheduler = BackgroundScheduler(timezone="UTC")
for i, country in enumerate(COUNTRIES):
scheduler.add_job(
crawl_country, "cron", args=[country],
hour=2, minute=i * 10, # 나라마다 10분씩 어긋나게
)
데스크톱 앱으로 마무리 — Electron + React
상시 도는 도구라 데스크톱 경험을 챙겼다. 백엔드(FastAPI)는 로컬에서 돌고, 그 위에 Electron + React로 5개 화면과 트레이를 올렸다.
- 트레이 상주 + 알림: 백그라운드로 돌며 수집 결과를 트레이 알림으로.
- 윈도우 자동 시작: 부팅 시 자동 실행.
- 즉시 수집: 스케줄을 기다리지 않고 바로 한 번 돌리는 버튼.
- 반응형 UI, 날짜 포맷, 전각 정규화(3편), 에러 처리 정리.
프론트엔드는 API 클라이언트로 백엔드와만 통신한다. "수집·파싱·번역은 백엔드, 화면은 프론트"라는 경계를 끝까지 지켜, 한 대의 PC에서 도는 자족적인 앱이 됐다.
시리즈를 마치며
네 편을 관통한 교훈은 "크롤링의 본질은 HTTP가 아니라 데이터의 비일관성과 싸우는 일"이라는 점이다.
- 1편: 다양한 소스를 추상 클래스로 흡수
- 2편: 복잡한 포맷은 흐름이 아닌 구조로 파싱
- 3편: 환경 차이를 명시적으로 못박아 재현 안 되는 버그 제거
- 4편: 번역은 틀리지 않게, 중복은 키로 차단, 운영은 분산 스케줄로
수집 대상이 늘어도 베이스 크롤러 하나만 더 얹으면 되고, 데이터 품질을 지키는 장치들이 파이프라인에 녹아 있어 확장이 두렵지 않은 구조가 된 점이 가장 만족스러웠다.
'기술 개발일지' 카테고리의 다른 글
| 대량 메일 자동화 개발기 (2) — 수신자별 PDF 첨부를 단계별로 쌓기 (0) | 2026.06.11 |
|---|---|
| 대량 메일 템플릿 변수 치환 + 값 미리보기 엔진 (1) (0) | 2026.06.11 |
| cross-PC 강건성 잡기 — '내 PC에서는 되는데' 크롤러 (3) (0) | 2026.06.11 |
| 외국어 PDF 표 구조 파싱하기 — 공개 데이터 크롤러 (2) (0) | 2026.06.11 |
| 공개 데이터 크롤러 파이프라인 설계 — BaseCrawler로 5개국 출처를 하나로 통합하기 (1) (0) | 2026.06.11 |