sgkstudio.
엔지니어링

웹 스크래핑 URL 누락 — 범인은 하나가 아니라 셋이었다

「웹 스크래핑 URL 누락」의 원인은 수집기가 아니었습니다. 수집기는 7월 23일부터 원글 주소를 100% 담고 있었고, 주소가 사라진 자리는 그 뒤쪽 세 군데였습니다. 처음 잰 숫자는 후보 20건 중 자동 심사 통과 0건이었고, 세 군데를 다 막고 나서 2건이 됐습니다. 한 군데만 고쳤다면 숫자는 그대로였을 겁니다.

2026-09-07원본 저장소 전수 대조 — 수집기는 7월 23일부터 원글 주소를 100% 담고 있었다주제별 원글 주소 보유율 — 감시용 경로만 훑는 주제 4개 0%, 전체 주제 경로만 훑는 주제 100%, 두 경로가 겹치는 주제 3개 96~97%덮어쓰기 실측 — 1회 실행에 「홈페이지 제작」 157줄이 7줄로, 「AI 자동화」 96줄이 6줄로수정 전후 같은 명령 재실행 — 자동 심사 통과 0건에서 2건, 7점 이상 후보 주소 결측 8건 중 3건에서 1건, 채점 기준 묶음 재현율·정밀도 88%·88% 무변경잘못 붙은 주소 감사 — 조회 열쇠 7,318개 중 한 열쇠에 주소가 둘 이상 매달린 경우 0건

증상 — 후보 20건 중 자동 심사를 통과한 글이 0건이었다

우리는 스레드와 네이버 블로그·카페에서 「홈페이지 제작해 주실 분 계실까요」 같은 글을 자동으로 찾습니다. 찾은 글에는 점수를 매겨 줄을 세우고, 일정 점수를 넘긴 글에만 사람이 답글을 답니다. 그 사이에 자동 심사가 하나 있습니다. 원글 주소가 비어 있으면 점수가 아무리 높아도 거기서 떨어집니다. 답글을 달 자리가 없는 글은 후보가 아니기 때문입니다.

최근 14일치를 5점 이상으로 걸러 심사에 넣어 봤습니다. 후보는 20건이었고 통과한 글은 0건이었습니다. 점수가 낮아서가 아닙니다. 7점 이상인 스레드 후보 8건 가운데 3건이 원글 주소가 없다는 이유로 탈락했습니다. 비율로는 38%입니다.

처음 잰 숫자 세 칸 — 자동 심사 통과 후보 0/20, 7점 이상 후보 중 주소 결측 3/8(38%), 수집기 원글 주소 보유율 100%
처음 잰 숫자 — 통과는 0건인데 수집기 쪽 보유율은 100%였다

0건이라는 숫자는 파이프라인 뒤쪽 공정이 통째로 멈춰 있다는 뜻입니다. 답글을 자동으로 다는 코드도 이 숫자 때문에 손을 못 대고 있었습니다. 보낼 대상이 한 건도 없는 발송 코드는 짤 수가 없습니다. 그래서 이 한 칸을 채우는 일이 그 주의 가장 앞자리 작업이었습니다.

처음 의심한 것 — 「수집기가 원글 주소를 못 담는다」

처음 세운 가설은 짧았습니다. 수집기가 원글 주소를 못 담는다. 스레드는 로그인하지 않으면 화면에 내주는 정보가 줄어드는 곳이라, 주소를 뽑는 자리에서 실패했으리라고 봤습니다. 직전 기록에도 그렇게 적혀 있었습니다. 수집기가 주소를 못 담는 경로를 찾아 막는 게 다음 차례다.

그럴듯했습니다. 그럴듯한 가설이 위험한 이유는 확인을 건너뛰게 만들기 때문입니다. 이 가설대로라면 고칠 곳은 한 군데, 수집 코드의 주소 추출 부분입니다. 거기만 손보면 끝난다는 그림을 이미 머릿속에 그려 놓고 있었습니다.

결과부터 적으면 이 가설은 반쯤 틀렸습니다. 절반은 맞았고 절반은 완전히 빗나갔습니다. 그리고 빗나간 절반 쪽에 진짜 원인 두 개가 숨어 있었습니다. 한 군데만 고쳤다면 심사 통과 수는 0건에서 움직이지 않았을 겁니다.

그 가설을 무엇으로 확인했나

확인 수단은 두 가지를 잡았습니다. 성격이 다른 두 방향에서 같은 가설을 찔러 보려는 의도였습니다.

  • 첫째, 원본 저장소를 직접 셉니다. 우리는 긁어 온 글을 가공하기 전 상태 그대로 주제별·날짜별 파일에 쌓아 둡니다. 수집기가 주소를 못 담는다면 이 원본에도 주소 칸이 비어 있어야 합니다
  • 둘째, 주소 보유율을 주제별로 갈라 잽니다. 수집기는 검색 주제 단위로 돌고, 주제마다 어느 경로가 훑는지가 다릅니다. 원인이 코드 한 군데라면 보유율은 모든 주제에서 고르게 낮아야 합니다

두 번째 수단이 이 사건을 갈랐습니다. 전체 평균만 봤다면 「보유율이 낮다」에서 멈췄을 겁니다. 평균은 원인의 위치를 알려 주지 않습니다. 같은 값을 조건별로 쪼개면 값이 갈리는 자리가 곧 원인이 있는 자리입니다. 쪼개는 축은 우리가 이미 알고 있었습니다 — 어느 경로가 그 주제를 훑느냐입니다.

알리바이 — 수집기는 7월 23일부터 100% 담고 있었다

원본 저장소를 열었습니다. 수집기는 7월 23일부터 원글 주소를 100% 담고 있었습니다. 한 건도 빠뜨리지 않았습니다. 첫 가설의 절반이 여기서 깨졌습니다. 주소를 못 담아서가 아니라, 담아 둔 주소가 뒤쪽 어딘가에서 사라지고 있었습니다.

수집기가 주소를 못 담는다 — 이 문장이 참이었다면 원본 파일의 주소 칸이 비어 있어야 합니다. 비어 있지 않았습니다. 그래서 범인은 수집 이후 공정에 있습니다.

주제별 보유율은 더 선명했습니다. 15분마다 도는 감시용 경로만 훑는 주제 4개(랜딩페이지·웹사이트 제작·외주·창업)는 0%였습니다. 전체 주제를 한 번에 도는 경로만 훑는 주제는 100%였습니다. 두 경로가 겹치는 주제 3개는 96~97%였고, 모자란 3~4%가 정확히 감시용 경로가 가져온 몫이었습니다.

주제별 원글 주소 보유율 막대 — 감시용 경로만 훑는 주제 4개 0%, 두 경로가 겹치는 주제 3개 96~97%, 전체 주제 경로만 훑는 주제 100%, 조건문 수정 후 감시용 경로 100%
값이 0%와 100%로 갈렸다 — 원인은 코드가 아니라 부르는 쪽에 있었다

숫자가 이렇게 갈리면 원인은 코드 내용이 아니라 호출 방식입니다. 같은 파일, 같은 함수인데 부르는 쪽에 따라 결과가 0%와 100%로 갈렸습니다. 이 시점에 남은 질문은 하나로 좁혀집니다. 두 경로가 그 함수를 어떻게 다르게 부르는가.

진짜 원인 하나 — 로그인 여부로 막아 둔 조건문이 주소를 통째로 버렸다

주소를 뽑는 코드는 로그인 여부를 묻는 조건문 안에 들어가 있었습니다. 그런데 그 안의 내용은 로그인과 아무 상관이 없었습니다. 화면 구조에서 주소 한 칸을 읽어 오는 평범한 조회였습니다. 로그인한 경우에만 뽑을 이유가 처음부터 없었습니다.

이 조건문 하나가 경로를 갈랐습니다. 15분마다 도는 감시용 스크립트는 로그인 없이 부릅니다. 그래서 이 스크립트는 매번 조건문 밖으로 떨어져 글자만 긁는 대체 경로로 갔고, 그 대체 경로는 주소 칸에 빈 문자열을 넣습니다. 오류가 나지 않습니다. 빈 값이 조용히 채워집니다.

무음 결함이 이래서 오래 삽니다. 예외가 터졌다면 그날 바로 봤을 겁니다. 빈 문자열은 정상값처럼 생겼기 때문에 뒤쪽 공정까지 그대로 흘러갑니다. 그러다 자동 심사가 「주소 없음」으로 떨어뜨릴 때에야 겨우 모습을 드러냅니다. 그것도 원인이 아니라 증상의 얼굴로 나타납니다.

고치는 일 자체는 조건문 밖으로 한 블록을 꺼내는 데서 끝났습니다. 진단에 걸린 시간과 수정에 걸린 시간의 차이가 이 사건의 성격을 말해 줍니다.

진짜 원인 둘 — 같은 글이 왜 시점마다 다른 글이 됐나

조건문을 고치자 새로 긁는 글에는 주소가 붙었습니다. 그런데 이미 쌓아 둔 글에서 주소를 찾아오는 조회가 여전히 실패했습니다. 어떤 계정 한 곳의 주소는 원본 저장소에 8월 내내 멀쩡히 있었는데도, 후보 목록 쪽에서는 계속 주소 없음이었습니다.

조회는 열쇠로 합니다. 우리가 쓰던 열쇠는 계정 이름과 본문 앞 40자를 묶은 한 쌍이었습니다. 문제는 긁어 온 글의 본문이 작성자·날짜·더보기·본문이 한 덩어리로 붙어 있다는 데 있었습니다. 앞 40자를 자르면 그 안에 날짜가 들어갑니다.

그리고 스레드는 같은 글의 날짜를 볼 때마다 다르게 찍습니다. 갓 올라온 글에는 3일이라고 쓰고, 시간이 지나면 2026-07-13처럼 절대 날짜로 바꿉니다. 열쇠 안에 이 값이 들어 있으니 같은 글이 시점마다 다른 열쇠가 됐습니다. 저장할 때 만든 열쇠와 찾을 때 만든 열쇠가 서로 다른 글을 가리킨 셈입니다.

같은 글 한 건이 시점에 따라 두 열쇠로 갈리는 흐름도 — 갓 올라왔을 때는 본문 앞 40자에 3일, 며칠 뒤에는 2026-07-13이 들어가 열쇠가 달라지고 조회가 실패한다
변하는 값이 식별자 안에 있으면 같은 글이 시점마다 다른 글이 된다

고친 방법은 열쇠를 하나 더 잡는 것이었습니다. 더보기 뒤의 본문만 떼어 두 번째 열쇠로 쓰면 날짜가 열쇠 밖으로 빠집니다. 기존 열쇠도 지우지 않고 그대로 두었습니다. 예전에 쌓아 둔 글은 옛 열쇠로만 찾히기 때문입니다.

진짜 원인 셋 — 찾다가 나온 데이터 손실, 15분마다 그날치를 지우고 있었다

여기까지 파는 동안 훨씬 나쁜 것이 하나 나왔습니다. 원본 파일이 15분마다 지워지고 있었습니다. 찾던 문제와 다른 사고인데, 같은 경로를 따라가다 걸렸습니다.

원본은 주제와 날짜로 갈라 한 파일에 쌓습니다. 스레드·네이버 블로그·카페가 한 파일에 섞여 누적되는 구조입니다. 전체 주제를 도는 경로는 이 파일에 이어 붙이고 중복만 걸러 냈습니다. 그런데 주제 하나만 도는 경로는 같은 파일을 쓰기 모드로 열었습니다. 이어 붙이는 것이 아니라 새로 쓰는 동작입니다.

15분마다 도는 감시용 작업이 하필 그 경로를 주제 8개마다 부릅니다. 한 번 도는 것만으로 그날 쌓아 둔 다른 출처의 글이 전부 날아갔습니다. 실측하면 「홈페이지 제작」 주제 파일이 157줄에서 7줄로 줄었고, 사라진 150건은 전부 네이버에서 온 글이었습니다. 「AI 자동화」 주제는 96줄에서 6줄이었습니다.

덮어쓰기 1회 실행 전후 원본 파일 줄 수 막대 — 홈페이지 제작 157줄에서 7줄, AI 자동화 96줄에서 6줄
찾던 문제보다 이쪽이 더 급했다 — 한 번 도는 것만으로 그날 원본이 사라졌다

지워진 분량은 다른 저장 경로에 남아 있던 사본을 병합해 되살렸습니다. 피해 구간은 이 감시용 작업을 등록한 당일 하루로 한정합니다. 다만 되살리지 못한 것이 있습니다. 덮어쓰기가 지운 회차와 그다음 회차 사이에 올라온 글은 어디에도 남지 않았습니다. 하루치 일부는 영구 소실로 적었습니다.

여러 작업이 같은 파일을 동시에 만지다 기록이 사라진 다른 사례는 Claude Code 병렬 세션의 기록 유실에 적어 두었습니다. 쌓아 두는 파일에 쓰기 모드로 접근하는 코드는 그 자체로 점검 대상입니다. 두 사건의 공통점이 거기 있습니다.

딸려 나온 것 — 처리한 상대가 다시 알림에 뜬 이유는 무엇이었나

같은 병이 한 군데 더 있었습니다. 이미 답글을 단 상대가 다음 회차에 새 후보로 다시 뜨는 문제입니다. 열쇠 이야기를 하고 나니 원인이 바로 보였습니다.

우리는 처리한 글에 지문을 찍어 둡니다. 지문은 원문 앞 80자를 해시로 줄인 값이었습니다. 앞 80자 안에는 날짜가 들어가고, 꼬리에는 좋아요와 댓글 수가 붙습니다. 둘 다 시간이 지나면 바뀌는 값입니다. 조회 열쇠와 정확히 같은 병입니다 — 변하는 값을 식별자에 넣었습니다.

그래서 꼬리의 참여 수를 떼어 내고 본문과 작성자만 묶어 해시를 만들도록 바꿨습니다. 여기에 한 가지를 더 붙였습니다. 지문 만드는 방식을 바꾸면 예전에 찍어 둔 지문이 전부 무효가 되고, 이미 처리한 글이 한꺼번에 새 글로 되살아납니다. 그래서 옛 지문으로도 한 번 더 조회하고, 조회에 성공하면 새 방식으로 옮겨 적는 절차를 같이 넣었습니다.

식별 방식을 바꾸는 작업에는 이 이사 절차가 거의 항상 따라붙습니다. 방식만 바꾸고 이사를 빼면, 고친 그날 밤에 지금까지 처리한 모든 건이 새 알림으로 쏟아집니다.

고친 방법과 다시 잰 숫자

네 군데를 고쳤습니다. 로그인 조건문에서 주소 추출을 꺼냈고, 파일을 쓰기 모드로 열던 자리를 이어 붙이기로 바꿨으며, 조회 열쇠를 하나 더 잡았고, 지문에서 변하는 값을 뺐습니다. 그리고 처음 문제를 잰 명령을 그대로 다시 돌렸습니다. 같은 명령이어야 비교가 됩니다.

수정 전후 — 같은 명령으로 다시 잰 값
측정 항목이전이후
7점 이상 스레드 후보의 원글 주소 결측8건 중 3건 (38%)8건 중 1건 (12%)
자동 심사 통과 (최근 14일·5점 이상)20건 중 0건20건 중 2건
감시용 경로 주제의 원글 주소 보유율0%100%
채점 기준 묶음 재현율·정밀도88% · 88%88% · 88%

마지막 줄이 안 움직였다는 사실도 결과입니다. 주소를 채우는 작업이 점수 판정을 건드리지 않았다는 뜻입니다.

수정 전후 표 — 주소 결측 8건 중 3건에서 1건, 자동 심사 통과 20건 중 0건에서 2건, 감시용 경로 주제 보유율 0%에서 100%, 채점 재현율·정밀도 88%·88% 무변경
세 줄이 움직이고 한 줄이 안 움직인 것까지가 이번 수정의 결과다

자동 심사 통과는 0건에서 2건으로 올라왔습니다. 9점짜리 한 건과 8점짜리 한 건입니다. 감시용 경로 주제의 주소 보유율은 0%에서 100%가 됐고, 7점 이상 후보의 주소 결측은 8건 중 3건에서 8건 중 1건으로 줄었습니다.

같이 확인한 숫자가 하나 더 있습니다. 채점 정확도를 재는 기준 묶음의 재현율과 정밀도는 88%와 88%로 한 칸도 움직이지 않았습니다. 이 값이 그대로라는 사실이 중요합니다. 고치다 보면 옆의 것이 같이 움직이는데, 안 움직였음을 숫자로 확인해 두지 않으면 나중에 생긴 변화를 전부 이번 수정 탓으로 돌리게 됩니다.

같은 실수를 막는 장치

세 개를 붙였습니다. 고친 자리마다 하나씩입니다.

  • 잘못 붙은 주소를 세는 감사. 조회 열쇠 7,318개를 전수로 훑어 한 열쇠에 주소가 둘 이상 매달린 경우를 셌습니다. 0건이었습니다. 이 값이 0이 아니면 엉뚱한 사람의 글에 답글이 갑니다
  • 재현 명령을 기록에 그대로 남기기. 수정 전후에 같은 명령을 돌려야 숫자를 비교할 수 있습니다. 지문 검사와 심사 자체 점검은 각각 명령 한 줄로 돌아갑니다
  • 회귀 테스트. 숫자로 확인한 사실을 테스트에 박아 둡니다. 며칠 뒤 같은 파이프라인에서 다른 오탐이 났을 때도 이 방식을 그대로 썼습니다

세 번째 것을 조금 더 적습니다. 어느 날 아침 알림에 프리랜서의 자기 광고 글이 후보로 올라왔습니다. 「필요한 분 있어?」라는 말투를 사는 사람의 문장으로 읽은 겁니다. 쌓아 둔 원본 전체를 그 어순으로 훑어 19건을 찾았는데, 19건 전부 파는 쪽이 쓴 글이었고 사는 쪽이 쓴 사례는 0건이었습니다.

그래서 그 어순 하나만 규칙에 추가하고, 잘못 잡힌 3건과 제대로 잡아야 하는 2건을 합쳐 5건을 테스트로 고정했습니다. 넓게 잡지 않고 실측으로 확인한 문법만 좁게 막았습니다. 다음 오탐이 다른 말투라면 그때 그 말투를 다시 실측합니다.

고치고 끝내면 같은 자리가 몇 달 뒤에 다시 열립니다. 숫자로 확인한 사실을 테스트로 박아 두는 일이 이런 사건에서 유일하게 남는 자산입니다.

고치지 않기로 한 것

남겨 둔 것도 적습니다. 세 가지입니다.

  • 한 계정은 여전히 주소가 없습니다. 원본 어디에도 없습니다. 대체 경로로만 긁던 7월 10일부터 14일 사이에 수집한 글이기 때문입니다. 작성자와 본문으로 추측해 채울 수는 있지만 하지 않았습니다. 같은 사람의 다른 글에 답글을 다는 것이 이 파이프라인에서 가장 나쁜 실수라서입니다
  • 덮어쓰기가 지운 회차의 글은 되찾지 못합니다. 하루치 일부는 영구 소실로 남겼습니다
  • 지문 방식을 바꾼 여파로 한 계정이 알림에 한 번 더 뜹니다. 대신 그 알림에 이미 2회 접촉한 상대라는 표시를 붙여 사람이 보고 거르게 했습니다

정리하면 이렇습니다. 수집기가 주소를 못 담는다는 첫 가설은 반쯤 틀렸고, 수집기는 7월 23일부터 100% 담고 있었습니다. 주소는 로그인 조건문·조회 열쇠·파일 덮어쓰기 세 군데에서 따로 사라졌고, 셋을 다 막고서야 자동 심사 통과가 0건에서 2건이 됐습니다.

모아 온 데이터에서 한 칸이 자꾸 빈다면, 그 칸을 채우는 코드보다 먼저 값이 갈리는 조건을 찾는 편이 빠릅니다. 조건별로 쪼갠 숫자가 0%와 100%로 갈리면 원인은 이미 그 자리에 있습니다. 회사 업무에 이런 수집·자동화 파이프라인을 얹는 문제는 상담으로 문의 주시면 어디부터 봐야 하는지 같이 봅니다. 이 글은 SGK Studio가 썼습니다.

같은 방식으로 우리 업무를 재보고 싶다면

이 글에 쓴 계측·판정·자동화는 저희가 실제로 매일 돌리는 방식 그대로입니다. 어떤 작업을 자동화할 수 있고 어디를 사람이 잡아야 하는지, 현재 업무 흐름을 놓고 같이 짚어 드립니다.

새 글이 올라오면 알려 드립니다

이런 계측 기록을 새로 쓰면 메일로 한 통 보냅니다. 광고나 판촉은 보내지 않습니다.