코드 없이 베스트셀러 긁기 — n8n HTML 노드로 배우는 웹 스크래핑
이 글과 연결된 글 보기
드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.표시된 글 7편 목록
API를 제공하지 않는 사이트라도, n8n의 HTTP Request 노드로 페이지 HTML을 받아 HTML 노드에서 CSS 선택자로 원하는 부분만 뽑으면 코드 한 줄 없이 데이터를 모을 수 있습니다. 이 글의 워크플로우는 온라인 서점 베스트셀러 페이지에서 24권의 순위·제목·저자·가격을 뽑아 매주 일요일 아침 구글시트에 한 주치씩 쌓습니다. 한 번 만들어두면 URL과 선택자만 바꿔 채용 공고, 공지사항, 상품 목록 같은 "목록 페이지"는 전부 같은 방식으로 긁을 수 있습니다.
이 워크플로우 하나로 보여줄 수 있는 것
구글 뉴스 RSS 편에서는 사이트가 미리 정리해 둔 피드를 받아왔습니다. RSS는 제목·링크·날짜가 이미 필드로 나뉜 구조화된 데이터입니다. 문제는 대부분의 사이트가 RSS도 API도 주지 않는다는 점이고, 그럴 때는 사람이 보는 HTML을 그대로 받아 필요한 부분을 직접 잘라내야 합니다. 그게 웹 스크래핑입니다.
이 워크플로우로 면접에서 설명할 수 있는 역량은 세 가지입니다.
- API 없는 데이터 수집 — HTTP Request로 HTML을 통째로 받고, 원하는 데이터의 위치를 CSS 선택자로 특정해본 경험
- 목록 → 아이템 변환 — 페이지 하나에 담긴 24권을 배열로 뽑은 뒤 아이템 24개로 풀어, 이후 노드가 한 권씩 처리하게 만든 경험
- 파생 필드 설계 — 뽑은 상품 링크에서 번호를 잘라내고, 그 번호로 이미지 URL과 상세 페이지 URL을 조립해본 경험
배경 — 매주 베스트셀러를 시트에 쌓아두고 싶었다
이 워크플로우는 데이터팝콘 n8n 인스턴스에 교육용으로 만들어 둔 것입니다. 캔버스 맨 위 스티키 노트에 목적이 세 줄로 적혀 있습니다. "매주 일요일 아침 9시에 실행, 베스트셀러 영역 부분을 가져온다, 구글시트에 적재." 그 아래에 "후속: 구글시트에 저장된 내용을 바탕으로 질의응답하기"라는 확장 계획도 붙어 있습니다.
베스트셀러 목록은 매주 바뀌지만 사이트는 지난주 순위를 보여주지 않습니다. 시트에 주차별로 쌓아두면 "이 책이 몇 주째 10위 안에 있나" 같은 질문에 답할 수 있게 됩니다. 설치나 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 있으니, 여기서는 스크래핑에 필요한 부분만 다룹니다.
구조 — Trigger / Task / Target
- Trigger — 매주 일요일 오전 9시. 테스트용 Manual Trigger도 같은 HTTP Request 노드에 붙어 있습니다
- Task — HTML 받기 → 책 블록을 배열로 → 아이템으로 분리 → 블록마다 필드 8개 추출 → 컬럼 정리
- Target — 구글시트에 행 추가. 같은 데이터를 CSV로 메일에도 첨부합니다
만들기
원본 캔버스에는 만든 사람이 적어 둔 체크리스트 스티키가 있습니다. 첫 줄이 "Bot이 접근을 허용하는지 확인하기. URL/robots.txt로 확인 가능"이고, 끝에 "인증(로그인)이 필요 없어야 함", "동적 웹페이지 안 됨"이라는 조건이 붙어 있습니다. 여기에 세 가지를 더합니다.
- 요청 빈도 — 이 워크플로우는 주 1회, 페이지 1장입니다. 분 단위로 돌리면 상대 서버에 부담이고 차단 대상이 됩니다
- 용도 — 개인 학습·포트폴리오용과 상업 이용은 다릅니다. 긁은 데이터를 서비스에 쓰려면 사이트 이용약관을 먼저 읽으세요
- 깨짐 전제 — 사이트가 HTML 구조를 바꾸면 선택자는 그날로 멈춥니다. 스크래핑은 "만들고 끝"이 아니라 "깨지면 고치는" 자동화입니다
1. HTTP Request — 페이지 HTML 통째로 받기
HTTP Request 노드에 베스트셀러 페이지 URL을 넣고 GET으로 실행합니다. 원본은 https://www.yes24.com/Product/Category/BestSeller?categoryNumber=001&pageNumber=1&pageSize=24입니다. 쿼리 파라미터가 카테고리 001(종합), 1페이지, 24권이라는 뜻이라, 다른 카테고리나 2페이지를 긁고 싶으면 이 숫자만 바꾸면 됩니다.
실행하면 출력에 data 필드 하나가 생기고 그 안에 페이지 HTML 전체가 문자열로 들어옵니다. 캔버스에 고정된 실행 결과를 보면 약 58만 자입니다. 원본 노드에는 Ignore SSL Issues 옵션이 켜져 있는데, 인증서가 정상인 사이트라면 끄고 시작해도 됩니다.
2. 브라우저에서 CSS 선택자 찾기
HTML 노드에 넣을 선택자는 브라우저 검사 도구에서 찾습니다. 페이지에서 책 제목 하나를 우클릭 → 검사(Inspect)를 누르면 Elements 패널이 해당 요소 위치로 열립니다. 거기서 위로 거슬러 올라가면 책 한 권이 <li> 하나이고, 그 <li>들이 <ul id="yesBestList"> 안에 모여 있는 구조가 보입니다.
그래서 "책 목록 전체"를 가리키는 선택자는 #yesBestList > li입니다. #은 id, >는 바로 아래 자식이라는 뜻입니다. 개별 요소는 Elements 패널에서 우클릭 → Copy → Copy selector로 복사할 수 있는데, 이렇게 복사한 선택자는 페이지 최상단부터 시작하는 긴 경로라 필요한 부분만 남기는 편이 안전합니다. 원본의 선택자들도 <li> 내부에서 시작하도록 다듬어져 있습니다.
3. HTML 노드 — 책 블록 24개를 배열로
HTML 노드의 Operation을 Extract HTML Content로 두고, Source Data는 JSON, JSON Property는 data(HTTP Request가 HTML을 담아준 필드)로 설정합니다. Extraction Values에는 항목 하나만 넣습니다.
| Key | CSS Selector | Return Value | Return Array |
|---|---|---|---|
books | #yesBestList > li | HTML | 켬 |
Return Value를 Text가 아니라 HTML로 두는 게 포인트입니다. 텍스트로 뽑으면 제목·저자·가격이 한 줄로 뭉개져서 다시 나눌 수 없고, HTML로 뽑아야 다음 노드에서 그 안에 다시 선택자를 쓸 수 있습니다. Return Array를 켜면 24개가 각각 배열 원소가 되고, 끄면 하나로 이어 붙인 문자열이 됩니다.
4. Split Out — 배열을 아이템 24개로
3단계의 출력은 아이템 1개에 books 배열(원소 24개)이 들어 있는 형태입니다. Split Out 노드에서 Field To Split Out을 books로 두면 원소 하나가 아이템 하나가 되어 24개로 풀리고, 각 아이템의 books 필드에 책 한 권 분량의 HTML 조각이 남습니다.
이 단계가 HTML 노드를 두 번 쓰는 이유입니다. 페이지 전체에서 제목 24개, 가격 24개를 따로 뽑으면 두 배열을 다시 짝지어야 하고, 어떤 책에 가격 표시가 없으면 순서가 밀려 엉킵니다. 먼저 책 단위로 잘라 아이템을 만들면 그 뒤로는 "아이템 하나 = 책 한 권"이 보장되어 필드끼리 어긋날 일이 없습니다.
5. HTML 노드 — 제목·저자·가격 뽑기
두 번째 HTML 노드도 Extract HTML Content인데, 이번에는 JSON Property를 books로 둡니다. 아이템마다 들어 있는 HTML 조각 안에서 뽑는 것이라 선택자는 <li> 내부 기준입니다. 원본의 항목은 여덟 개입니다.
| Key | CSS Selector | Return Value |
|---|---|---|
ranking | div>div.item_img>div.img_canvas>div>em | Text |
title | div>div.item_info>div.info_row.info_name>a.gd_name | Text |
author | div>div.item_info>div.info_row.info_pubGrp>span.authPub.info_auth>a | Text |
Publisher | div>div.item_info>div.info_row.info_pubGrp>span.authPub.info_pub>a | Text |
Publication Date | div>div.item_info>div.info_row.info_pubGrp>span.authPub.info_date | Text |
price | div>div.item_info>div.info_row.info_price>strong>em | Text |
rating | div>div.item_info>div.info_row.info_rating>span.rating_grade>em | Text |
goods_no | div>div.item_info>div.info_row.info_name>a.gd_name | Attribute: href |
마지막 goods_no만 Return Value가 Attribute입니다. 제목 링크의 href를 가져오는 것으로, 고정된 실행 결과에는 /product/goods/152298374 같은 상대 경로가 들어 있습니다. 같은 요소에서 Text로는 제목을, Attribute로는 링크를 뽑는 식입니다. 책 한 권 안에 제목은 하나라 Return Array는 끕니다. 실행하면 아이템마다 title, price("16,920"처럼 쉼표 포함 문자열), ranking("1")이 채워지고, 여기까지가 스크래핑입니다.
6. Set — 컬럼 이름 정리와 파생 필드
Edit Fields(Set) 노드에서 시트에 넣을 컬럼 10개를 정리합니다. 대부분은 이름만 바꾸는 매핑이고(Publisher → publisher, Publication Date → pub_date), 눈여겨볼 건 표현식이 들어간 네 개입니다.
standard_range = {{ $now.minus(7,'days').format('yyyy.MM.dd') }}~{{ $now.minus(1,'day').format('yyyy.MM.dd') }}
goods_no = {{ $json.goods_no.split('/')[3] }}
image_url = =IMAGE("https://image.yes24.com/goods/{{ $json.goods_no.split('/')[3] }}")
goods_url = https://www.yes24.com/product/goods/{{ $json.goods_no.split('/')[3] }}
standard_range는 실행일 기준 지난 7일 구간으로, 매주 24행씩 쌓이는 시트에서 어느 주 데이터인지 구분하는 키입니다. goods_no는 /product/goods/152298374를 /로 자르면 네 번째(인덱스 3)가 상품 번호입니다. 이 번호로 상세 페이지 URL과 표지 이미지 URL을 조립하고, image_url은 아예 구글시트의 =IMAGE() 수식으로 만들어 셀에 표지가 바로 보이게 했습니다. 5단계에서 뽑은 rating은 여기서 매핑하지 않아 시트에 들어가지 않는데, 남기고 싶으면 필드 하나만 추가하면 됩니다.
7. Google Sheets — Append, Map Automatically
시트에 헤더 행으로 standard_range, ranking, goods_no, image_url, goods_url, title, price, author, publisher, pub_date를 만들어 두고, Google Sheets 노드를 Append Row, Mapping Column Mode는 Map Automatically로 둡니다. Set 노드의 필드 이름이 헤더와 같으면 그대로 들어갑니다. 원본은 Cell Format 옵션을 기본값(Let Google Sheets format)으로 두었는데, image_url이 수식으로 계산되지 않고 문자열로 보이면 이 옵션부터 확인하세요.
Set 노드 뒤에는 Google Sheets와 나란히 Convert to File(CSV) → Gmail 노드가 붙어 있습니다. 같은 24행을 CSV로 만들어 "[지난주 구간] 종합 베스트"라는 제목으로 본인 메일에 첨부해 보내는 흐름입니다. 시트가 원본이고 메일은 알림 겸 백업입니다.
8. Schedule Trigger — 일요일 9시, 그리고 시간대
Schedule Trigger의 Trigger Interval을 Weeks로 두고 Trigger on Weekdays에서 Sunday, Trigger at Hour를 9로 설정합니다. 원본 노드 이름이 Sun(9AM)이라 캔버스에서 바로 읽힙니다. 스케줄은 워크플로우 설정의 시간대를 따르므로 n8n cloud에서 서울로 잡혀 있는지 한 번 확인해두세요. 개발 중에는 옆에 붙은 Manual Trigger로 실행하고, 시트에 24행이 들어가는 걸 확인한 뒤 Active를 켭니다.
원본 캔버스에 남아 있는 흔적 — 계획과 결과의 차이, 그리고 두 번째 사이트
이 워크플로우는 운영 데이터가 없는 교육용이라, 대신 캔버스에 남은 흔적 세 가지를 옮깁니다. 셋 다 스크래핑을 처음 해보면 겪는 일입니다.
첫째, 계획한 필드와 실제 뽑은 필드가 다릅니다. TASK 영역 스티키에는 "랭킹, 책제목, 저자, 가격, 판매지수, 태그"가 적혀 있는데, 완성된 노드에는 판매지수와 태그가 없고 대신 출판사·출간일·평점이 들어갔습니다. 페이지를 열어 선택자를 찾다 보면 "있을 줄 알았는데 없는 값"과 "생각 못 했는데 쉽게 뽑히는 값"이 생깁니다. 스티키의 "원하는 결과 나올 때까지 계속 반복해서 테스트"라는 줄이 그 과정입니다.
둘째, 같은 패턴을 다른 사이트에 적용한 흔적입니다. "쿠팡 상품 스크래핑" 스티키 아래에 HTTP Request → HTML(#product-list > li, 배열) → HTML(상품명) 세 노드가 본 흐름과 분리되어 있고, 고정된 실행 결과에 베이커리 상품명 60개가 남아 있습니다. 목록을 배열로 자르고 아이템 안에서 뽑는 구조가 사이트가 달라도 그대로라는 데모인데, 상품명 선택자가 div.ProductUnit_productName__gre7e처럼 무작위 접미사가 붙은 클래스라 사이트를 다시 빌드하면 바뀌기 쉽습니다. 이 분기는 시트까지 연결되지 않은 채 남아 있습니다.
셋째, 스티키의 "후속" 계획이 실제로 붙어 있습니다. Chat Trigger → AI Agent(OpenAI gpt-4.1-mini)에 Google Sheets 도구(Get rows)가 연결되어, 채팅으로 "이번 주 1위가 뭐야?"라고 물으면 에이전트가 시트를 읽고 답합니다. 시스템 프롬프트는 "사용자의 질문에 답을 해주는 유용한 비서입니다. 최대한 불렛포인트로 답변해주세요" 정도가 전부입니다. 스크래핑으로 쌓은 시트가 그대로 에이전트의 지식이 되는 예입니다.
한계
- 구조 변경에 취약 — 사이트가 클래스 이름이나 목록 구조를 바꾸면 선택자가 빈 값을 돌려줍니다. 에러 없이 빈 행이 조용히 쌓이는 게 더 문제라, "title이 비어 있으면 중단" 같은 IF 조건을 시트 앞에 두는 게 좋습니다
- 1페이지 24권만 —
pageNumber=1이 고정입니다. 100위까지 모으려면 페이지 번호를 바꿔가며 반복해야 하는데, 급식 API 페이지네이션 편에서 다룹니다 - 동적 페이지 불가 — 자바스크립트로 목록을 나중에 그리는 사이트는 HTTP Request가 받은 HTML에 데이터가 없습니다. 검사 도구에서는 보이는데 HTML 노드가 못 찾으면 대부분 이 경우입니다
- 중복 적재 — 같은 주에 두 번 실행하면 24행이 한 번 더 쌓입니다.
standard_range와goods_no를 묶어 Append or Update로 바꾸면 해결됩니다 - 가격이 문자열 —
16,920처럼 쉼표가 있어 바로 계산하기 어렵습니다. Set 노드에서{{ $json.price.replace(/,/g, '') }}로 숫자화하면 됩니다
포트폴리오·면접 포인트
이 구조는 "목록 페이지"라면 무엇이든 재료가 됩니다. 채용 사이트의 공고 목록, 관심 기관의 공지사항, 중고 거래 검색 결과처럼 본인 관심사에 맞는 사이트로 바꿔 만들면 그 자체가 포트폴리오 항목입니다. 캔버스 스크린샷과 주차별로 쌓인 시트 캡처에 robots.txt를 확인한 화면 한 장을 더하면 "긁을 줄 안다"와 "긁어도 되는지 확인할 줄 안다"를 동시에 보여줄 수 있습니다.
면접에서 "왜 HTML 노드를 두 번 썼나요?"라는 질문이 오면, 제목과 가격을 따로 뽑으면 두 배열을 다시 짝지어야 하고 하나라도 비면 어긋나기 때문에 먼저 책 단위로 잘라 아이템을 만든 뒤 그 안에서 뽑았다고 답하면 됩니다. "API가 있으면 API를 쓰고, 없을 때만 스크래핑하되 robots.txt와 요청 빈도를 지켰다"는 한 문장을 덧붙이면 도구가 아니라 경계를 아는 사람으로 보입니다.
다음 액션
이 워크플로우의 전제는 HTTP Request가 HTML을 순순히 돌려준다는 것입니다. 그런데 많은 상거래 사이트는 n8n 같은 서버의 요청에 403을 돌려주고, 그 순간 이 글의 1단계부터 막힙니다. 다음 편은 바로 그 상황에서 시작합니다. 403이 뜨는 사이트의 가격을 우회 도구로 받아와 어제 값과 비교하고 변동이 있을 때만 알림을 보내는 워크플로우인데, 뒤쪽 구조는 이 글과 같고 앞쪽 "HTML을 받는 방법"만 달라집니다.
