링크를 모아두는 일은 끝이 없다. 오늘은 멀쩡하던 주소가 내일이면 404로 바뀌고, 리다이렉트가 한 번만 걸리던 사이트가 세 번을 돌아 들어가야 열리기도 한다. 커뮤니티에서 공유되는 링크모음이나 팀 내부의 주소모음 시트, 소규모 포털처럼 자주 쓰는 링크가 모여 있으면 이 문제가 더 빠르게 드러난다. 주소아지트나 개인 북마크 서비스에 저장해 둔 URL도 마찬가지다. 핵심은 사람이 직접 하나씩 눌러보지 않고도, 가능한 한 짧은 시간에 상태를 가늠해 내는 절차와 도구를 갖추는 일이다.
이 글은 실무에서 반복적으로 링크 검사를 자동화해 온 경험을 바탕으로, 적은 노력으로 죽은 링크를 솎아내는 방법을 정리했다. 소수의 링크에 대한 빠른 진단부터 수만 건 규모의 주소모음 처리, 그리고 자동 모니터링까지, 상황별로 쓸 만한 전략과 도구를 짚어 본다.

죽은 링크가 늘어나는 진짜 이유
링크가 깨지는 이유는 단순히 사이트가 사라졌기 때문만은 아니다. 실제로 걸려 넘어지는 지점은 더 다양하다.
- 도메인 이전 과정에서 발생한 301, 302 체인과 최종 404. 한 번쯤은 이동하지만 목적지 서버가 늦거나 설정이 엉키면 끝에서 404가 난다. HTTPS 전환 이후 인증서 오류. 만료, 중간 인증서 누락, SNI 미지원 같은 이유로 브라우저는 경고를 띄우고 자동 요청은 실패한다. 클라우드프레어와 같은 프록시에서 발생하는 레이트 리밋, 봇 차단. 자동 점검 스크립트가 차단 리스트에 걸려 403, 503을 빈번하게 맞는다. 소프트 404. 상태 코드는 200이지만 화면에는 “페이지를 찾을 수 없음”만 보인다. 검색엔진은 이걸 소프트 404로 본다. 자바스크립트 렌더링 의존. 초기 HTML은 빈껍데기라서 단순 요청으로는 아무 내용도 오지 않는다. 국가 또는 IP 기반 차단. 국내에서는 열리지만 해외 서버에서 점검하면 막히거나, 그 반대도 있다.
경험상 링크모음에서 발견되는 문제의 절반가량은 리다이렉트, 20%는 인증서 관련, 20%는 실제 삭제, 나머지는 지역 차단이나 로봇 차단에 묶인다. 이 분포는 업종과 대상 사이트 성격에 따라 달라지지만, 적어도 리다이렉트와 인증서를 먼저 본다면 시간을 크게 아낄 수 있다.
빠른 진단의 원칙
빠른 점검은 정확성과 속도의 균형을 잡아야 한다. 모든 링크를 브라우저로 열어 DOM을 다 받아 확인하면 정확하지만 너무 느리다. 반대로 상태 코드만 확인하면 속도는 빠르지만 소프트 404나 자바스크립트 의존 사이트에서 놓치는 게 많다. 실무에서는 보통 2단계 접근이 가장 효율적이었다. 1차로 헤드 요청과 제한 리다이렉트 추적을 통해 상태를 걸러내고, 2차로 의심군만 실제 GET과 간단한 콘텐츠 검사, 필요 시 헤드리스 브라우저로 재검한다.
여기서 중요한 두 가지는 타임아웃과 동시성이다. 타임아웃을 공격적으로 짧게 잡으면 응답이 느린 사이트가 모두 실패로 분류되고, 길게 잡으면 전체 처리 시간이 길어진다. 동시성은 많을수록 빠르지만 봇 차단에 걸리기 쉽다. 적정선은 보통 50에서 200 동시 요청 사이에 있다. 공용 네트워크에서라면 50 내외, 사내 점검 서버에서 목적 사이트와의 신뢰가 어느 정도 쌓였다면 200도 무리가 없다.
시작은 정리부터: 링크 품질을 올리는 전처리
링크모음이 엉켜 있으면 점검도 엉킨다. 실제로 속도를 가장 많이 끌어올려 주는 건 다음의 전처리다.
- 공백, 줄바꿈, 제어문자 제거. 시트나 복붙 과정에서 붙은 보이지 않는 문자 때문에 요청이 실패하는 경우가 잦다. 스킴 보정. Www.example.com처럼 스킴이 없는 항목은 https로 보정 후 http 폴백을 시도한다. Https 전환 비율은 업종별로 다르지만, 최근에는 https가 실패하더라도 http는 열리는 경우가 거의 없다. 트래킹 파라미터 정리. Utm_source 같은 파라미터는 점검에는 필요 없다. 동일 페이지 중복 검사를 줄이기 위해 제거한다. 중복 제거와 정규화. Trailing slash, 대소문자, 앵커(#section), 세션 파라미터 차이로 생긴 중복을 그룹화한다. 국제 도메인은 punycode로 표준화한다.
이 전처리를 거치면 보통 전체 링크 수의 10에서 30%가 정리 단계에서 묶인다. 중복 제거만 해도 점검 시간이 같은 비율로 줄어드는 셈이다.
10분 안에 끝내는 퀵스캔 워크플로
아래 순서는 100개 미만의 링크모음, 혹은 주소아지트 같은 개인 저장소에서 쓰면 효과가 좋다. 별도의 서버가 없어도 노트북 하나면 충분하다.
URL 정리 스크립트를 돌려 공백, 스킴, 트래킹 파라미터를 정리한다. 헤드 요청으로 1차 스캔을 돌리되, 리다이렉트는 최대 5회, 타임아웃은 3초로 잡는다. 400에서 599 응답, 무한 리다이렉트 의심 링크, 인증서 오류 링크를 의심군으로 표시한다. 의심군만 GET으로 다시 확인하고, 본문 길이가 500바이트 미만이거나 “not found”, “삭제되었습니다” 같은 패턴을 찾는다. 소프트 404 가능성이 있는 링크 중 주요 링크만 브라우저로 열어 시각 확인한다.이 다섯 단계만으로도 체감상 80% 이상의 죽은 링크를 빠르게 솎아낼 수 있다. 포인트는 지나치게 세밀하게 보지 않는 것이다. 검증이 필요한 링크만 다음 단계로 넘기고, 확인된 사망 링크부터 처리한다.
HTTP 상태 코드 해석, 함정과 우회
상태 코드 해석에는 함정이 많다. 200이면 무조건 정상, 404면 무조건 삭제라고 단정하기 어렵다. 실무에서 기준을 다음처럼 두면 오판을 줄일 수 있다.
- 200 + 본문 길이 1 KB 미만, 또는 타이틀에 “에러”, “오류”, “not found”가 포함되면 소프트 404 의심으로 분류한다. 301과 302는 최종 목적지만 본다. 체인이 5회를 넘어가면 설정 오류 가능성이 높다. 이 경우 마지막 두 번의 목적지를 기록해 운영자에게 전달하면 원인 파악이 빠르다. 401, 403은 정책 문제다. 로그인 또는 IP 제한이 없던 곳에서 새로 등장했다면 담당자 확인이 필요하다. 410은 영구 삭제다. 404보다 확정적이므로, 링크 교체가 아니라 제거를 기본으로 본다. 429, 503은 서버가 바쁜 상태거나 레이트 리밋에 걸린 경우다. 동시성이나 요청 헤더 조정으로 재시도하고, 같은 링크에서 세 번 연속 발생하면 나중에 다시 본다.
또 하나, 헤드 요청이 막히는 사이트가 있다. 일부 CDN이나 WAF는 헤드 요청을 비정상으로 보고 차단한다. 이럴 때는 헤드 대신 소용량 GET을 시도하고 Range 헤더로 첫 수백 바이트만 받아도 된다.
현장에서 자주 쓰는 도구와 쓰임새
작은 링크모음이라면 시스템 도구로도 충분하다. 운영체제에 따라 접근은 약간씩 다르다.
Curl로 빠르게 확인할 때:
Curl -I -L --max-redirs 5 --connect-timeout 3 --retry 1 https://example.com
-I는 헤드 요청, -L은 리다이렉트 추적, --max-redirs는 안전장치다. 인증서 오류를 확인하고 싶다면 -v를 추가해 핸드셰이크 로그를 본다.
PowerShell에서는 Invoke-WebRequest가 편하다.
$urls = Get-Content .\links.txt $results = foreach ($u in $urls) Try $res = Invoke-WebRequest -Uri $u -Method Head -MaximumRedirection 5 -TimeoutSec 3 [PSCustomObject]@ Url=$u; Status=$res.StatusCode; FinalUrl=$res.BaseResponse.ResponseUri catch [PSCustomObject]@ Url=$u; Status="ERR"; FinalUrl="" $results | Export-Csv .\scan.csv -NoTypeInformation대상을 많이 다룬다면 Python이 확장성 면에서 유리하다. Requests로 시작해도 되고, 수천 건 이상이면 aiohttp로 비동기 처리하는 편이 시간 대비 효율이 확실히 좋다.
Python, 간단한 동시성 스캐너 예시:
Import asyncio, aiohttp, async_timeout, re PATTERNS = re.compile(r"(not\s+found|삭제되었습니다|에러|오류)", re.I) Async def check(session, url): Try: With async_timeout.timeout(3): Async with session.head(url, allow_redirects=True, max_redirects=5) as r: Code = r.status Final = str(r.url) If 200 <= code < 300: # lightweight GET to confirm soft 404 Async with session.get(final, headers="Range":"bytes=0-1023") as g: Text = await g.text(errors="ignore") If len(text) < 200 or PATTERNS.search(text): Return url, code, final, "SOFT404?" Return url, code, final, "" Except Exception as e: Return url, "ERR", "", str(e) Async def main(urls): Conn = aiohttp.TCPConnector(limit=100, ssl=False) Async with aiohttp.ClientSession(connector=conn) as session: Tasks = [check(session, u) for u in urls] Return await asyncio.gather(*tasks) Urls = [l.strip() for l in open("links.txt") if l.strip()] Results = asyncio.run(main(urls)) For row in results: Print("\t".join(map(str,row))) <p> Ssl=False는 인증서 오류로 전체가 멈추는 현상을 피하려는 목적이다. 실제 운영에서는 인증서 오류도 수집해야 하니, 예외 메시지를 기록하고 분류하는 편이 낫다.브라우저가 필요한 케이스와 헤드리스 전략
일부 링크는 초기 HTML이 비어 있고 자바스크립트가 내용을 채운다. 이런 경우 상태 코드는 멀쩡하고 텍스트 길이도 충분한데 막상 사람이 보면 빈 화면이 뜬다. SSR이 아닌 SPA, 또는 로그인 후 렌더링되는 대시보드 링크에서 흔하다.

이럴 때는 헤드리스 브라우저가 답이다. Playwright나 Puppeteer 같은 도구로 2차 검증만 수행한다. 모든 링크에 브라우저를 쓰면 비용과 시간이 폭증하니, 1차 점검에서 소프트 404 의심으로 묶인 링크, 메타 리프레시가 보이는 링크, hash 기반 라우팅 링크만 브라우저로 렌더링해 타이틀과 주요 영역의 텍스트를 추출해 본다. 타임아웃은 10초를 넘기지 않는 것이 좋다. 그 이상 기다려야 열리는 사이트는 사용자 경험 관점에서도 교체 대상이다.
대용량 주소모음 처리 파이프라인
수만 건의 주소모음을 다룬 적이 있다면, CPU보다 IO와 네트워크가 병목이라는 사실을 체감했을 것이다. 시스템은 단순하지만 원칙을 지키면 잘 돌아간다.
- URL 정리와 중복 제거를 선행한다. 해시 기반으로 호스트, 경로, 주요 파라미터만 남기고 동일군을 묶는다. 큐를 사용해 동시성을 제어한다. 단일 머신에서 200 내외, 여러 지역에 퍼져야 한다면 리전별 워커를 띄운다. 결과는 가급적 스트리밍으로 저장한다. 로컬 파일이라면 append, 클라우드라면 Kinesis, Pub/Sub 같은 스트림에 넣고 후처리한다. 같은 호스트에 대한 요청은 라운드로빈으로 시간차를 준다. 호스트당 초당 1에서 3건 정도로 제한하면 봇 차단을 덜 맞는다. 실패 재시도는 2에서 3회로 제한하고, 재시도 간격은 지수 백오프를 쓴다. 503이 길게 이어지는 호스트는 잠시 큐에서 제외한다.
이 구조로 5만 건 링크를 점검했을 때, 도심 오피스 인터넷 환경에서 15에서 25분이 소요되었다. 헤드 요청 위주, 브라우저 검증은 상위 의심 링크 3%에만 적용했다.
Google Sheets와 Excel에서의 간단 자동화
팀이 스프레드시트로 링크모음을 관리한다면, 시트 안에서 최소한의 점검을 돌릴 수 있다. Google Sheets는 Apps Script로, Excel은 Power Query나 Office Script를 이용한다.
Google Sheets Apps Script 예시:
Function CHECK_URL(url) Try Var options = 'method': 'head', 'followRedirects': true, 'muteHttpExceptions': true ; Var res = UrlFetchApp.fetch(url, options); Return res.getResponseCode(); catch (e) Return "ERR";사용법은 =CHECK_URL(A2)처럼 셀에 넣으면 된다. 다만 UrlFetchApp은 동시 호출 제한이 있어 대량 점검에는 맞지 않는다. 트리거로 밤 시간에 배치 실행하고 결과만 써두면, 낮에는 가볍게 확인하는 방식이 현실적이다.
Excel에서는 Power Query로 웹 콘텐츠를 불러와 상태 코드를 취합하거나, Office Script에서 fetch를 이용해 간단한 헤드 요청을 보낼 수 있다. 회사 네트워크 정책 때문에 외부 호출이 막히는 경우가 있으니, IT 정책을 먼저 확인하는 것이 좋다.
리다이렉트 정리와 링크 교체 기준
죽은 링크를 찾는 것 못지않게 중요한 일이 대체 링크를 확보하고, 리다이렉트 체인을 줄이는 일이다. 기준을 몇 가지 잡아두면 정리가 수월하다.
- 301, 308로 영구 이동하는 링크는 최종 목적지로 교체한다. 리다이렉트가 짧아져 사용자 체감 속도도 좋아진다. 302, 307 같은 임시 이동이지만 3개월 이상 지속되면, 내부 규칙으로도 교체 대상에 포함한다. 실무에서 임시 이동이 오래가는 경우가 많다. 짧은 URL은 확장한다. Bit.ly 같은 서비스는 클릭 추적에 유용하지만, 영속성 측면에서는 원본 URL이 낫다. 콘텐츠가 삭제되었지만 대체 자료가 있다면, 운영자 노트에 교체 사유와 날짜를 적어 둔다. 주소모음이 결국 문서 시스템이라면 변경 이력은 비용이 아니라 자산이다.
소프트 404를 가리는 간단한 규칙
상태 코드가 200이라도 실제로는 깨진 경우가 꽤 된다. 완벽한 판정은 어렵지만, 다음 조합이면 소프트 404로 분류해 사람 검토 대상으로 넘기는 편이 안전하다.
- 본문 길이가 비정상적으로 짧다. 예를 들어 1 KB 미만이거나, 이전 판본 대비 90% 이상 줄었다. 타이틀과 H1에 “없음, 삭제, 오류, not found, gone” 같은 단어가 있다. canonical이 홈페이지를 가리킨다. 상세 페이지가 갑자기 홈으로 정규화되면 종종 비정상이다. 구조화 데이터가 사라졌다. 뉴스, 상품 페이지처럼 정형 데이터를 쓰던 페이지에서 마크업이 없어지면 링크가 죽었을 가능성이 높다.
인증서와 DNS, 네트워크 변수를 다루는 요령
HTTPS 시대에는 인증서 문제가 잔고장 1순위다. 자동화 도구에서는 크게 세 가지를 확인한다.
- 인증서 만료일이 지났다. 오차가 있어도 브라우저는 즉시 경고를 띄운다. 이 경우는 점검 도중에도 회복되는 일이 잦다. 호스트명이 인증서의 SAN에 없다. 서브도메인 추가 과정에서 빠뜨린 경우다. 중간 인증서가 누락되었다. 특정 OS나 언어 런타임에서만 실패한다. Curl로는 열리는데 Java 애플리케이션에서만 실패하는 식이다.
DNS는 CNAME 체인과 A 레코드 TTL을 확인한다. 갑자기 응답 시간이 늘어났는데 상태 코드가 멀쩡하면, DNS 지연이 대부분의 원인이다. 대규모 점검 시에는 로컬 DNS 캐시를 비우지 말고, 리졸버를 1에서 2개로 고정해 불필요한 변동을 줄인다.
윤리, 정책, 그리고 안전장치
봇 차단을 무시하고 무작정 긁어서는 안 된다. 합리적인 레이트 리밋, User-Agent 지정, robots.txt 존중은 체크리스트의 기본이다. 사이트 소유자가 명시적으로 금지한 경로는 점검 대상에서 제외하고, 유료 서비스나 로그인 뒤 페이지는 각 서비스의 약관을 우선한다. 기업 환경에서는 보안팀과 협의해 전용 점검 IP를 받는 편이 가장 깔끔하다. 이 IP를 화이트리스트에 올리면 불필요한 403과 503이 줄고, 운영자에게도 예측 가능한 트래픽으로 보인다.
협업과 커뮤니케이션, 의심 링크 처리의 생활화
주소모음이 팀 공유 자산이라면, 죽은 링크를 발견했을 때의 커뮤니케이션 흐름이 중요하다. 담당자를 지정하고, 상태를 기록하는 필드를 만든다. 상태, 마지막 점검 일시, 최종 목적지, 교체 여부, 비고 같은 칼럼이 있으면 중구난방이 줄어든다. 경험상 이런 필드는 너무 많아도, 너무 적어도 안 된다. 다섯 칼럼 정도가 유지 가능한 균형이다.
의심 링크를 바로 지우기보다는 비활성 처리하고, 2주에서 4주 뒤 재점검해 확정 삭제하는 절차를 두면 안전하다. 특히 일시 장애가 잦은 서비스, 유지보수 공지를 자주 올리는 공공기관 사이트는 며칠 후에 복구되는 경우가 많다.
사례에서 배우는 작은 디테일
콘텐츠 마케팅 팀이 관리하던 링크모음 9,800건을 점검한 적이 있다. 처음에는 단일 서버에서 400 동시성으로 밀어붙였고, 30분쯤 지나 429가 폭주했다. 프록시 뒤 사이트 몇 곳이 방화벽으로 1시간 차단을 걸어버린 것이다. 동시성을 120으로 낮추고 호스트당 레이트 리밋을 걸자 에러율이 18%에서 4%대로 떨어졌다. 최종적으로 삭제 확정은 7.1%, 리다이렉트 교체는 19.4%였다. 의외였던 점은 소프트 404가 3% 가까이 나왔다는 사실이다. 200이라 방심했다가, 실제로는 빈 템플릿만 남은 페이지가 상당했다.
개인적으로 주소아지트 같은 링크 저장 서비스에 아침마다 들어가 즐겨찾기를 훑는다. 일주일에 한 번은 간단한 스크립트로 1분짜리 퀵스캔을 돌린다. 리스트가 300개 정도라 3초 타임아웃에 80 동시성만 써도 20초 내외로 끝난다. 이 루틴을 들이고 나서, 새로 들어온 링크의 불량률이 체감상 절반 이하로 줄었다. 사람이 최종 검토하기 전에 자동 필터가 한 번 걸러주는 셈이다.
장기적으로는 모니터링이 답이다
링크 품질 유지의 핵심은 주기적 점검이다. 배치를 밤마다 돌리고, 변동이 있는 링크만 알림을 보낸다. 변동이라 함은 상태 코드가 2xx에서 4xx로 바뀌거나, 최종 목적지가 바뀌거나, 본문 길이가 급감한 경우다. 알림은 이메일보다 슬랙, 팀즈 같은 채팅이 유용하다. 메시지 하나에 링크, 이전 상태, 현재 상태, 교체 제안까지 담으면, 담당자가 바로 처리할 수 있다.
변동 추적에는 해시가 편하다. 본문 전체를 해시할 필요는 없다. 타이틀, canonical, 주요 H 태그, 그리고 본문 첫 2 KB 정도만 주소모음 묶어 해시하면 충분히 민감하게 변화를 잡아낸다. 과하게 민감하면 노이즈가 많아지니, 임계값을 잡고 연속 두 번 이상 변동이 있을 때만 알리는 식으로 튜닝한다.
빠른 검사를 위한 운영 체크리스트
- 헤드 요청이 막히는 사이트를 대비해 소용량 GET과 Range 헤더를 준비한다. 동시성은 전역과 호스트당 두 축으로 제한하고, 429, 503이 보이면 즉시 내려라. 소프트 404 패턴을 언어별로 유지하라. 한국어와 영어만으로도 대부분의 케이스를 잡는다. 리다이렉트 체인과 최종 목적지를 저장해 다음 점검 때 초기 URL을 줄여라. 사용자에게 보이는 변화가 더 중요하다. 코드가 200이어도 비어 보이면 죽은 링크로 취급한다.
마지막 손질, 기록과 유지
점검이 끝나면 결과를 남기는 습관이 필요하다. 언제, 무엇을, 어떤 기준으로 삭제했는지 기록하면, 몇 달 뒤 같은 논쟁이 반복되지 않는다. 주소모음의 각 항목에 간단한 주석 란을 두고, “2026-05-18, 리다이렉트 영구화, 최종 URL로 교체”처럼 적어 둔다. 리스트가 커질수록 이 작은 문장이 시간을 벌어준다.
죽은 링크는 피할 수 없다. 하지만 정리와 전처리, 가벼운 자동화, 그리고 소수의 브라우저 검증만으로도 품질을 눈에 띄게 끌어올릴 수 있다. 링크모음이나 주소모음 같은 단순한 목록도 관리와 운영의 시선으로 보면 작은 제품이 된다. 한 번만 제대로 세팅해 두면, 매주 쌓이는 유지 비용이 크게 줄어든다. 그리고 무엇보다, 링크를 눌렀을 때 바로 열린다는 그 단순한 안도감이 사용자 경험 전체를 바꿔 놓는다.