Streamlit으로 LLM기반 AI 콘텐츠 추천 서비스 구현하기
25년만에 집 거실에 TV가 생겼다. 자식들 교육 때문에 TV를 없앴다가, 막내 동생 대학 입학을 기념으로 거실 한쪽 벽을 꽉 채우는 TV를 들였다.
TV가 생기고, 부모님은 저녁을 먹고 나면 적어도 2시간은 TV 앞에 앉아 넷플릭스를 보시곤 한다. 나한테도 종종 볼만한 드라마를 추천해달라고 물어보시지만, 나 또한 TV를 많이 보는 사람은 아닌지라, 추천하는데 한계가 있더라..
부모님도 딱 이런 배우가 보고 싶다, 이런 장르가 보고 싶다가 아니라, 이런 내용~ 이런 분위기~의 콘텐츠가 보고 싶다는 어려운 요청을 주시곤 한다.. 음 그래서 부모님이 애매하게 물어봐도 찰떡처럼 콘텐츠를 추천해주는 AI 서비스를 만들어보기로 마음 먹었고, 한번 만들어봤다.
OTT 검색은 보통 제목, 출연진, 국가 등 키워드 중심으로 이루어진다. 생각나는대로 검색을 하면 (ex.멍 때리면서 생각 없이 보기 좋은 드라마. 사랑이 하고 싶어질 때 보고 싶은 드라마. 그냥 하염없이 웃고 싶을 때) 콘텐츠가 검색되지 않는다. 내가 명확히 보고 싶은 게 없고 원하는 감정이나 분위기만 있을 경우, 적절한 콘텐츠를 찾기 쉽지 않아 보인다.
서비스명: 머볼래
한 줄 설명: 자연어로 원하는 분위기나 느낌을 말하면, 넷플릭스에서 딱 맞는 콘텐츠를 찾아주는 AI 기반 추천 검색 서비스
핵심 기능
1. 자연어 문장 기반 콘텐츠 검색
2. AI 감성·분위기 분석 기반 추천
3. 포스터 이미지 & 메타데이터 자동 출력
4. 한국 콘텐츠 우선 추천 알고리즘
주요 기술: Python, Streamlit, OpenAI API (GPT-4o/Embedding), TMDB API
핵심 데이터: 약 276MB 크기의 임베딩 벡터가 포함된 Netflix_Embedded.csv
머볼래 서비스의 기초 데이터는 Kaggle의 Netflix 콘텐츠 데이터셋을 활용했다. 장르, 국가, 타입, 출시 연도, 줄거리 등의 메타데이터가 들어있어서, 추천 모델 구축에 적합하다고 생각했다.
https://www.kaggle.com/datasets/rohitgrewal/netflix-data?resource=download
본격적으로 개발에 들어가기 앞서, 폴더 속 파일을 데이터/코드/환경 설정으로 분리했다.
머볼래/
├─ Netflix_Embedded.csv # AI 검색용 콘텐츠 데이터베이스
├─ app.py # Streamlit 메인 서비스 파일
├─ .env # API 키
└─ requirements.txt # 필요한 라이브러리 목록
─ Netflix_Embedded.csv : 넷플릭스 콘텐츠 원본 데이터에 임베딩을 미리 계산해 저장한 파일
─ app.py : 서비스 UI와 알고리즘이 담긴 핵심 코드 파일
─ .env : OpenAI API Key, TMDb API Key 등 비밀 키를 숨겨주는 파일
─ requirements.txt : 서비스 실행에 필요한 Python 패키지 목록
raw 데이터에 있는 description만으로는 콘텐츠의 감정·갈등·서사 흐름이 충분히 드러나지 않는다. 즉, 제대로 학습하지 못해 추천 정확도가 좋지 않았다. 그래서 LLM으로 콘텐츠 별 태그를 생성하여, 빈약한 description 내용을 풍부한 의미 구조 데이터로 강화하고자 했다.
GPT-4o-mini를 활용해 콘텐츠 설명을 7개 의미(장르, 분위기, 감정 강도, 갈등 구조, 관계 축, 서사 방식, 제외 요소) 축으로 확장 태깅하는 파이프라인을 구축했다. 콘텐츠 별로 18~35개 태그를 생성했다.
아래는 GPT한테 줄거리에 숨겨진 정서·갈등·관계·서사적 특징을 태그로 추출하라고 명령한 프롬프트이다.
prompt = f"""
다음 콘텐츠 설명을 바탕으로 **태그만** 생성해줘.
형식 규칙 (절대 어기면 안 됨)
- 반드시 명사형 단어만 사용 (문장, 형용사, 동사는 금지)
- 구체적인 감정·갈등·관계·서사 특징이 드러나는 태그로 작성
- 각 항목은 최소 3개 이상, 전체 18~35개 태그
- 태그는 쉼표(,)로 구분하되, 줄바꿈이 있는 7개의 섹션만 사용
- 설명·문장·예시 금지, 개조식 단어만
태그 구조 (반드시 이 순서로)
#장르:
[장르 관련 태그들]
#분위기:
[분위기 관련 태그들]
#감정강도:
[감정 강도·정서 구체 태그들]
#갈등:
[사건·이슈·문제·대립 관련 태그들]
#관계:
[인물 관계·역할·관계 축 관련 태그들]
#서사:
[서사 스타일·구조·리듬·플롯 특징]
#제외:
[사용자가 원하지 않을 가능성이 있는 요소 태그]
콘텐츠 설명:
{desc}
"""
위 프롬프트를 포함한 python 코드를 10시간 정도.. 돌렸다. 이렇게 생성된 데이터는 기존보다 10배 이상 많은 의미 단서가 들어가기 때문에, 검색어가 모호해도 정확한 추천이 가능하다. 실제로, 요 태깅 덕분에 임베딩의 정확도가 비약적으로 상승했다.
임베딩을 하기 전, description 데이터와 내가 생성한 Tag 데이터를 합친 임베딩용 컬럼을 만들었다. 그 다음, OpenAI text-embedding-3-small 모델로 벡터화했다. 이 벡터는 추천 로직의 핵심 데이터 구조로 활용될 예정이다.
임베딩은 아래와 같은 코드로 처리된다.
def embed(text):
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
임베딩이 무엇인지 다시 정리해보면, 콘텐츠 description과 태그들을 하나의 문장으로 합친 뒤, 이걸 AI가 숫자로 된 의미 벡터로 바꾼다. 사용자가 문장을 입력하면, 이 또한 같은 방식으로 숫자로 바뀐다. 이 두 숫자를 비교하면, 이 문장에 가장 가까운 콘텐츠가 무엇인지 계산할 수 있고, 유사한 콘텐츠를 파악할 수 있게 된다.
사용자가 입력한 자연어를 그대로 이해하는 검색 엔진을 구축하는 걸 목표로 했다. 실제로 사용자 입력을 분석하고, 데이터를 벡터 검색으로 매칭하고, 최종 추천 후보를 고도화하는 로직을 만들어봤다.
1) 검색어를 의미 키워드로 변환한 후 임베딩
사용자가 검색어를 입력하면, 임베딩 하기 전에 의도를 추출하는 과정을 넣었다. 이렇게 함으로써 애매한 입력어도 정확한 검색이 가능하도록 했다. GPT에게 아래와 같은 프롬프트를 줬다.
eng_prompt = """
You convert user queries into English semantic keywords strictly about story, mood, conflict, and genre.
RULES:
- Never translate in a way that implies a specific culture, nation, or style
- Never assume the content's origin if the user didn't specify it.
- Only capture: themes, emotions, relationships, tone, plot, conflict, character roles.
- If the user expresses dislikes, include them (e.g., "no gore, no horror").
- Output should be short keyword-style English phrases.
Examples:
"잔잔하고 감동적인 로맨스" : "calm emotional romance, heartwarming love story, no horror"
"인간을 배신하는 로봇" : "robot rebellion, machines betray humans, AI uprising, dystopia"
"""
결과를 살펴 보면, [눈물 콧물 쏙 빼는 슬픈 한국 드라마]를 입력하면, GPT가 해당 검색어를 아래처럼 키워드로 변환한다. 단어 순서를 재정렬하고, 감정/갈등/장르를 골라내고, 불필요한 정보를 제거한 후 영어로 바꾼다.
tragic emotional drama, heartwarming sorrow, no gore
이렇게 정제된 텍스트를 임베딩 모델(text-embedding-3-small)에 넣어 단어를 좌표로 만든다.
[0.0123, -0.554, 0.071, 1.044, ... , -0.113]
2) 1차 필터링: 임베딩 기반 500개 후보
검색어 기반 임베딩과 콘텐츠 데이터셋 임베딩의 유사도를 비교하여 의미적으로 가까운 500개 콘텐츠를 뽑는다. 의미적으로 가까운 걸 빠르고 정확하게 걸러내지만, 정서나 배제 조건을 판단하는 것은 잘 못한다.
500개를 뽑는 이유는? 선택지가 너무 좁으면 GPT가 나은 작품을 고르기 어렵고, 너무 많으면 품질이 떨어진다. 500개는 의미적으로 가까운 범위이고, GPT가 판단하기에도 무리가 없는 개수이다.
3) 2차 필터링: GPT Pick 10개
1차 단계에서 임베딩 유사도로 500개 후보를 추린 뒤, 나는 GPT가 실제 콘텐츠 큐레이터처럼 판단하도록 정책 기반 프롬프트를 설계했다. 이 프롬프트는 서비스가 가져야 할 규칙을 반영해서 작성했다. 정해진 규칙에 가장 적합한 8개만 추려내고, 필요한 경우 순서를 재정렬해(re-reank_selection 함수) 최종 추천 목록을 만든다.
아래는 내가 GPT에게 준 프롬프트 중 핵심 규칙이다.
pick_prompt = f"""
당신은 '영화/드라마 추천 큐레이터'입니다.
아래 후보 중 사용자 의도에 가장 적합한 콘텐츠 10개를 선택하세요.
....
선택 규칙
- 스토리·갈등·분위기·장르 적합도 기준
- 사용자가 특정 국가 언급 시 최우선 적용
- 공포·잔혹·피 등 제외 요구 시 반드시 제거
- 영화/TV 균형
- 출력: 오직 제목만, 한 줄에 하나씩. 번호·따옴표 금지.
"""
1) AS-IS: description 기반 임베딩. TO-BE: 태그 생성
초기에는 RAW Description만으로 임베딩을 진행했지만, 감정·갈등·관계 같은 핵심 의미가 빠져 있어 검색 품질이 낮았다. 이를 해결하기 위해 GPT로 고정 구조의 7섹션 태그를 자동 생성하여 콘텐츠의 의미 밀도를 높였다. 이후 Description + Tags를 합쳐 의미가 풍부한 벡터로 재임베딩했다.
2) 국가 가중치를 넣어 한국 사용자 취향 반영
사용자가 특정 국가를 명시하면 국가 기반 하드 필터링도 적용했다. 또한, 국가 언급이 없을 때는 한국, 미국, 영국 순으로 기본 가중치를 적용했다. 넷플릭스 데이터셋에는 한국인에게 익숙하지 않은 인도나 멕시코 등 콘텐츠 수가 많기 때문에, ‘한국 취향과 동떨어진 결과’가 상위에 뜨는 문제를 보완하여 추천 품질을 확보하기 위함이었다.
한국 +0.04
미국 +0.025
영국 +0.015
3) 콘텐츠 줄거리 한국어 요약
Description이 영어라 한국 사용자에게 직관적이지 않아, GPT로 자연스러운 한국어 요약을 생성하는 로직을 추가했다. 그냥 번역이 아니라 읽기 쉬운 한 문단 요약이 되도록 프롬프트를 작성했다.
messages=[
{"role": "system", "content": "다음 내용을 자연스럽고 담백하게 한 문단으로 요약해 주세요. '~해요' 금지, 과장 금지, 자연스러운 문장."},
{"role": "user", "content": desc}
]
4) TMDB API로 포스터 연동
콘텐츠 검색 결과에 포스터 이미지가 없으면, 어떤 콘텐츠인지 직관적으로 판단하기 어렵다. 이를 해결하기 위해, TMDB API를 연동하여 콘텐츠 포스터를 매칭했다. 포스터 매칭 정확도를 높이기 위해, 콘텐츠 title, year, type 조건을 조합해 검색하도록 설계했다. 넷플릭스 데이터셋의 경우 작품명이 동일하거나, 다른 국가 버전이 존재하는 경우가 있어서 연도·타입 정보를 함께 사용해야 오매칭 확률을 줄일 수 있었다.
배포 과정도 다사다난 했다. Github에 파일을 올린 다음 streamlit.app으로 배포하려고 했는데, 넷플릭스 데이터셋이 대용량이라 업로드가 안되더라.. 몇번의 시행착오를 거쳐 Git LFS를 통해 대용량 파일 업로드에 성공했다. 그런 다음 Github Repository를 Streamlit Cloud에 연결하여 앱 배포를 완료했다.
화면 첫 로딩 땐 시간이 좀 걸려요..!
일단 MVP는 Streamlit 기반으로 빠르게 검증 가능한 형태로 만들었다. 실제 서비스 수준으로 확장하기 위해서는 아래 고도화가 필요해보인다.
1. 한국 콘텐츠 제목은 한국어로 노출되도록
넷플릭스 데이터셋의 title 컬럼이 영어라, 한국 콘텐츠도 영어 제목으로 노출된다. 현재 한국 콘텐츠에 한해 title을 한국어로 바꾼 데이터를 만들었고, 다음 업데이트때 수정할 예정이다.
2. UI를 더 예쁘고, 더 편하게
Streamlit은 UI 자유도가 매우 낮다. 실제 내가 원하는 UI 디자인을 하나도 반영하지 못했다. 추후, Next.js 같은 프론트 프레임워크로 전환해서 디자인을 고도화할 예정이다.
3. 프론트, 백 분리
지금은 app.py 하나에 UI, 검색 로직, 추천 로직, 데이터 처리까지 모든 기능이 다 들어 있는 구조다. MVP 단계에서는 빠르게 만들 수 있지만, 기능이 늘어나면 수정할 때 서로 영향이 생겨 유지보수가 어려워진다. 그래서 다음 단계에서는 다음처럼 구조를 나누는 것이 필요할 것 같다.
프론트엔드: 사용자 화면만 담당
백엔드 API: 검색, 임베딩 호출, 추천 로직 처리
DB: 콘텐츠와 벡터를 안정적으로 저장
4. 상세 기능 고도화
MVP 단계에서는 기본 검색·추천 흐름에 집중했다면, 고도화할 때는 사용자 경험을 확실히 높이는 세부 기능들이 필요하다.
넷플릭스 콘텐츠 페이지 연결: 검색된 작품을 탭하면 즉시 넷플릭스 상세 화면으로 이동하도록 연결해, 탐색에서 시청까지의 전환 비용을 줄인다.
검색 결과 복사 기능: 사용자 클립보드로 추천 목록을 바로 복사할 수 있게 해, 친구에게 공유하거나 다른 앱에서 기록할 때 편리성을 높인다.
기본 필터 제공(국가, 장르, 분위기 등): 사용자의 상세한 요구사항을 필터로 가능하게 하여, 검색 의도를 더 정확히 반영한다.
최근 검색 내역: 사용자가 이전에 찾았던 검색 결과를 다시 확인하거나 이어서 탐색할 수 있도록, 최근 검색한 콘텐츠 목록을 자동으로 저장해 보여준다.
부모님이 원하는 드라마를 찾기 어렵다는 문제에서 출발해서, 자연어 그대로 검색되는 서비스에 대한 관심으로 확장되었다. 데이터 준비부터 임베딩 전략, 검색 로직, 2단계 GPT 큐레이션까지 전 과정을 직접 설계하면서, LLM이 기존 검색 경험을 어떻게 바꿀 수 있는지 실험해보았다. 이번 프로젝트는 서비스 기획자가 LLM을 실제 서비스에 어떻게 녹여낼 수 있는지 확인해보는 과정이었다.
앞으로도 LLM을 활용해 일상 문제를 가볍게 해결해보는 서비스를 더 만들어보고 싶다. 이번을 시작으로, 계속 다양한 실험을 해보고 브런치에 기록을 남겨보겠다.