한 줄 결론
원본 보관함과 정리된 상품 DB를 분리한다. 원본은 증거 보관용, 정리본은 검색·비교·추천용이다.
엑셀 파일 하나에 모든 걸 다 넣으려고 하면 처음에는 편하지만, 나중에 문제가 생긴다. 예를 들어 상품명에 마그네슘 350mg이라고 적혀 있어도 그게 1정 기준인지, 1일 섭취량 기준인지, 원료량인지 유효성분량인지 헷갈릴 수 있다.
그래서 처음 크롤링한 값은 raw_crawls에 그대로 넣고, 확인이 끝난 값만 products, offers, ingredient_facts 같은 정리된 저장칸에 넣는다.
제일 중요한 원칙: 애매한 값은 억지로 채우지 않는다. 빈칸은 null로 두고, flags에 “왜 애매한지”를 남긴다.
마트 영수증 예시
크롤링 데이터는 마트 영수증과 비슷하다. 영수증은 “무엇을 샀는지”의 증거지만, 그 자체가 깔끔한 가계부는 아니다.
영수증 같은 원본
- 상품명: 프리미엄 마그네슘 징코, 60정, 2개
- 가격: 31,400원
- 평점: 4.0
- 상품 URL, 수집 시간, 노출 순위
가계부 같은 정리본
- 제품명: 프리미엄 마그네슘 징코
- 총 개수: 120정
- 1정 가격: 약 262원
- 성분: 마그네슘, 징코 등
왜 둘 다 필요할까?
나중에 “2개 묶음 상품은 가격 계산을 다르게 해야겠다”라고 판단하면, 원본이 남아 있어야 다시 계산할 수 있다. 원본 없이 정리본만 있으면 어디서 틀렸는지 추적하기 어렵다.
MongoDB 적재용 중간산물 템플릿
이 파일은 사람이 엑셀을 펼쳐서 보는 최종 문서가 아니라, 크롤링 결과를 MongoDB JSON으로 변환하기 전 임시 입력 형식이다. 핵심은 상품 옵션 1줄과 성분 fact 여러 줄을 분리해, 이후 ETL에서 raw_crawls, products, offers, ingredient_facts로 안정적으로 변환하는 것이다.
MongoDB 중간산물 템플릿 다운로드
최소 운영은 01_crawl_items와 03_ingredient_rows 두 시트만 있어도 된다. 나머지 시트는 상세페이지/OCR까지 붙일 때 쓰면 된다.
| 시트 | 무엇을 저장하나 | 한 줄 기준 |
01_crawl_items | 검색 결과에서 바로 얻은 상품/가격/URL 원본 | 검색 결과에 노출된 상품 옵션 1개 |
02_product_review | 사람 또는 파서가 정리한 제품 카드 | 제품 1개 |
03_ingredient_rows | 성분명, 함량, 단위, 기준 | 제품의 성분 1개 |
04_raw_evidence | 상세페이지 텍스트, OCR, 이미지 URL, 캡처 경로 | 증거 자료 1개 |
01_crawl_items: 기존 엑셀을 계승하는 메인 시트
| 구분 | 컬럼 | 설명 |
| 수집 정보 | crawl_run_id, source, search_keyword, search_page, rank, collected_at | 언제, 어디서, 어떤 검색어로 나온 결과인지 추적 |
| 기존 상품 정보 | name, imageUrl, productUrl, productId, isAd | 기존 엑셀의 핵심 원본 컬럼 유지 |
| 옵션 ID | itemId, vendorItemId | 같은 productId 안에서도 1개/2개/판매자 옵션이 갈리므로 URL에서 따로 저장 |
| 가격/수량 | price, currency, tabletCount, bundleCount, totalUnitCount, pricePerUnit | 기존 pricePerTablet은 pricePerUnit으로 이름만 일반화 |
| 평판 | rating, reviewCount | 평점과 리뷰 수 |
| 기존 정리값 | companyName_raw, dosageForm_raw, kind_raw | 기존 회사이름, 제형, 종류 컬럼을 raw 값으로 유지 |
| 검수 | parse_status, flags, raw_record_json, memo | 애매한 점, 원본 JSON, 사람 메모 |
03_ingredient_rows: 성분은 긴 표로 저장
기존처럼 마그네슘, 셀레늄, 비타민B6 컬럼을 계속 늘리면 오메가3, 유산균, 홍삼, 루테인으로 갈수록 표가 너무 넓어진다. 대신 성분 하나를 한 줄로 저장한다.
| 컬럼 | 예시 | 뜻 |
product_key | coupang:8719232761 | 어느 제품의 성분인지 연결 |
ingredient_name_raw | 마그네슘 | 성분 원문 이름 |
ingredient_name_norm | magnesium | 검색용 표준 이름 |
amount_value, amount_unit | 350 / mg | 함량 숫자와 단위 |
basis | per_daily_serving | 1정당인지, 1회 섭취량당인지, 1일 섭취량당인지 |
form | glycinate | 글리시네이트, 시트레이트, 산화마그네슘 같은 원료형 |
source_text | 1일 섭취량당 마그네슘 350mg | 그 값을 뽑은 근거 문장 |
confidence, flags | 0.84 / ambiguous_basis | 자동 추출 신뢰도와 검수 필요 표시 |
예시: 한 제품에 성분 2개가 있으면?
01_crawl_items에는 상품이 1줄만 들어간다. 03_ingredient_rows에는 같은 product_key로 마그네슘 1줄, 징코 1줄을 넣는다. 이렇게 해야 성분이 2개든 20개든 양식이 깨지지 않는다.
MongoDB JSON 예시
아래 예시는 실제 저장 모양을 짧게 줄인 것이다. 기술적으로는 JSON이지만, 사람 눈으로 보면 “제품 카드 + 판매 정보 + 성분 목록 + 검수 표시”라고 보면 된다.
{
"schema_version": 1,
"parser_version": "supplement-parser-2026-06-09",
"source_raw_ids": ["raw_coupang_20260609_000001"],
"canonical_product_key": "coupang:8719232761",
"product": {
"source": "coupang",
"product_id": "8719232761",
"name_raw": "USPHARMATECH 프리미엄 마그네슘 징코, 60정, 2개",
"name_clean": "USPHARMATECH 프리미엄 마그네슘 징코",
"brand": "USPHARMATECH",
"dosage_form": "tablet",
"category_tags": ["magnesium", "ginkgo", "complex"],
"product_type": null
},
"serving": {
"unit_count": 60,
"bundle_count": 2,
"total_units": 120,
"units_per_serving": null,
"servings_per_day": null,
"days_supply": null,
"flags": ["serving_basis_unknown"]
},
"offers": [
{
"source": "coupang",
"item_id": "27364031247",
"vendor_item_id": "94887942792",
"url": "https://www.coupang.com/vp/products/8719232761?...",
"price": { "final": 31400, "currency": "KRW", "per_unit": 262 },
"rating": 4.0,
"review_count": 15,
"is_ad": false,
"rank": 2,
"collected_at": "2026-06-09T06:00:00Z"
}
],
"ingredients": [
{
"name_raw": "마그네슘",
"name_norm": "magnesium",
"category": "mineral",
"amount": { "value": null, "unit": "mg" },
"basis": "unknown",
"form": "unknown",
"source_text": "",
"confidence": 0.0,
"flags": ["missing_amount", "unknown_basis"]
}
],
"quality": {
"certifications": [],
"manufacturer": null,
"importer": null,
"origin_country": null
},
"safety": {
"allergens": [],
"warnings": [],
"interaction_tags": []
},
"evidence": {
"raw_name": "USPHARMATECH 프리미엄 마그네슘 징코, 60정, 2개",
"nutrition_panel_text_raw": "",
"product_notice_text_raw": "",
"ocr_text": "",
"image_urls": [],
"screenshot_paths": []
},
"flags": ["needs_detail_page", "needs_ocr", "ingredient_amount_incomplete"],
"created_at": "2026-06-09T06:00:00Z",
"updated_at": "2026-06-09T06:00:00Z"
}
핵심은 빈칸을 감추지 않는 것이다. MongoDB에는 알 수 없는 값을 null로 넣고, 왜 알 수 없는지를 flags로 남긴다. 예를 들어 함량 기준을 모르면 serving_basis_unknown, 상세페이지 확인이 필요하면 needs_detail_page를 붙인다.