쉬운 저장 구조 안내 · 2026-06-09

영양제 크롤링 데이터, 어떻게 저장하면 나중에 덜 고생할까?

핵심은 간단하다. 처음 긁어온 원본은 버리지 않고 그대로 보관하고, 사람이 보기 좋은 정리본은 따로 만든다. 그래야 나중에 성분 파서가 좋아지거나 가격 계산 방식이 바뀌어도 처음부터 다시 크롤링하지 않고 다시 정리할 수 있다.

원본 보관 정리본 생성 성분은 배열로 저장 모르는 값은 모른다고 표시
1단계원본긁어온 그대로 저장
2단계정리본검색·비교하기 좋게 가공
성분목록마그네슘, 비타민D 등을 배열로
검수표시단위 없음, 기준 불명확 등을 태그로

한 줄 결론

원본 보관함정리된 상품 DB를 분리한다. 원본은 증거 보관용, 정리본은 검색·비교·추천용이다.

엑셀 파일 하나에 모든 걸 다 넣으려고 하면 처음에는 편하지만, 나중에 문제가 생긴다. 예를 들어 상품명에 마그네슘 350mg이라고 적혀 있어도 그게 1정 기준인지, 1일 섭취량 기준인지, 원료량인지 유효성분량인지 헷갈릴 수 있다.

그래서 처음 크롤링한 값은 raw_crawls에 그대로 넣고, 확인이 끝난 값만 products, offers, ingredient_facts 같은 정리된 저장칸에 넣는다.

제일 중요한 원칙: 애매한 값은 억지로 채우지 않는다. 빈칸은 null로 두고, flags에 “왜 애매한지”를 남긴다.

마트 영수증 예시

크롤링 데이터는 마트 영수증과 비슷하다. 영수증은 “무엇을 샀는지”의 증거지만, 그 자체가 깔끔한 가계부는 아니다.

영수증 같은 원본

  • 상품명: 프리미엄 마그네슘 징코, 60정, 2개
  • 가격: 31,400원
  • 평점: 4.0
  • 상품 URL, 수집 시간, 노출 순위

가계부 같은 정리본

  • 제품명: 프리미엄 마그네슘 징코
  • 총 개수: 120정
  • 1정 가격: 약 262원
  • 성분: 마그네슘, 징코 등

왜 둘 다 필요할까?

나중에 “2개 묶음 상품은 가격 계산을 다르게 해야겠다”라고 판단하면, 원본이 남아 있어야 다시 계산할 수 있다. 원본 없이 정리본만 있으면 어디서 틀렸는지 추적하기 어렵다.

용어 쉽게 풀기

용어쉬운 뜻이 페이지에서의 예시
raw긁어온 원본 그대로쿠팡 검색 결과 row, 상품명 원문, 원본 가격
normalized나중에 검색하기 좋게 정리한 값브랜드, 제품명, 총 정 수, 성분 목록
collectionMongoDB 안의 저장칸원본칸, 제품칸, 가격칸처럼 나눠 둠
ETL원본을 읽고, 정리하고, DB에 넣는 작업엑셀 row → 제품 문서 → 성분 문서
upsert있으면 업데이트, 없으면 새로 저장같은 상품을 다시 크롤링하면 가격만 갱신
flags검수 필요 표시단위 없음, 1일 기준 불명확, OCR 필요

현재 크롤링 파일을 보는 법

원본으로 그대로 보관할 값

  • 상품명, URL, productId, itemId, vendorItemId
  • 가격, 단위수 추정, 묶음수, 평점, 리뷰수, 광고 여부
  • 검색 키워드, 노출 순위, 수집 시각, 소스 URL
  • 파싱 전 원본 레코드 전체

그대로 믿으면 위험한 값

  • 상품명에서 추정한 정/캡슐 수
  • 상품명에서 추정한 성분 함량
  • 단위 없는 숫자: 예 마그네슘: 350
  • 브랜드/회사/제조사처럼 상세페이지 확인이 필요한 값

현재 파일은 최종 DB라기보다 재료에 가깝다. 상품명에서 뽑은 값은 “후보”로만 저장하고, 상세페이지·상품고시·OCR에서 확인한 값이 들어오면 그때 정식 값으로 승격한다.

저장칸 5개

처음부터 하나의 큰 표에 몰아넣지 말고, 역할별로 저장칸을 나누는 편이 좋다.

저장칸쉬운 역할들어가는 값
raw_crawls 원본 창고 검색 결과 JSON, 엑셀 row, 상세페이지 raw text, OCR text, 이미지 URL, 캡처 경로
products 제품 카드 브랜드, 제품명, 제형, 섭취 기준, 카테고리 태그, 품질/주의 태그
offers 판매 조건과 가격 source, itemId, vendorItemId, 가격, 배송, 쿠폰, 묶음, 수집시각
ingredient_facts 성분 검색용 목록 제품별 성분명, 함량, 단위, 기준, 화학형, source_text, confidence
parse_jobs 작업 일지 parser_version, input_hash, status, flags, error, reprocess_needed

저장 규칙

원본 문서 규칙

  • 원본 row를 가능한 그대로 저장
  • source, keyword, rank, collected_at, crawl_run_id 포함
  • HTML/text/OCR/image evidence를 함께 저장
  • parser가 실패해도 raw는 삭제하지 않음

정리본 문서 규칙

  • 타입을 명확히 함: 숫자는 number, 날짜는 Date, 태그는 array
  • 불명확한 값은 빈 문자열 대신 nullflags로 저장
  • 어떤 원본에서 나온 값인지 source_raw_ids로 연결
  • 같은 입력을 여러 번 돌려도 중복 저장되지 않게 설계

제품 문서에 있으면 좋은 큰 묶음

schema_versionparser_versionsource_raw_idscanonical_product_keyproductservingingredientsqualitysafetyevidenceflagscreated_atupdated_at

성분 저장법

영양제 DB에서 제일 자주 바뀌고 복잡한 부분은 성분이다. 그래서 성분을 마그네슘, 비타민D, EPA 같은 고정 컬럼으로 만들면 금방 막힌다.

추천 방식: 제품 안에 ingredients라는 성분 목록을 넣는다. 성분이 1개면 1줄, 20개면 20줄이다.

예시

“마그네슘 350mg, 비타민D 1000IU” 제품이라면 성분 목록에 두 줄을 넣는다. 나중에 “마그네슘 들어간 제품만 보기”, “비타민D 함량 높은 순서로 보기”가 쉬워진다.

필드쉬운 설명예시
name_raw원문 성분명마그네슘
name_norm검색용 표준 이름magnesium
category성분 분류mineral, vitamin, fatty_acid, probiotic
amount.value수치350
amount.unit단위mg, mcg, IU, CFU
basis무엇을 기준으로 한 함량인지1정당, 1회 섭취량당, 1일 섭취량당, 모름
form화학형/원료형glycinate, citrate, oxide, D3, MK-7
source_text파싱 근거 원문1일 섭취량당 마그네슘 350mg
confidence자동 추출 신뢰도0.84
flags검수 플래그missing_unit, ambiguous_basis

중복 방지 기준

같은 상품을 여러 번 크롤링해도 DB에 똑같은 줄이 계속 쌓이면 안 된다. 그래서 “이건 같은 상품이다”라고 판단할 기준이 필요하다.

같은 것으로 보는 기준

  • raw_crawls: source + crawl_run_id + row_hash
  • products: canonical_product_key
  • offers: source + item_id + vendor_item_id + collected_at_bucket
  • ingredient_facts: product_id + name_norm + basis + form

빨리 찾기 위한 인덱스

  • 제품명/브랜드 검색
  • 성분명 검색
  • 카테고리와 검수 플래그 검색
  • 판매처와 가격 정렬
  • { "updated_at": -1 }
상품명만 보고 같은 제품이라고 합치면 위험하다. 같은 제품이라도 60정 1개, 60정 2개, 판매자별 가격이 다를 수 있으므로 제품 정보와 판매 옵션은 분리한다.

MongoDB 적재용 중간산물 템플릿

이 파일은 사람이 엑셀을 펼쳐서 보는 최종 문서가 아니라, 크롤링 결과를 MongoDB JSON으로 변환하기 전 임시 입력 형식이다. 핵심은 상품 옵션 1줄성분 fact 여러 줄을 분리해, 이후 ETL에서 raw_crawls, products, offers, ingredient_facts로 안정적으로 변환하는 것이다.

MongoDB 중간산물 템플릿 다운로드

최소 운영은 01_crawl_items03_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기존 엑셀의 핵심 원본 컬럼 유지
옵션 IDitemId, 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_keycoupang:8719232761어느 제품의 성분인지 연결
ingredient_name_raw마그네슘성분 원문 이름
ingredient_name_normmagnesium검색용 표준 이름
amount_value, amount_unit350 / mg함량 숫자와 단위
basisper_daily_serving1정당인지, 1회 섭취량당인지, 1일 섭취량당인지
formglycinate글리시네이트, 시트레이트, 산화마그네슘 같은 원료형
source_text1일 섭취량당 마그네슘 350mg그 값을 뽑은 근거 문장
confidence, flags0.84 / ambiguous_basis자동 추출 신뢰도와 검수 필요 표시

예시: 한 제품에 성분 2개가 있으면?

01_crawl_items에는 상품이 1줄만 들어간다. 03_ingredient_rows에는 같은 product_key로 마그네슘 1줄, 징코 1줄을 넣는다. 이렇게 해야 성분이 2개든 20개든 양식이 깨지지 않는다.

작업 흐름

1원본 저장검색 결과와 엑셀 row를 그대로 보관
2증거 보강상세페이지, 이미지, OCR 텍스트 추가
3제품 정리브랜드, 제품명, 개수, 가격 분리
4성분 추출성분명, 함량, 단위, 기준 저장
5검수 표시모호한 값에 flags를 붙임
단계하는 일저장 위치
1. ingestJSON/엑셀/CSV row를 그대로 읽고 저장raw_crawls
2. enrich상세페이지, 상품고시, 이미지 OCR, 원문 텍스트 추가raw_crawls.evidence
3. normalize상품명/브랜드/제형/섭취 기준/가격 정리products, offers
4. parse ingredients성분표를 성분 목록으로 변환products.ingredients, ingredient_facts
5. validate단위 없음, 기준 불명, OCR 필요 같은 문제 표시flags, parse_jobs
6. reprocess파서가 개선되면 원본에서 다시 정리본 생성기존 문서 업데이트, 이력 유지

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를 붙인다.

실행 체크리스트

먼저 만들 것

  • raw_crawls에 원본 row 저장
  • 상품마다 원본을 다시 찾을 수 있는 ID 저장
  • 제품 정보와 판매 옵션을 분리
  • 성분은 고정 컬럼이 아니라 배열로 저장

나중에 확장할 것

  • 상세페이지 OCR로 성분표 보강
  • 성분 표준명 사전 만들기
  • 함량 기준 자동 판별
  • 검수 필요한 상품만 모아 보는 화면 만들기