웹 크롤링 데이터를 RAG에 연결하는 실전 가이드
"우리 회사 데이터로 답변하는 AI 챗봇 만들고 싶은데요." — 요즘 이런 요청, 정말 많이 듣습니다. ChatGPT가 똑똑한 건 맞지만, 우리 회사만의 최신 데이터를 기반으로 답변하게 하려면 RAG가 필요합니다. 그리고 RAG의 성능은 결국 데이터 품질에서 갈립니다.
이 글에서는 웹 크롤링으로 수집한 데이터를 RAG 파이프라인에 연결하는 전체 과정을 실전 코드와 함께 정리합니다.
목차
- RAG란 무엇인가?
- 전체 파이프라인 흐름
- Step 1: 크롤링 — 원본 데이터 수집
- Step 2: 청킹 — 텍스트를 적절한 크기로 분할
- Step 3: 임베딩 + 벡터DB 저장
- Step 4: RAG 쿼리 — LLM에게 질문하기
- 데이터 품질이 RAG 성능을 좌우한다
- 해시스크래퍼 MCP + LangChain 연동
- 마무리
RAG란 무엇인가?
RAG(Retrieval-Augmented Generation)는 간단히 말해 "검색 + 생성" 입니다.
LLM이 답변을 생성하기 전에, 외부 데이터베이스에서 관련 문서를 먼저 검색해서 컨텍스트로 제공하는 방식이죠. GPT가 학습하지 않은 최신 정보, 내부 문서, 도메인 특화 데이터를 활용할 수 있어서 환각(hallucination)을 크게 줄일 수 있습니다.
핵심은 이겁니다: LLM은 똑똑하지만, 모르는 건 모릅니다. RAG는 LLM이 "모르는 것"을 검색으로 보완하는 구조입니다.
전체 파이프라인 흐름
RAG 파이프라인의 전체 흐름은 5단계로 나뉩니다:
웹 크롤링 → 텍스트 청킹 → 임베딩 변환 → 벡터DB 저장 → LLM 쿼리
각 단계를 하나씩 코드와 함께 살펴보겠습니다.
Step 1: 크롤링 — 원본 데이터 수집
RAG의 출발점은 데이터입니다. 웹에서 수집한 제품 정보, 뉴스, 기술 문서, 커뮤니티 글 등이 모두 RAG의 소스가 될 수 있습니다.
여기서 중요한 건 깨끗한 텍스트 추출입니다. HTML 태그, 광고, 네비게이션 메뉴가 섞인 데이터를 그대로 넣으면 RAG 성능이 크게 떨어집니다.
import requests
from bs4 import BeautifulSoup
def crawl_page(url):
"""단순 크롤링 예시 — 실전에서는 봇 차단, JS 렌더링 등 고려 필요"""
response = requests.get(url, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
})
soup = BeautifulSoup(response.text, "html.parser")
# 본문 텍스트만 추출 (네비게이션, 광고 등 제거)
for tag in soup(["nav", "header", "footer", "script", "style", "aside"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
return text
하지만 현실은 이렇게 단순하지 않습니다. JavaScript로 렌더링되는 SPA, 로그인이 필요한 페이지, 봇 차단이 걸린 사이트에서는 requests만으로는 한계가 있죠.
Step 2: 청킹 — 텍스트를 적절한 크기로 분할
수집한 문서를 통째로 임베딩하면 검색 정확도가 떨어집니다. 적절한 크기의 청크(chunk)로 나누는 게 핵심입니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크당 최대 500자
chunk_overlap=50, # 청크 간 50자 겹침 (문맥 유지)
separators=["\n\n", "\n", ". ", " "] # 분할 우선순위
)
documents = splitter.split_text(crawled_text)
print(f"총 {len(documents)}개 청크 생성")
청킹 팁:
- 너무 작으면 문맥이 끊기고, 너무 크면 검색 정확도가 떨어집니다
- 한국어 텍스트는 500~800자 청크가 일반적으로 좋은 성능을 보입니다
- chunk_overlap을 설정해서 문장이 잘리는 것을 방지하세요
Step 3: 임베딩 + 벡터DB 저장
청크를 벡터(숫자 배열)로 변환하고, 벡터DB에 저장합니다. 여기서는 OpenAI 임베딩 + ChromaDB 조합을 사용합니다.
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 벡터DB에 저장
vectorstore = Chroma.from_texts(
texts=documents,
embedding=embeddings,
collection_name="crawled_data",
persist_directory="./chroma_db"
)
print(f"{len(documents)}개 청크를 벡터DB에 저장 완료")
다른 벡터DB(Pinecone, Weaviate, Qdrant 등)도 LangChain에서 동일한 인터페이스로 사용 가능합니다.
Step 4: RAG 쿼리 — LLM에게 질문하기
이제 사용자의 질문이 들어오면, 벡터DB에서 관련 문서를 검색하고 LLM에게 컨텍스트와 함께 전달합니다.
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
# LLM 설정
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# RAG 체인 구성
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)
# 질문하기
answer = qa_chain.invoke("이 제품의 최신 가격은 얼마인가요?")
print(answer["result"])
참고:
RetrievalQA는 LangChain의 레거시 Chain 인터페이스입니다. 최신 버전에서는 LangGraph 기반의 RAG 구성을 권장하지만, 개념 이해를 위해 가장 직관적인 이 방식으로 설명합니다.
이것이 RAG의 전체 흐름입니다. 크롤링 → 청킹 → 임베딩 → 벡터DB → LLM 쿼리. 구조 자체는 단순하죠.
데이터 품질이 RAG 성능을 좌우한다
RAG에서 가장 과소평가되는 부분이 입력 데이터의 품질입니다.
아무리 좋은 임베딩 모델과 LLM을 써도, 입력 데이터가 지저분하면 결과도 지저분합니다. 실제로 RAG 성능 이슈의 대부분은 모델이 아니라 데이터 문제에서 발생합니다.
흔한 데이터 품질 문제:
- 네비게이션, 사이드바 텍스트가 본문과 섞임 → 검색 시 노이즈 발생
- 불완전한 크롤링 (JS 렌더링 실패) → 핵심 정보 누락
- 중복 데이터 → 검색 결과 편향
- 인코딩 깨짐, 특수문자 오류 → 임베딩 품질 저하
Garbage In, Garbage Out. 크롤링 단계에서 깨끗한 데이터를 확보하는 것이 RAG 파이프라인 전체의 성패를 결정합니다.
해시스크래퍼 MCP + LangChain 연동
직접 크롤러를 만들면 봇 차단, JS 렌더링, 프록시 관리 등 부수적인 문제에 시간을 빼앗기게 됩니다. 해시스크래퍼의 MCP(Model Context Protocol) 서버를 활용하면 크롤링 인프라 걱정 없이 RAG 파이프라인에 집중할 수 있습니다.
# 해시스크래퍼 MCP를 LangChain Document Loader로 활용하는 예시
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# 해시스크래퍼 API로 데이터 수집 — 봇 차단 사이트도 OK
import requests
response = requests.post(
"https://api.hashscraper.com/api/scrape",
json={
"url": "https://target-site.com/products",
"format": "markdown" # markdown 또는 text
},
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json; version=2"
}
).json()
# LangChain Document로 변환
docs = [
Document(
page_content=response["data"]["content"],
metadata={
"url": response["data"]["url"],
"title": response["data"]["title"]
}
)
]
# 이후는 동일: 청킹 → 임베딩 → 벡터DB → RAG
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
vectorstore = Chroma.from_documents(chunks, embeddings)
해시스크래퍼를 RAG 데이터 소스로 쓰는 이유:
- 봇 차단 사이트에서도 안정적으로 깨끗한 텍스트 추출
- JavaScript 렌더링, 로그인 처리 자동화
- 프록시 관리, IP 로테이션 내장 — 인프라 신경 쓸 필요 없음
- 정형화된 데이터 출력 → 청킹/임베딩 품질 향상
마무리
RAG 파이프라인은 구조적으로는 단순합니다. 하지만 실전에서 성능을 좌우하는 건 결국 첫 번째 단계인 데이터 수집의 품질입니다.
크롤러를 직접 만들고 유지보수하는 데 시간을 쏟는 대신, 검증된 크롤링 인프라 위에서 RAG 로직에 집중하는 것이 더 효율적인 선택일 수 있습니다.
크롤링 데이터 기반 RAG 파이프라인 구축이 필요하시다면, 해시스크래퍼에서 상담을 받아보세요. 데이터 수집부터 AI 연동까지, 실전 경험을 바탕으로 도와드립니다.




