쿠팡 리뷰기반 추천시스템 (문제 정의, 목표 설계, 데이터 수집, 정제)
문제 정의 & 목표 설계



해결하고자 하는 문제
온라인 쇼핑몰에는 방대한 리뷰데이터가 지속적으로 축적되고 있으나,
고객의 개인별 특성이 리뷰 텍스트에 충분히 반영되어 있음에도 불구하고 이를 체계적으로 분석하여 개인 맞춤형 상품 추천에 활용하는 데에는 한계가 있다.
현재의 추천 시스템은 주로 판매량, 별점 평균, 인기 순위에 의존하고 있어 개인별 니즈를 정밀하게 반영하지 못하며,
그 결과 고객은 자신에게 적합하지 않은 제품을 추천받는 경험을 하게된다.
특히 화장품은 피부 타입과 취향에 따른 만족도 편차가 크기 때문에 이러한 문제는 구매 실패와 재구매율 저하로 이어질 수 있다.
본 프로젝트는 화장품 리뷰 텍스트를 기반으로 감성 분석과 키워드 유사도 분석을 수행하여,
설명 가능한 상품 유사도 추천 시스템을 구축하는 것을 목표로 한다.
기대 효과
- 리뷰 감성 분석을 통한 부정 이슈 탐지 및 피드백
- 리뷰 텍스트 기반 감성 분석과 키워드 유사도를 활용한 설명 가능한 추천 결과 제공
- 단순 별점·판매량 중심 추천 방식의 한계 보완
- 화장품 구매 실패 감소 및 고객 만족도 향상
- 리뷰 데이터의 실질적 비즈니스 활용 사례 제시
소주제
- 이미지 유무에 따른 감성분류, 평점, 글 길이간의 상관관계 분석
- 배송 유형에 따른 감성분류, 평점 차이 분석
- 이 리뷰가 도움됐어요’의 개수와 감성분류, 평점, 글 길이간의 상관관계
- 시계열 감성 지수 모니터링을 통한 부정 이슈 조기 탐지
청사진
Streamlit 대시보드에서 상품 검색 or 드롭 다운으로 선택시
선택한 상품과 리뷰 감성이 긍정적이며 유사도가 높은 화장품 추천
추천 근거가 되는 주요 키워드 및 감성 점수 시각화
데이터 수집
화장품 관련 약 60개 카테고리에서
카테고리당 5만개 정도의 리뷰를 수집
데이터 구조
{
"search_name": "string",
"total_collected_reviews": "int",
"total_text_reviews": "int",
"total_product": "int",
"total_rating_distribution": {
"5": "int",
"4": "int",
"3": "int",
"2": "int",
"1": "int"
},
"data": [
{
"product_info": {
"product_id": "string",
"brand": "string",
"category_path": "string",
"product_name": "string",
"price": "string",
"delivery_type": "string",
"total_reviews": "string",
"product_url": "string",
"rating_distribution": {
"5": "int",
"4": "int",
"3": "int",
"2": "int",
"1": "int"
}
},
"reviews": {
"total_count": "int",
"text_count": "int",
"data": [
{
"id": "int",
"score": "int",
"date": "string",
"collected_at": "string",
"nickname": "string",
"has_image": "boolean",
"helpful_count": "int",
"title": "string",
"content": "string",
"full_text": "string"
}
]
}
}
]
}
문제점
1. postman에서의 단순 get 요청은 거부 당함
-> 유저 인증 토큰 넣어서 get 요청
2. 단순 get으로 가져온 데이터는 상품 정보는 가져 오지만 리뷰데이터는 가져 오지 않음
-> 리뷰는 10개씩 끊어서 쿠팡에서 자체적으로 가져옴. 리뷰의 페이지의 다음페이지 눌렀을때 비동기 요청으로 일반 유저가 알 수 없는 api로 요청을 보내기 때문
-> 셀레니움으로 페이지 하나하나 넘기면서 리뷰 수집
3. 각 스크립트 끝날때 driver.quit() 하면 오류
-> 다른데서 driver 써야하는데 driver 꺠우기 전에 실행되므로 다른 함수 호출시 drive 넘겨서 재사용
4. 크롤링에 시간이 오래 걸림
-> ThreadPoolExecutor를 이용한 병렬 수집을 구현 했으나 병렬로 접근시 바로 접근제한됨

-> 안전하게 직렬수집
5. 대량의 데이터 수집시 차단당함
-> 키워드별로 드라이버 재실행 시켜봄
-> 드라이버 1개당 6000개의 데이터 수집시 차단됨을 확인




-> 가설 1. 클릭 횟수 or 페이지 이동 어느 정도 이상이면 차단
-> 가설 2. 드라이버 켜진 시간 어느 정도 이상이면 차단
어느 가설이 맞는지 확인하기 위해서 딜레이를 더 늘려도 보고 줄여도 봤는데
여전히 6000개 정도 수집했을때 차단당한것으로 보아 가설 1번이 맞아보인다
-> 각 상품별로 드라이버 새로 띄우고 딜레이는 줄였다
6. 문제 7로 인해서 상품별로 따로 드라이버 켜줬더니 드라이버 충돌이 잦음
-> 1. Garbage Collector의 모듈 설치해서 안쓰는 메모리 까지 삭제
-> 2. driver.quit()만이 아닌 del driver로 이중 삭제
-> 3. 실패시 드라이버 삭제후 같은 url로 재시도
-> 4. 메모리 정리위해 딜레이 시간 충분히 줌
으로 해결했다
7. 리뷰수 상품당 1500개 랜더 제한
-> postman을 써봤는데 헤더 넣어도 request앱은 차단되는거 같아서
직접 url로 써보니까

이런식으로 뜨기는 하는데
이런 요청에서 기본 size=10에 page는 150페이지까지 뜨는데
size를 30으로 늘린다면 page는 50까지밖에 안뜬다 결국 1500개 제한은 백단에서 걸려있어서 해결 불가
-> 점수별로 긁어오기. 5점 리뷰 1500개, 4점 리뷰 1500개 이런식으로 긁어오면 최대 7500개 긁어올 수 있다
8. 리뷰 10페이지에서 다음 페이지 이동시 10 -> 20으로 가지므로 11부터 다시 수집하게 수정
성능 개선
1. 상품별로 드라이버 재실행하면 딜레이가 길어짐
-> 현 상품의 메타데이터를 미리 가져와서 현재 드라이버에서 수집한 리뷰개수와 수집될 리뷰개수의 합이 6000이상이면 재실행
2. options.add_argument("--blink-settings=imagesEnabled=false") 옵션으로 이미지 안뜨게 해서 속도 상승
3. 전체 리뷰의 n%정도의 리뷰만 content가 존재함
-> 현재 각 별점별 수집할 리뷰수(REVIEW_TARGET)를 고정적으로 수집함
각 별점별의 총 개수 * 0.n과 REVIEW_TARGET중 큰 값만큼을 각 별점별로 수집하도록 변경해서 수집속도와 content있는 리뷰의 비율을 늘림
REVIEW_TARGET는 200으로 고정하고 0.n의 값을 바꿔봄
| 전체 리뷰수 | 내용 포함 리뷰수 | 내용 포함 리뷰수 비율 | 비고 | |
| 리뷰 200개씩 고정 | 101338 | 50268 | 49.6% | 스킨, 아이라이너 |
| 0.1 | 101229 | 52440 | 51.8% | 로션, 마스카라 |
| 0.25 | 99744 | 53523 | 53.7% | 에센스_세럼_앰플, 쿠션_팩트 |
별점 별 댓글 수집수 = max(200, 현 별점의 리뷰수 * 0.25)로 잡는게 딜레이 시간도 줄이고 양질의 데이터를 모을 수 있음
전처리
1. 데이터 클리닝 및 정규화
- 노이즈 제거: HTML 태그와 특수문자를 제거하여 순수 텍스트만 남겼습니다.
- 이모지 및 특수 기호 삭제: 분석에 불필요한 이모지와 별표(★) 등의 기호를 정규표현식으로 제거했습니다.
- 감성 표현 정규화: 'ㅋㅋ', 'ㅎㅎ'와 같이 반복되는 자모음을 축소하여 데이터의 일관성을 높였습니다.
'ㅋㅎㅋㅎㅋㅎㅋㅎ'같은것도 자모음의 경우 2개까지 하나의 묶음으로 봐서 축소시켰습니다.
2. 데이터 형식 및 자료형 변환
- 자료형 정수화: 문자열로 되어 있던 가격, 별점, 리뷰 수, 상품 ID 등을 수치 연산이 가능하도록 정수형(int)으로 변환했습니다.
- 날짜 및 시간 표준화: 제각각이던 날짜 형식을 ISO 표준 형식(YYYY-MM-DD)으로 통일했습니다.
3. 결측치 및 중복 처리
- 결측치 제거: helpful_count가 0이거나 has_image가 False인 필드, 그리고 빈 문자열 필드를 삭제하여 데이터 용량을 최적화했습니다.
- 중복 리뷰 제거: 닉네임, 날짜, 리뷰 본문을 기준으로 동일한 리뷰가 중복 수집된 경우를 찾아 삭제했습니다.
- 데이터 분리: 텍스트 내용이 있는 리뷰와 없는 리뷰를 별도의 파일로 분리하여 분석 효율을 높였습니다.
4. 브랜드 및 카테고리 표준화
- 브랜드명 통합: 국문/영문 혼용 브랜드명을 하나의 대표 명칭으로 통일했습니다.
- 카테고리 그룹화: 복잡한 카테고리 경로를 '스킨', '로션' 등 60여 개의 대표 카테고리로 단순화했습니다.
- 상품명 정제: 상품명 내에 포함된 브랜드명이나 용량(ml), 수량(개) 정보를 삭제하여 핵심 상품 속성만 남겼습니다.
5. 자연어 처리 (NLP) 기반 전처리
- 형태소 분석 및 토큰화: Okt 분석기를 사용하여 명사, 동사, 형용사 위주로 단어를 추출했습니다.
- 불용어(Stopwords) 제거: 분석 의미가 없는 단어들을 미리 준비한 stopwords-ko.txt 목록을 기반으로 필터링했습니다.
- 감성 라벨링: 별점을 기준으로 4점 이상은 긍정(1), 2점 이하는 부정(0)으로 분류하는 라벨을 생성했습니다.
- 벡터화 (Word2Vec): 리뷰 텍스트를 100차원의 수치 벡터로 변환하여 기계가 읽을 수 있는 형태로 만들었습니다.
문제점
1. word2vec 모델의 학습 범위
리뷰기반으로 word2vec을 하였는데 이러면 각 카테고리별로 새로운 word2vec모델을 학습함
이러면 예를들어 오일 파일에서 촉촉하다라는 단어의 벡터와 크림 파일에서의 촉촉하다라는 단어의 벡터가 다른 수치를 가지게 됨
-> 모든 파일의 리뷰 토큰을 모아서 하나의 word2vec 모델을 한 번만 학습해야함 그래야지 동일한 의미 공간 안에서 비교 가능
2. 개별 리뷰 단위로만 벡터 저장중
-> 상품의 대표 벡터를 product_info 영역에 추가하는게 좋음
1~2. 관련 해결법
300만개의 리뷰를 모두 모아 임베딩 스페이스(공통 벡터)를 생성
-> 공통 벡터를 이용하여 각각의 리뷰 벡터를 생성

-> 각 리뷰벡터의 평균으로 상품의 대표 벡터를 생성

-> 상품의 대표 벡터를 이용해서 상품관 연관성 비교
단순 상품 추천만 구현 한다면 각 리뷰의 벡터는 필요없음
그러나
1. 상품의 특징을 가장 잘 요약하는 대표 리뷰 선정
2. 리뷰 내 검색 및 필터링
3. 벡터를 군집화하여 '보습력에 대한 칭찬이 많다' 와 같은 통계 내기
이런것을 하려면 각 리뷰의 벡터도 필요함
3. 300만건의 리뷰를 koNLPy의 Okt로 분석하는건 오래걸림
-> 멀티프로세싱으로 cpu코어를 모두 활용하거나, Okt대신 더 빠른 Mecab형태소 분석기가 좋음
-> 그러나 Mecab는 정말 대용량 데이터에서 쓰는게 좋고 Okt에 비해 OS도 타고 형태소가 쓸데없이 더 세분화되서 쪼개지는 문제가 있어서 Okt유지
4. JSON 파일로 저장시 파일이 너무 무거워져서 읽이 힘듦
-> Apache Parquet을 이용. 벡터를 Binary로 압축하여 용량을 줄이고 열지향이라 유사도 계산에 필요한 벡터 칼럼만 골라서 메모리 올릴 수 있음(상품 검색 속도 향상)
5. NFC와 NFD문제

-> .parquet에 저장할때 NFD형태로 저장이되서 사람이 타이핑한 글자(NFC)로 서치가 안됐음
-> 저장 및 불러올때 NFC로 형변환
데이터 저장
기존에는 json파일로 저장
json은 용량이 너무 커지고 불러올 때도 로드하는 양이 많기떄문에
수정 1.
Aparch Parquet을 이용, parquet. 파일로 저장
이점 1. 벡터의 경우 BInary로 저장해서 문자열이 아닌 정수형으로 저장된다
문자열의 경우에도 압축이 되므로 용량이 10~20%정도로 줄어 든다
이점 2. 열 방식으로 저장하므로 접근이 빠르다
수정 2.
AWS를 이용해서 데이터를 저장해보려고함