LH 공고 첨부를 받는 명령이 '목록에 없다'고 했다 — 원인은 첫 쪽만 읽던 조회 코드였다

·

청약 공고를 모으는 내 스크립트에서 공고문 PDF를 받는 명령이 실패했다. 메시지는 **“목록 첫 페이지에 없다”**였다. 같은 공고는 목록 조회 명령에는 분명히 떴다. 원인은 id 오타도, 마감된 공고도 아니었다. 첨부 다운로드 코드가 게시판의 1쪽만 읽고 있었다.

처음 본 것

2026년 10월 7일, 아산 국민임대 예비입주자 모집공고(공고일 2026.09.15)의 원문을 받으려고 했다.

npm run notice -- --fetch lh:2015122300020726
# ❌ lh:2015122300020726 가 LH 공고 목록 첫 페이지에 없다 …

공고 id는 목록 조회 명령(npm run notice)이 출력한 값을 그대로 붙여 넣은 것이다. 같은 id가 한쪽에서는 보이고 다른 쪽에서는 없다는 말이다. id를 의심하기 전에 두 명령이 같은 곳을 읽는지부터 확인해야 했다.

게시판은 몇 쪽인가

두 명령 모두 LH 청약플러스의 임대 공고 게시판(mi=1026)을 읽는다. 쪽별로 직접 요청해서 <tr> 개수를 세어 봤다. 아래 값은 2026년 10월 7일에 이 명령을 그대로 돌려 얻은 것이다.

U='https://apply.lh.or.kr/lhapply/apply/wt/wrtanc/selectWrtancList.do?mi=1026'
UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
for p in 1 2 3 4 5; do
  curl -s -A "$UA" "$U&currPage=$p" -o /tmp/lh$p.html
  printf 'page %s  tr=%s  hash=%s\n' $p \
    "$(grep -o '<tr>' /tmp/lh$p.html | wc -l | tr -d ' ')" \
    "$(grep -o 'data-id1="[^"]*"' /tmp/lh$p.html | md5 | cut -c1-8)"
done
page 1  tr=51  hash=26f51caf
page 2  tr=14  hash=6c62d3a9
page 3  tr=14  hash=6c62d3a9
page 4  tr=14  hash=6c62d3a9
page 5  tr=14  hash=6c62d3a9

위 명령을 그대로 붙여 넣으면 재현된다. 다만 공고가 새로 올라오거나 내려가면 행 수는 달라질 수 있다.

제목줄을 빼면 1쪽은 50건, 2쪽은 13건이다. 그리고 2쪽부터 5쪽까지 해시가 전부 같다. 범위를 넘은 쪽 번호를 주면 서버는 빈 목록이 아니라 마지막 쪽을 그대로 다시 준다.

찾던 공고가 어디 있는지도 같은 파일에서 확인했다.

grep -c 'data-id1="2015122300020726"' /tmp/lh1.html   # 0
grep -c 'data-id1="2015122300020726"' /tmp/lh2.html   # 1

1쪽에는 0건, 2쪽에는 1건이다. 1쪽만 읽는 코드에게 이 공고는 존재하지 않는다. 메시지의 “목록 첫 페이지에 없다”는 문장은 사실이었지만, “그러니 id가 틀렸다”는 방향으로 읽게 만드는 진단이었다.

같은 게시판을 읽는 코드가 두 곳이었다

원인은 코드 구조에 있었다. 이 스크립트에는 LH 게시판을 읽는 곳이 두 군데 있다.

함수용도읽는 범위
목록 조회접수 중인 공고 후보 찾기최대 4쪽, 같은 행이 다시 나오면 중단
첨부 다운로드공고 id로 공고문 PDF 받기1쪽만

목록 조회는 10월 2일에 2쪽 이후 공고를 놓친다는 걸 알고 4쪽까지 읽게 고쳤다. 첨부 다운로드는 첨부 파일 메타 요청에 필요한 코드(data-id2~4)를 목록 HTML에서 찾는 부분이 따로 구현되어 있었고, 거기는 그대로였다. 같은 게시판을 같은 방식으로 읽어야 하는 두 곳에서 규칙이 어긋났다.

고친 것

첨부 다운로드도 목록 조회와 같이 쪽을 넘기며 찾도록 바꿨다. 임대 게시판에서 못 찾으면 분양 게시판(mi=1027)도 본다.

let codes = null;
for (const list of [LH_LIST, LH_SALE_LIST]) {
  for (let page = 1; page <= 4 && !codes; page++) {
    codes = parseLhRowCodes(await (await get(`${list}&currPage=${page}`)).text(), panId);
  }
}

쪽 번호에 상한(4)을 둔 것은 위에서 본 반복 응답 때문이다. 빈 쪽이 나오면 멈추는 방식은 이 서버에서 작동하지 않는다. 분양 게시판은 같은 방식으로 세어 보니 1쪽과 2쪽 모두 16줄이라 한 쪽짜리로 보인다(해시까지 비교하지는 않았다).

고친 뒤 같은 명령을 다시 돌렸다.

npm run notice -- --fetch lh:2015122300020726
# ✅ …/lh-2015122300020726-2.pdf  655KB
#   (공고문 PDF 외에 hwpx, 신청서 양식까지 첨부 5개 목록 출력)

공고문 PDF 655KB가 내려왔고 첨부 5개의 목록이 같이 출력됐다. 이 수정은 외부 사이트를 직접 읽는 코드라 자동 테스트로 고정하지 않았다. 확인은 위 실행이 전부다.

남은 위험

  • 상한 4쪽은 임의로 정한 값이다. 게시판이 지금 2쪽이라 여유를 둔 것일 뿐이고, 4쪽을 넘게 늘면 같은 문제가 다시 생긴다.
  • 두 곳을 하나의 함수로 합친 것이 아니다. 규칙을 맞췄을 뿐이라 한쪽만 또 고쳐질 수 있다. 합치는 쪽이 맞지만 이번에는 하지 않았다.
  • 쪽 번호를 5까지만 확인했다. 훨씬 큰 번호나 음수에서 서버가 어떻게 하는지는 보지 않았다.

다음부터 할 것

같은 대상을 읽는 코드를 고칠 때는 먼저 그 대상을 읽는 곳이 몇 군데인지 센다.

grep -n "selectWrtancList\|currPage" scripts/*.mjs

한 곳만 고쳤다면 나머지를 같은 시점에 열어서 같은 규칙인지 본다. 에러 메시지가 “없다”고 말할 때는 대상이 없는 것인지, 내가 보는 범위가 좁은 것인지를 먼저 가른다. 이번에는 grep 한 번과 curl 다섯 번이면 됐다.

자주 묻는 질문

존재하지 않는 쪽 번호를 주면 게시판은 어떻게 응답하나요?

이 게시판은 빈 목록이나 오류가 아니라 마지막 쪽의 내용을 그대로 돌려줬습니다. 2026년 10월 7일에 임대 게시판에 currPage=2부터 5까지 요청했더니 네 응답의 행 수가 모두 14줄(제목줄 포함)이었고, 안에 든 공고 id 목록의 해시도 같았습니다. 그래서 '빈 쪽이 나오면 멈춘다'는 종료 조건은 쓸 수 없고, 이미 본 행이 다시 나오면 멈추거나 쪽 수 상한을 두어야 합니다. 5쪽까지만 확인했고 더 큰 번호의 동작은 보지 않았습니다.

쪽수 상한을 4쪽으로 둔 이유는 무엇인가요?

게시판이 지금 2쪽이라 여유를 두고 정한 값이고, 4라는 숫자에 공식 근거는 없습니다. 목록 조회 함수가 이미 4쪽까지를 상한으로 쓰고 있어서 첨부 다운로드도 같은 값으로 맞췄습니다. 게시판이 4쪽을 넘게 늘면 이 상한이 다시 문제를 만듭니다.

같은 게시판을 읽는 코드가 두 곳이면 왜 위험한가요?

한쪽을 고칠 때 다른 쪽이 같이 고쳐지는 장치가 없기 때문입니다. 이번에도 목록 조회는 2쪽 이후를 놓치는 문제를 10월 2일에 고쳤지만, 첨부 다운로드는 조회 방식이 따로 구현되어 있어서 그대로 남았습니다. 이번 수정은 두 곳의 규칙을 맞췄을 뿐 하나의 함수로 합치지는 않았기 때문에 같은 위험이 완전히 사라진 것은 아닙니다.

#웹스크래핑#페이지네이션#curl#디버깅#LH