이상욱

어려운 것이 아니라, 아직 안 해본 것뿐입니다.

에어소프트건 직구 가격 비교 서비스

개인 프로젝트 · 2026.07 ~ 운영 중 · Node.js, JavaScript, Cloudflare Pages, Windows 작업 스케줄러, Google Apps Script · sopnawa.pages.dev

19개해외 샵
약 14만수집 상품
하루 2회무인 크롤·배포
누적 약 7,700회방문

만든 이유

샵이 여러 곳이라 가격을 비교하려면 창을 여러 개 띄워야 했고, 샵마다 상품명도 달라서 같은 물건인지 확인하기 어려웠습니다. 영어 검색이 어려워 직구를 포기하는 지인들도 있었습니다. 그래서 수집을 허용한 해외 샵들의 상품을 한곳에 모으고, 배송비·관세·검사비까지 더한 예상 도착가를 보여주는 사이트를 만들었습니다.

구조

[Windows 작업 스케줄러] 04:00 / 18:30
        │
        ▼
  샵별 수집기 (Shopify 공용 / 샵 전용)
        │  점증 백오프 재시도, 요청 간격·상한
        ▼
  병합 전 검증 ── 직전 대비 80% 미만이면 덮어쓰기 취소
        │
        ▼
  매칭·가격 이력 생성 → 정적 JSON
        │
        ▼
  Cloudflare Pages 배포 → 배포 후 라이브 확인 → 실패 시 Discord 알림

[감시 창]  마지막 성공 후 17시간 넘으면 자동 재크롤 (self-heal)
[Google Apps Script]  PC 외부에서 1시간마다 점검, 20시간 미갱신 시 Discord 알림

문제와 해결

1. 샵마다 구조가 전부 다름

처음 생각

샵별로 수집기를 하나씩 만들려고 했습니다.

해결

각 샵의 구조를 파악해서, 같은 방식(Shopify 등)은 공용 수집기로 묶고 구조가 다른 샵은 전용 수집기를 만들었습니다.

→ 7종 플랫폼과 자체 구축 2곳, 19개 샵 약 14만 상품을 하나의 데이터로 통합

2. 봇 차단과 요청 제한

처음 생각

간격을 두고 요청을 계속 보내면서, 얼마나 많이 요청하면 제한이 걸리고 제한이 얼마나 오래가는지 알아보려 했습니다.

해결

요청 사이에 딜레이를 두고 순차로 보내며 상한을 걸었습니다. 그래도 429(요청 제한)나 5xx(서버 오류)가 오면 15초, 30초, 45초로 대기 시간을 늘려가며 최대 3번 다시 요청했습니다. 403이나 404처럼 다시 보내도 결과가 같은 응답은 재시도하지 않고 바로 중단했습니다.

async function fetchPage(url) {
  let res = await fetch(url, { headers: { 'User-Agent': UA } });
  // 429·5xx만 재시도. 403·404는 다시 보내도 같으므로 바로 반환
  for (let attempt = 1; attempt <= 3 && (res.status === 429 || res.status >= 500); attempt++) {
    const wait = 15000 * attempt;   // 15s → 30s → 45s
    await sleep(wait);
    res = await fetch(url, { headers: { 'User-Agent': UA } });
  }
  return res;   // 실패면 호출부가 중단을 판단하고, 3번의 안전장치가 기존 데이터를 지킴
}

3. 부분 수집으로 인한 데이터 훼손 위험

처음 생각

봇 차단으로 수집이 중간에 끊기는 걸 겪으면서, 그 상태로 저장되면 멀쩡한 데이터가 덮어써질 수 있다고 판단했습니다. 실제로 훼손된 적은 없고, 설계 단계에서 미리 막아두기로 했습니다.

해결

데이터를 합치기 전에 직전 수집량과 비교해서, 80% 미만이면 덮어쓰기를 취소하고 기존 데이터를 유지했습니다. 1,000개 중 100개 정도는 단종이나 매진으로 자연스럽게 빠질 수 있어 기준을 80%로 잡았습니다.

→ 차단된 회차에도 데이터 무손실, 다음 정상 회차에 자동 복구

4. 일부 상품이 수집되지 않음 (페이지네이션 함정)

처음 생각

크롤링을 해도 수집되지 않는 상품이 있어서 추적해보니, 한 샵에서 약 1만 개 중 211개만 수집되고 멈추고 있었습니다. 처음에는 요청한 개수(250개)보다 적게 오면 마지막 페이지라고 판단했습니다.

// 처음 종료 조건
if (batch.length < LIMIT) break;   // 250개보다 적게 오면 마지막 페이지
해결

그 샵은 250개를 요청해도 페이지마다 211개, 235개처럼 들쭉날쭉하게 응답했고, 첫 페이지부터 마지막으로 오판해 멈춘 것이었습니다. 그래서 "적게 오면 끝"이라는 판단을 버리고, 빈 페이지가 오거나 받아온 상품이 전부 이미 본 것일 때만 멈추도록 바꿨습니다. 상품은 id로 중복을 제거했습니다.

// 바꾼 종료 조건
if (batch.length === 0) break;                  // 빈 페이지면 끝
if (batch.every(p => seen.has(p.id))) break;    // 전부 이미 본 상품이면 끝
// seen은 Set — 상품이 1만 개를 넘어도 확인 비용이 늘지 않음

→ 211개 → 약 1만 개 수집

5. 조용한 배포 실패

처음 생각

데이터 수신이 원활하지 않은 곳에서 휴대폰으로 사이트를 보다가, 날짜가 최신으로 바뀌지 않은 것을 보고 배포가 실패하고 있었다는 걸 알았습니다. 실패해도 알림이 없어서 우연히 보기 전까지 몰랐습니다.

해결

배포 직전에 토큰을 자동 갱신하고, 배포 후 실제 사이트에 반영됐는지 확인하며, 실패하면 Discord로 알림이 오도록 했습니다.

→ 배포 실패를 우연히 발견하는 대신 알림으로 바로 인지

6. 감시 프로세스의 단일 장애점

처음 생각

감시 프로세스를 작업 창의 백그라운드로 띄워두면 충분하다고 생각했습니다. 그런데 7월 25일 감시가 스스로 종료된 뒤 약 17시간 동안 데이터가 갱신되지 않는 것을 알아채지 못했습니다. 감시가 로그온 세션에 붙어 있어서, 세션이 내려가면 감시도 함께 내려가는 구조였습니다.

해결

감시를 세션과 무관한 독립 실행으로 분리하고, 마지막 크롤 완료 후 17시간 안에 성공 기록이 없으면 스스로 다시 크롤하는 self-heal을 넣었습니다. 처음에는 14시간이었는데, 정상 운영 중 최대 간격이 약 16시간이라 매일 저녁 오작동 직전까지 가 있던 것을 발견하고 17시간으로 고쳤습니다.

PC가 통째로 꺼지면 PC 안의 감시도 함께 멈추기 때문에, PC 바깥의 Google Apps Script가 1시간마다 점검해 데이터가 20시간 넘게 바뀌지 않으면 Discord로 알리도록 했습니다. 20시간은 가장 긴 크롤 간격(14시간 30분)에 크롤 소요 시간을 더해도 오탐이 나지 않도록 잡은 값입니다. 운영 중인 PC를 끌 수 없어서, 기준을 1시간으로 낮춰 알림이 오는 것을 확인한 뒤 되돌렸습니다.

→ 17시간 자동 복구 → 20시간 사람 호출의 단계형 대응

장애 분석 보고서
7월 25일 크롤 자동화 중단 장애 분석 보고서 (작성 당시 기준이며, 이후 self-heal 기준을 14시간에서 17시간으로 고치고 외부 감시와 Discord 알림을 추가했습니다)

사용자 반응과 반영

에어소프트 커뮤니티(구독자 약 2만 4천 명)에 사이트를 공개하고, 업데이트마다 글을 올려 의견을 받았습니다.

9개공개 글
약 1.1만 회누적 조회
203첫 글 추천
커뮤니티 첫 소개 글
첫 소개 글 (2026.07, 조회 2,789 · 추천 203 · 댓글 98)
Cloudflare Web Analytics
Cloudflare Web Analytics, 최근 30일 sopnawa.pages.dev 방문 약 1,800회 (Cloudflare 봇 필터 적용, 2026.10.20 기준). 7월 13일 공개 이후 누적 약 7,700회입니다. 분석 항목 이름은 이전 주소로 남아 있습니다.

다시 만든다면

TarGila — 게임 퀘스트·아이템 체크리스트 크롬 확장

개인 프로젝트 · 2026.07 ~ 운영 중 · TypeScript, WXT, Chrome Extension MV3 · 크롬 웹스토어 활성 사용자 214명, 평점 5.0

화면은 AI 코딩 도구로 빠르게 만들고, 데이터 계층을 직접 설계했습니다.