403이 뜨는 사이트 가격 추적하기 — 스크래핑이 막혔을 때 n8n으로 우회하고 어제 가격과 비교하기
이 글과 연결된 글 보기
드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.표시된 글 7편 목록
HTTP Request 노드로 쇼핑몰 상품 페이지를 열었더니 403이 돌아온다면, 그 자리에 Firecrawl 노드 하나를 끼우는 것으로 스크래핑이 다시 살아납니다. 거기에 구글시트 Append → 어제 행 조회 → If 세 노드를 붙이면 매일 아침 9시에 가격을 쌓고, 어제보다 내려간 날에만 "살 시간"이라는 슬랙 DM이 옵니다. 이 워크플로우를 만들어보면 "막힌 사이트를 어떻게 우회하는가"와 "시트를 어제 값의 저장소로 쓰는 법" 두 가지가 한 번에 손에 익습니다.
이 워크플로우 하나로 보여줄 수 있는 것
HTML 노드 편에서 다룬 스크래핑은 "막지 않는 사이트"가 전제였습니다. 실제로 사고 싶은 상품의 페이지는 대부분 그렇지 않습니다. 이 워크플로우는 그 벽에 부딪힌 뒤에 어떻게 넘어가는지를 보여주는 편이라, 면접에서 "스크래핑하다 막힌 적 있나요?"라는 질문에 답이 됩니다.
- 막힌 요청 우회 — 403이 왜 나는지 이해하고, 헤더를 만지는 대신 렌더링을 대신해 주는 서비스(Firecrawl)를 끼워 넣은 경험
- credential로 키 관리 — 전용 노드가 없어도 HTTP Request + Header Auth credential로 외부 API를 부르고, API 키를 노드 파라미터에 적지 않는 습관
- 시트를 상태 저장소로 쓰기 — 오늘 값을 쌓고 어제 행을 다시 읽어 비교하는, "이전 실행 결과와 비교"하는 자동화의 기본형
세 번째가 응용 범위가 가장 넓습니다. 가격뿐 아니라 팔로워 수, 검색 순위, 재고 수량처럼 "어제와 달라졌을 때만 알려줘" 유형은 전부 같은 구조로 만들어집니다.
배경 — 세일 시작한 날 아침에 알고 싶다
사고 싶은 옷이 있는데 정가로는 사기 아깝고, 매일 들어가서 확인하기는 귀찮습니다. 이 워크플로우는 한 스포츠웨어 브랜드 공식몰의 상품 한 개를 매일 아침 9시에 확인해, 어제보다 가격이 내려가면 슬랙으로 본인에게 DM을 보내도록 만든 것입니다. 데이터팝콘 n8n 인스턴스에 실습용으로 만들어 두었고, Active 상태로 저장돼 있습니다.
처음 만들 때 가장 먼저 부딪힌 것이 403이었습니다. 원본 캔버스에는 그 순간이 스티키 노트 한 장으로 남아 있는데, 아래에서 그대로 옮깁니다. 설치나 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 있으니, 이 글은 우회와 비교 로직에만 집중합니다.
구조 — Trigger / Task / Target
- Trigger — 매일 9시 스케줄
- Task — Firecrawl로 렌더된 HTML 받기 → 가격 추출 → 시트에 오늘 행 추가 → 어제 행 조회 → 두 값 비교
- Target — 조건을 통과했을 때만 슬랙 DM
원본 캔버스에는 Firecrawl 부분이 두 갈래로 그려져 있습니다. 스케줄 트리거에 연결된 본 흐름은 Firecrawl 커뮤니티 노드를 쓰고, 그 옆에 Manual Trigger로 시작하는 HTTP Request 버전이 따로 있습니다. 스티키 노트에 각각 "firecrawl 커뮤니티 노드 사용", "firecrawl 커뮤니티 노드 없는 경우"라고 적혀 있어서, 하나는 본 경로이고 하나는 대체 경로라는 의도가 분명합니다. HTML 노드부터 뒤쪽은 두 갈래가 파라미터까지 똑같으니 한 번만 설명합니다.
만들기
0. 준비물 — Firecrawl 계정과 시트
Firecrawl은 자바스크립트까지 실행한 뒤의 페이지를 HTML이나 마크다운으로 돌려주는 스크래핑 API입니다. 공식 가격 페이지 기준으로 2026년 8월 현재 무료 플랜이 월 1,000 크레딧이고 scrape는 페이지당 1크레딧이라, 하루 한 번 상품 하나를 확인하는 이 워크플로우는 한 달에 30크레딧이면 됩니다. 가입 시점에 조건이 바뀌었을 수 있으니 무료 크레딧이 있는지는 가입 화면에서 한 번 더 확인하세요.
구글시트는 헤더 행에 날짜, 가격 두 컬럼만 있으면 됩니다. 이 시트가 곧 가격 이력이자 "어제 값"의 저장소입니다.
한 가지 미리 말해두면, 이 워크플로우는 개인 학습용으로 하루 1회만 요청합니다. 상대 사이트의 이용약관과 robots.txt를 확인하고, 요청 빈도를 분 단위로 올리거나 상품 수백 개를 한꺼번에 긁는 식으로 키우지 마세요. 우회할 수 있다는 것과 마음껏 해도 된다는 것은 다른 이야기입니다.
1. 왜 403인가 — 일반 HTTP Request가 막히는 이유
먼저 HTML 노드 편과 똑같이 HTTP Request 노드로 상품 URL을 GET 해보면 이 사이트는 403 Forbidden을 돌려줍니다. 원본 캔버스에는 그 순간의 해석이 스티키 노트로 남아 있습니다.
403 에러 — 너 자바스크립트 실행 못 하는 가짜 브라우저(봇)지?
403은 "서버가 요청을 이해했지만 거부한다"는 뜻입니다. 요즘 쇼핑몰은 봇 차단 서비스를 앞에 두는 경우가 많고, 이런 서비스는 브라우저처럼 자바스크립트를 실행하는지, 쿠키를 받아 되돌려주는지 같은 신호로 사람인지 봇인지 판단합니다. n8n의 HTTP Request 노드는 HTML을 받아올 뿐 스크립트를 실행하지 않기 때문에 그 문턱을 못 넘습니다.
원본에는 연결이 끊긴 채 남아 있는 HTTP Request 노드가 하나 있는데, 헤더에 User-Agent를 크롬 브라우저 값으로 바꿔 넣은 흔적이 있습니다. 그 노드가 본 흐름에 연결되지 않은 것을 보면 User-Agent만 바꾸는 방법으로는 해결되지 않았던 것으로 읽힙니다. 헤더 위장으로 될 사이트였으면 이 글도 필요 없었을 겁니다.
2. Firecrawl 커뮤니티 노드 — 본 경로
Firecrawl 노드에서 Operation을 Scrape로 두고 URL에 상품 페이지 주소를 넣습니다. Scrape Options의 Formats에는 html을 고릅니다. 마크다운도 받을 수 있지만, 뒤에서 CSS 셀렉터로 가격을 뽑으려면 HTML이 필요합니다. 실행하면 결과가 data.html에 렌더된 HTML로, data.metadata.url에 최종 URL로 들어옵니다.
n8n Cloud라면 이 노드를 노드 패널에서 바로 설치할 수 있습니다. n8n 문서에 따르면 검증(verified)된 커뮤니티 노드는 노드 패널에서 검색해 설치할 수 있고, 인스턴스 owner나 admin 계정이어야 합니다. Firecrawl 노드는 검증 노드이고, 2026년 8월 기준 Firecrawl 쪽 n8n 문서에는 노드 안의 Connect to Firecrawl 버튼으로 계정을 만들면 무료 크레딧을 준다는 안내도 있습니다. 검증되지 않은 커뮤니티 노드는 self-hosted 인스턴스에서만 npm 패키지로 설치할 수 있다는 점이 둘의 차이입니다.
3. 커뮤니티 노드가 없을 때 — HTTP Request + Header Auth
인스턴스에 커뮤니티 노드를 설치할 권한이 없거나 회사 정책상 막혀 있다면, Firecrawl API를 HTTP Request 노드로 직접 부릅니다. 원본의 대체 경로가 이 방식입니다.
- Method:
POST - URL:
https://api.firecrawl.dev/v2/scrape - Authentication: Generic Credential Type → Header Auth
- Body(JSON):
{
"url": "https://example.com/products/your-item",
"formats": ["html"]
}
Header Auth credential을 새로 만들 때 Name에 Authorization, Value에 Bearer 뒤에 Firecrawl API 키를 붙여 저장합니다. 원본 노드는 이 헤더를 노드 파라미터에 직접 적어 두었는데, 그대로 두면 워크플로우를 export하거나 공유하는 순간 키가 같이 나갑니다. 실제로 이 글을 쓰려고 원본을 열었을 때 첫 번째로 한 일이 그 키를 지우는 것이었습니다. 키는 예외 없이 credential에 넣으세요.
원본은 /v1/scrape 주소를 쓰고 있지만, 지금 Firecrawl API 문서는 v2를 기준으로 쓰여 있습니다. 새로 만든다면 v2로 시작하는 편이 안전하고, 응답이 data.html과 data.metadata.url에 들어오는 구조는 커뮤니티 노드 출력과 같아서 뒤쪽 노드를 손댈 필요가 없습니다.
4. HTML — 가격 셀렉터 하나만 뽑기
HTML 노드에서 Operation을 Extract HTML Content로 두고, JSON Property에 data.html을 적습니다. Firecrawl 출력이 최상위가 아니라 data 아래에 있기 때문입니다. Extraction Values에는 항목 하나만 넣습니다.
- Key:
rawPrice - CSS Selector:
.markdown-prices
셀렉터는 상품 페이지에서 개발자 도구로 가격 요소를 찾아 그 클래스명을 쓴 것이라 사이트마다 다릅니다. 여기서 나오는 rawPrice는 통화 기호와 쉼표가 섞인 문자열입니다. 숫자로 바꾸는 일은 다음 노드에서 합니다.
5. Google Sheets Append — 오늘 날짜와 가격
Google Sheets 노드(Operation: Append)에서 Mapping Column Mode를 Map Each Column Manually로 두고 두 컬럼을 채웁니다.
날짜:{{ $now.toFormat('yyyy-MM-dd') }}가격:{{ parseInt($json.rawPrice.replace(/[^0-9]/g, '')) }}
replace(/[^0-9]/g, '')가 숫자 아닌 글자를 전부 지우고, parseInt가 문자열을 정수로 바꿉니다. 날짜를 yyyy-MM-dd로 고정하는 이유는 다음 노드에서 이 문자열로 어제 행을 찾기 때문입니다. 형식이 하루라도 다르면 조회가 빈손으로 돌아옵니다.
6. Google Sheets Get Row — 어제 행 찾기
같은 시트를 다시 읽는 Google Sheets 노드(Operation: Get Row(s))를 붙이고 Filters에 조건 하나를 넣습니다.
- Column:
날짜 - Value:
{{ $now.minus({ days: 1 }).toFormat('yyyy-MM-dd') }}
$now.minus({ days: 1 })이 어제 날짜입니다. 이 노드의 출력은 어제 적재한 행 하나이고, 그 안의 가격이 비교 대상이 됩니다. 어제 행이 없으면 출력이 0개가 되어 뒤 노드는 실행되지 않습니다. 처음 만든 날 알림이 안 오는 게 정상인 이유입니다.
7. If — 오늘 가격이 어제보다 낮으면
If 노드 조건은 하나입니다.
- 왼쪽:
{{ $('Append row in sheet').item.json['가격'] }}— Append 노드가 돌려준 오늘 가격 - 연산자: Number → is less than
- 오른쪽:
{{ $json['가격'] }}— 직전 노드가 찾아온 어제 가격
$('Append row in sheet')로 두 노드 앞의 출력을 꺼내 오는 게 핵심입니다. 직전 노드 출력($json)은 어제 행이라 오늘 값이 없기 때문입니다. 컬럼명이 한글이라 $json['가격']처럼 대괄호로 접근했습니다. 만약 시트에서 온 값이 문자열로 들어와 타입 오류가 나면, If 노드 옵션의 Type Validation을 Loose로 바꾸면 됩니다.
8. Slack — 본인에게 DM
Slack 노드(Send a message)에서 Send Message To를 User로 두고 자기 계정을 고릅니다. 채널이 아니라 DM이라 봇을 채널에 입장시키는 절차가 없습니다. 메시지는 원본에서 브랜드명만 뺀 형태로 이렇습니다.
🎉 *살 시간* 🤗
👉 <{{ $('Scrape a url and get its content').item.json.data.metadata.url }}|상품 페이지 바로가기>
Firecrawl 노드가 돌려준 data.metadata.url을 슬랙 링크 문법으로 감싸서, DM에서 바로 상품 페이지로 갈 수 있게 했습니다. 여기까지 만든 뒤 Execute workflow로 시트에 오늘 행이 들어가는지 확인하고, Active를 켜면 다음 날 9시부터 비교가 시작됩니다.
원본 캔버스에 남아 있는 흔적
실행 이력 대신 캔버스에 남은 흔적 세 가지를 옮깁니다. 셋 다 따라 만들 때 그대로 마주치는 지점입니다.
첫째는 위에서 인용한 403 스티키 노트와 연결이 끊긴 HTTP Request 노드입니다. 실패한 시도를 지우지 않고 남겨둔 덕분에 "왜 Firecrawl이 필요한가"가 캔버스만 봐도 설명됩니다. 포트폴리오에서도 이 방식이 좋습니다. 막혔던 노드를 남기고 스티키로 이유를 적어두면 문제 해결 과정이 그대로 보입니다.
둘째는 두 갈래 경로입니다. "커뮤니티 노드 사용"과 "커뮤니티 노드 없는 경우"를 나란히 두고 뒤쪽 노드를 복사해 붙인 구조라, 어느 환경에서 열어도 한쪽은 동작합니다.
셋째는 그 복사에서 생긴 작은 실수입니다. 대체 경로 끝의 Slack 노드도 링크를 $('Scrape a url and get its content'), 즉 커뮤니티 노드 이름으로 참조하고 있습니다. 대체 경로만 단독으로 실행하면 그 노드는 돈 적이 없어서 링크가 채워지지 않습니다. HTTP Request 경로를 쓴다면 이 참조를 자기 경로의 노드 이름으로 바꿔야 합니다. 노드를 복사해 갈래를 만들 때 $('노드 이름') 참조가 옛 갈래를 가리키고 있지 않은지 확인하는 습관이 여기서 나옵니다.
한계
- 하루 빠지면 비교가 끊깁니다 — 어제 행을 날짜 문자열로 찾기 때문에, 어떤 이유로 하루 실행이 안 되면 다음 날 어제 행이 없어 조용히 넘어갑니다. "가장 최근 행"을 읽도록 바꾸거나, 어제 행이 없을 때 알림을 따로 보내는 분기를 추가하는 게 다음 단계입니다.
- 내려갈 때만 알립니다 — If 조건이 "오늘이 어제보다 낮으면"이라, 세일이 끝나 가격이 다시 오른 날은 아무 알림이 없습니다. 오른 경우도 알고 싶으면 If 대신 Switch로 세 갈래를 만들면 됩니다.
- 셀렉터가 깨지면 끝입니다 — 사이트가 마크업을 바꾸면
rawPrice가 비어parseInt가NaN을 돌려주고, 시트에 이상한 값이 쌓입니다. Append 앞에 "숫자인지 확인"하는 If를 하나 두는 것이 실무형입니다. - 상품 한 개, 주소 하드코딩 — 여러 상품을 보려면 URL을 시트에 두고 Loop로 돌리는 구조로 바꿔야 합니다. 그때는 요청 수가 상품 수만큼 늘어나니 Firecrawl 크레딧과 상대 사이트 예절을 같이 계산하세요.
포트폴리오·면접 포인트
포트폴리오에는 세 장이면 됩니다. 403이 찍힌 HTTP Request 실행 화면, Firecrawl 노드가 HTML을 돌려준 화면, 그리고 며칠 쌓인 시트와 도착한 슬랙 DM입니다. 첫 장이 특히 중요합니다. 실패 화면을 보여주고 그다음에 해결을 보여주는 순서가, 처음부터 잘 된 것처럼 보이는 포트폴리오보다 훨씬 설득력이 있습니다.
응용은 두 방향입니다. 하나는 같은 구조로 대상만 바꾸는 것으로, 중고 거래 시세나 항공권처럼 자주 보는 가격이면 무엇이든 됩니다. 다른 하나는 위의 한계 중 하나를 고치는 것으로, "가장 최근 행과 비교"로 바꾸는 개선은 If 조건과 Get Row 노드만 손대면 되면서도 설계 감각을 보여줍니다.
면접에서 "403이 나면 어떻게 하나요?"라는 질문이 오면, User-Agent를 바꿔보는 건 첫 시도일 뿐이고 자바스크립트를 실행하는 브라우저 기반 스크래핑으로 넘어가야 한다는 것, 그리고 우회가 가능하더라도 요청 빈도와 약관을 지키는 선에서만 쓴다는 것을 같이 말하면 됩니다. 기술과 예절을 한 문장에 담을 수 있으면 데이터 수집을 맡겨도 되는 사람으로 보입니다.
다음 액션
이 편의 시트는 매일 행 하나가 쌓이는 단순한 이력이었습니다. 다음 편은 매일 9시에 대통령 일정 페이지를 수집해 시트에 쌓는 워크플로우인데, 처음 한 번은 과거 일정을 통째로 적재하고 그다음부터는 새로 생긴 일정만 증분으로 넣어야 해서 "최초 1회 적재"와 "매일 증분"을 어떻게 나누는지가 주제입니다. 이 편의 "어제 행 조회" 감각이 있으면 증분 로직이 훨씬 자연스럽게 읽힐 겁니다.
