Mini Project 4 07
localStorage 기반 개인화 추천의 작동 원리
개인화 추천이라고 하면 회원 계정과 서버 추천 엔진이 먼저 떠오른다. 하지만 4차에서는 브라우저 안의 작은 기억만으로도 개인화 경험을 시도한다.
근거 · 4차 localStorage 선호/추천 구현
1장: 브라우저 속 개인의 작은 기억: 선호 기록하기
솔라의 손가락이 노트북 화면의 한 문장 위를 맴돌았다. 프로젝트 문서를 읽다가 마주친 짧은 설명이었지만, 머릿속에선 자꾸만 삐걱거리는 소리가 났다.
4차 개인화 추천은 로그인 없이 localStorage에 열람한 책의 태그/장르 선호를 쌓고...
“로그인 없이… 개인화 추천?”
솔라는 소리 내어 중얼거렸다. 익숙한 단어들의 낯선 조합이었다. 지금까지 솔라에게 ‘개인화’란 곧 회원가입과 로그인, 그리고 거대한 서버가 내 아이디에 맞춰 취향을 분석해주는 복잡한 과정이었다. 내가 어떤 상품을 클릭했고, 얼마나 머물렀는지 등의 정보가 서버로 차곡차곡 전송되고, 추천 엔진이 그 데이터를 바탕으로 다음 상품을 보여주는 방식. 그게 솔라가 알던 세계였다.
그런데 이 문장은 달랐다. 서버도, 계정도 아닌 브라우저의 작은 저장 공간, localStorage만으로 개인화를 시도한다고 말하고 있었다. 마치 인터넷이 끊어진 외딴섬에서 혼자 힘으로 세계 지도를 그리려는 것처럼, 어딘가 앞뒤가 맞지 않는 이야기로 들렸다.
“뭐가 그렇게 심각해?”
어느새 다가온 언니 루나가 솔라의 화면을 흘깃 보며 물었다.
“언니, 이거 좀 봐. 로그인도 안 했는데 어떻게 개인화 추천을 한다는 건지 모르겠어. 브라우저에 뭘 저장하기는 한다는데… 그게 어떻게 ‘나를 위한’ 추천이 되는 거야? 그냥 내 컴퓨터에만 기록되는 거잖아.”
솔라의 목소리에는 ‘그건 불가능하다’는 확신이 섞여 있었다. 그녀의 머릿속에서 ‘개인화 추천 시스템’이라는 개념은 ‘서버’라는 단단한 기둥에 묶여 있었다. 그 기둥이 없으니 시스템 전체가 와르르 무너지는 느낌이었다.
루나는 잠시 솔라 옆에 서서 문장을 읽어보더니, 의자를 끌어와 앉았다. 키보드를 가볍게 두드려 새 브라우저 창을 열고, 익숙한 온라인 서점 사이트로 이동했다.
“네 말이 맞아. 우리가 보통 생각하는 개인화는 서버가 하는 일이지. 하지만 여기서 말하는 건 조금 다른 방식이야. 서버가 ‘솔라’라는 사람을 기억하는 게 아니라, 솔라가 쓰는 이 ‘브라우저’가 솔라의 취향을 잠시 기억해두는 거지.”
“브라우저가 기억한다고?”
“응. 직접 확인해보자.”
루나는 솔라에게 브라우저의 ‘개발자 도구’를 열게 했다. 복잡한 코드와 탭들이 나타나자 솔라는 잠시 주춤했다. 루나는 ‘Application’ 탭을 누르고, 왼쪽 메뉴에서 ‘Local Storage’를 찾아 클릭하라고 알려주었다. 서점 사이트의 주소가 보이고, 그 아래 키(Key)와 값(Value)으로 이루어진 표가 나타났다. 아직은 텅 비어 있거나, 의미를 알 수 없는 몇 개의 항목만 보일 뿐이었다.
“여기가 이 브라우저만의 작은 창고야. 이제 저기서 아무 책이나 하나 눌러봐.”
솔라는 루나의 말에 따라 화면에 보이는 책들 중 ‘우주비행사 헬멧’이 그려진 SF 소설 표지를 클릭했다. 책의 상세 페이지가 화면을 가득 채웠다. 책 소개, 저자 정보, 그리고 ‘#우주’, ‘#AI’, ‘#탐사’ 같은 태그들이 붙어 있었다. 솔라는 잠시 페이지를 훑어본 후, 다시 원래 탭으로 돌아왔다.
루나는 아무 말 없이 개발자 도구 창을 가리켰다. 솔라의 눈이 커졌다. 아까는 보이지 않던 새로운 항목이 생겨 있었다.
Key: bookPreferences
솔라가 그 항목을 클릭하자, 오른쪽 ‘Value’ 영역에 전에 없던 내용이 나타났다.
브라우저 저장 형태를 이해하기 위해 단순화한 예시다. 실제 필드명과 값은 구현에 따라 달라질 수 있다.
{
"tags": {"우주":1, "AI":1, "탐사":1},
"genres": {"SF":1},
"viewedBookIds": ["bk-1024"],
"lastViewedAt": "2024-07-15T10:30:00Z"
}
“어…!”
탄성과 함께 솔라의 머릿속을 맴돌던 의문 하나가 풀려나갔다. 막연하게 ‘브라우저에 저장한다’고만 생각했던 과정이 눈앞에 구체적인 증거로 나타난 순간이었다. 서버로 데이터를 보내지 않았는데도, 내가 방금 본 책의 태그와 장르가 내 컴퓨터 브라우저의 ‘창고’ 안에 그대로 기록된 것이다.
“정말로… 그냥 여기에 메모를 해두는 거구나. 내가 ‘우주’랑 ‘AI’ 태그가 붙은 ‘SF’ 장르의 책을 봤다고.”
솔라는 개발자 도구 창과 책 페이지를 번갈아 보며 중얼거렸다. ‘로그인 없는 개인화’의 첫 번째 단추가 어떻게 끼워지는지 비로소 이해할 수 있었다. 거대한 서버의 기억장치가 아니라, 내 브라우저 속 작은 메모장 같은 존재. preferenceService.js라는 이름의 보이지 않는 일꾼이 내가 책 페이지를 열 때마다 부지런히 내 취향을 기록하고 있었던 셈이다.
지금까지 개인화는 서버의 역할이라고만 생각했던 단단한 믿음에 작은 균열이 생겼다. 브라우저 혼자서도 사용자의 행동을 기억하고, 개인화의 첫걸음을 뗄 수 있다는 사실을 직접 목격한 것이다.
하지만 여전히 모든 퍼즐이 맞춰진 것은 아니었다. 솔라는 bookPreferences에 기록된 값을 가만히 들여다보았다.
“좋아, 내가 뭘 봤는지 기록하는 건 알겠어. 그런데 이 기록이 대체 어떻게 추천 점수로 바뀐다는 거지? 그냥 ‘우주 태그를 한 번 봤다’는 이 기록만으로?”
2장: 선호 기록이 점수가 되는 마법: 가중치 계산
솔라는 브라우저 개발자 도구 창에 떠 있는 bookPreferences 값을 노려보듯 들여다보고 있었다. 내가 본 책의 정보가 tags와 genres로 깔끔하게 정리되어 기록되는 것은 확인했다. 하지만 이 메모 같은 기록이 어떻게 ‘추천’이라는 지능적인 행동으로 이어지는지는 여전히 안갯속이었다. ‘우주’ 태그를 한 번 봤다는 사실이 어떻게 다른 책을 나에게 밀어주는 힘이 될까. 막연한 영향력 이상의 구체적인 규칙이 궁금했다.
그때 옆에 있던 루나가 솔라의 노트북 화면 옆으로 작은 메모장을 열었다. 빈 페이지에 쓱쓱 선을 그어 간단한 표를 만들기 시작했다.
| 책 제목 | 태그 | 장르 | 추천 점수 |
|---|---|---|---|
| 은하수를 여행하는 히치하이커 | 우주, 코미디, 모험 | SF | ? |
| AI 시대의 윤리 | AI, 철학, 기술 | 교양과학 | ? |
| 심해 탐사 로봇 | 탐사, 로봇, 해양 | 과학 | ? |
| 안드로이드는 전기양을 꿈꾸는가? | AI, 디스토피아, 철학 | SF | ? |
“자, 이게 서점에 있는 다른 책들이라고 상상해 봐.”
루나가 만든 표였다. 추천 후보가 될 네 권의 책과 각각의 정보, 그리고 물음표가 찍힌 ‘추천 점수’ 칸이 보였다. 솔라는 고개를 갸웃했다.
“이 책들한테 점수를 매긴다고? 어떻게?”
“단순히 같은 태그가 있는지 없는지만 보는 게 아니야. 프로젝트 문서의 그 다음 문장을 기억해봐. recommendationService.js가 이 기록을 가지고 점수를 계산하는 방식에 대한 힌트가 있었어.”
루나의 말에 솔라는 다시 문서를 떠올렸다. ‘…tag weight 3과 genre weight 1로 추천 점수를 계산한다.’ 스쳐 지나갔던 구절이었다.
“가중치… 3과 1?”
“응. 바로 그거야. 네가 본 책의 태그와 같은 태그를 가진 책은 3점을 받고, 같은 장르의 책은 1점을 받는 거지. 한번 계산해 볼까?”
루나는 표의 첫 번째 줄, ‘은하수를 여행하는 히치하이커’를 가리켰다. 그리고 솔라가 보고 있던 개발자 도구의 bookPreferences 값을 다시 한번 확인시켰다.
{"tags": {"우주":1, "AI":1, "탐사":1}, "genres": {"SF":1}}
“자, 이 책은 ‘우주’ 태그를 가졌네. 네 선호 기록에 ‘우주’ 태그가 있으니, 태그 가중치 3점을 받아. 그리고 장르도 ‘SF’로 똑같지? 그럼 장르 가중치 1점을 더 받아. 총 몇 점일까?”
“3 더하기 1이니까… 4점!”
순간 솔라의 머릿속에서 무언가 ‘탁’ 하고 맞아 들어가는 느낌이 들었다. 막연하게 ‘연관이 있겠지’라고 생각했던 두 정보 사이에 구체적인 다리가 놓이는 순간이었다. 그냥 ‘일치한다’가 아니라, 정해진 규칙에 따라 정량적인 ‘점수’로 바뀌고 있었다.
“그럼 내가 한번 해볼게.”
솔라는 신이 나서 다음 책인 ‘AI 시대의 윤리’의 점수를 계산하기 시작했다.
“이 책은 ‘AI’ 태그가 있으니까 태그 점수 3점. 장르는 ‘교양과학’이니까 내 선호 기록인 ‘SF’와 달라서 0점. 그럼 총 3점이네.”
솔라는 마지막 책인 ‘안드로이드는 전기양을 꿈꾸는가?’까지 단숨에 계산해 나갔다. ‘AI’ 태그로 3점, ‘SF’ 장르로 1점. 총 4점. 계산을 마친 솔라는 완성된 표를 뿌듯하게 바라보았다.
| 책 제목 | 태그 | 장르 | 추천 점수 |
|---|---|---|---|
| 은하수를 여행하는 히치하이커 | 우주, 코미디, 모험 | SF | 4 |
| AI 시대의 윤리 | AI, 철학, 기술 | 교양과학 | 3 |
| 심해 탐사 로봇 | 탐사, 로봇, 해양 | 과학 | 3 |
| 안드로이드는 전기양을 꿈꾸는가? | AI, 디스토피아, 철학 | SF | 4 |
“아하! 이제 알겠다. 그냥 비슷한 책을 보여주는 게 아니었어. 태그가 장르보다 3배나 더 중요한 기준이었구나. recommendationService라는 일꾼은 이렇게 내 취향 기록에 가중치를 곱해서 모든 책에 점수를 매기고 있었던 거야.”
솔라는 이제 브라우저 속 작은 기억이 어떻게 의미 있는 숫자로 변환되는지 이해할 수 있었다. 마치 자신만의 ‘가중치 점수 계산기’를 손에 넣은 것 같았다. 모호한 가능성이 명확한 계산식으로 바뀌자, 로그인 없는 개인화라는 낯선 개념이 한 걸음 더 현실적으로 다가왔다.
하지만 완성된 점수표를 보고 나니 새로운 질문이 고개를 들었다.
“좋아, 점수 계산까지는 완벽히 이해했어. 그런데 ‘은하수를 여행하는 히치하이커’랑 ‘안드로이드는 전기양을 꿈꾸는가?’ 두 권이 똑같이 4점으로 제일 높네. 그럼 이 점수들이 실제 서점 홈페이지의 ‘나를 위한 추천’ 목록에는 어떻게 나타나는 거야? 둘 다 보여주는 건가? 순서는 어떻게 정하고?”
3장: 점수에서 추천 목록까지: 브라우저 개인화 완성
솔라의 시선은 직접 점수를 매겨 완성한 메모장 위 표에 머물러 있었다. ‘은하수를 여행하는 히치하이커’와 ‘안드로이드는 전기양을 꿈꾸는가?’ 두 권의 책 옆에 나란히 쓰인 숫자 ‘4’가 유독 선명했다. 가장 높은 점수를 받은 책들. 하지만 둘 다 똑같은 점수라는 사실이 새로운 질문의 실마리가 되었다. 이 점수들이 실제 서점의 ‘나를 위한 추천’ 목록으로 어떻게 변신하는 걸까.
그때 루나가 아무 말 없이 솔라가 보고 있던 개발자 도구 창을 다시 앞으로 가져왔다. 그리고 첫 장에서 확인했던 localStorage의 bookPreferences 값을 가리켰다. 솔라는 이미 익숙해진 tags와 genres 항목을 지나쳤다. 그러다 미처 신경 쓰지 못했던 다른 한 줄에 눈길이 닿았다.
"viewedBookIds": ["bk-1024"]
“아, 맞다. 내가 봤던 책 ID도 저장하고 있었지.”
지금까지 솔라의 머릿속은 오직 ‘어떤 태그와 장르를 선호하는가’에만 쏠려 있었다. 하지만 브라우저는 내가 ‘무엇을 보았는지’ 그 자체도 똑똑히 기억하고 있었다. 마치 “이 책은 이미 보셨잖아요”라고 말해줄 준비를 하는 것처럼.
루나는 솔라가 만든 점수 표 맨 위에 새로운 책 한 권을 추가했다.
| 책 제목 | ID | 태그 | 장르 | 추천 점수 |
|---|---|---|---|---|
| 우주비행사의 귀환 | bk-1024 | 우주, 귀환 | SF | ? |
| 은하수를 여행하는 히치하이커 | bk-42 | 우주, 코미디 | SF | 4 |
| AI 시대의 윤리 | bk-101 | AI, 철학 | 교양과학 | 3 |
| 심해 탐사 로봇 | bk-303 | 탐사, 로봇 | 과학 | 3 |
| 안드로이드는 전기양을 꿈꾸는가? | bk-2049 | AI, 철학 | SF | 4 |
“이 ‘우주비행사의 귀환’이라는 책이 네가 처음 클릭했던 책이라고 해보자. 이 책의 추천 점수는 몇 점일까?”
솔라는 자신 있게 계산을 시작했다. “내 선호 기록에 ‘우주’ 태그가 있으니 3점, ‘SF’ 장르가 같으니 1점. 합해서 4점이네.”
순식간에 4점짜리 책이 세 권이 되었다. 솔라의 미간이 살짝 좁혀졌다. 상황이 더 복잡해진 것 같았다.
“점수 계산만으로는 부족해. 점수가 높은 책들을 그냥 전부 보여주는 건 아닐 거야. ‘나를 위한 추천’ 칸은 보통 몇 권 안 되잖아.”
솔라는 중얼거리며 스스로 답을 찾아가기 시작했다. 그녀는 점수표 옆 빈 공간에 ‘최종 추천 목록 만들기’라고 적고, 몇 가지 규칙을 세워보기로 했다. 점수 계산을 마친 recommendationService가 실제 BookListPage에 책을 보여주기 전에 거쳐야 할 마지막 관문, 즉 필터링 규칙을 추리해보기 시작한 것이다.
“첫 번째 규칙. 내가 이미 본 책은 추천하면 안 돼.”
솔라는 방금 점수를 계산한 ‘우주비행사의 귀환 (bk-1024)’에 가로선을 그었다. viewedBookIds에 기록된 바로 그 책이었다. 높은 점수를 받았지만, 가장 먼저 탈락했다.
“두 번째 규칙. 점수가 높은 순서대로 정렬해야지.”
남은 네 권의 책을 점수 순으로 마음속에서 재배열했다. ‘은하수를 여행하는 히치하이커’와 ‘안드로이드는 전기양을 꿈꾸는가?’가 맨 위로 올라왔다.
“마지막 규칙. 추천 목록은 보통 3개나 5개 정도니까… 상위 3권만 보여주자.”
솔라는 자신의 규칙에 따라 최종 목록을 골라냈다.
- 은하수를 여행하는 히치하이커 (4점)
- 안드로이드는 전기양을 꿈꾸는가? (4점)
- AI 시대의 윤리 (3점)
‘심해 탐사 로봇’은 비록 3점이었지만, 3위 안에 들지 못해 목록에서 빠졌다. 솔라는 자신이 직접 만든 최종 목록을 보며 환하게 웃었다. 막연했던 추천 과정의 마지막 단계가 눈앞에서 명확하게 완성된 순간이었다. 점수는 단지 재료일 뿐, 진짜 추천은 세심한 필터링 규칙을 통과해야만 비로소 사용자에게 닿는다는 것을 깨달았다. 점수 계산 후 모든 책이 무질서하게 후보가 될 거라는 섣부른 추측이 틀렸음을 확인한 것이다.
“이제야 전체 그림이 보여.”
솔라는 메모장의 깨끗한 페이지를 새로 펼쳤다. 그리고 지금까지 알아낸 모든 과정을 하나의 흐름도로 그리기 시작했다.
[책 상세 페이지 보기 (BookDetailPage)] -> [localStorage에 선호 기록 저장 (preferenceService)] -> [가중치로 점수 계산 (recommendationService)] -> [필터링: 본 책 제외, 점수 정렬, 개수 제한] -> [최종 추천 목록 보여주기 (BookListPage)]
화살표로 이어진 간단한 상자들. 처음에는 불가능하게만 보였던 ‘로그인 없는 개인화’의 모든 비밀이 그 안에 담겨 있었다. 거대한 서버나 복잡한 인공지능이 아니라, 브라우저 안에서 조용히 작동하는 작은 규칙들의 합창이었다.
솔라는 자신이 그린 흐름도를 가만히 들여다보았다. 처음 프로젝트 문서에서 마주쳤던 낯선 문장이 이제는 당연한 사실처럼 느껴졌다. 브라우저 속 작은 기억이 어떻게 의미 있는 경험으로 바뀌는지, 그 여정의 시작과 끝을 모두 제 손으로 직접 따라가 본 덕분이었다.