

안녕하세요, 저는 오늘의집 개발자 Liz입니다. 안드로이드 개발자로 시작해 지금은 Product Engineer(PE)로 일하며, 제품의 문제를 정의하고 이를 해결해 가는 일을 하고 있습니다.
오늘은 사진 속 상품을 자동으로 찾아 태그해주는 오토태깅을 만들었던 과정을 소개하고자 합니다.
오늘의집에는 사진 속 상품을 클릭하면 바로 연결되는 '플러스 태그'가 있습니다. 하지만 크리에이터가 상품 하나를 태그하려면 사진에서 위치를 찍고, 상품을 검색하고, 결과에서 선택해야 합니다. 상품이 많아질수록 같은 작업을 반복해야 하고, 번거로워 태그를 건너뛰기도 합니다.
오토태깅은 이 과정을 자동화합니다. 사람이 직접 찾지 않아도 자동으로 사진에서 상품을 찾아 비슷한 상품을 검색하고, 태그 후보를 보여줍니다. 다음은 실제 앱에 적용된 오토태깅 화면입니다.

프로젝트를 시작할 때는 어떤 방식으로 태그를 보여줘야 하는지, 어디까지 자동화해야 하는지 미리 알 수 없었습니다. 이 글에서는 정답이 없는 문제를 어떻게 빠르게 검증했는지, 그리고 그 과정에서 필요한 경계를 어떻게 넘나들었는지 살펴보겠습니다.
앱보다 먼저 만든 웹 프로토타입
오토태깅은 사내 Build Anything Program(BAP) 과제로 시작했습니다. BAP는 AI 코딩 에이전트를 활용해 각자 오늘의집에 이로운 것을 만들어보는 사이드 프로젝트 프로그램입니다.
자유롭게 시도할 수 있는 프로그램인 만큼, 처음부터 앱을 완성하기보다 아이디어를 빠르게 검증하는 것에 집중했습니다. 그래서 첫 프로토타입은 앱이 아닌 웹으로 만들었습니다. 앱을 빌드해 참가자 기기에 설치하는 것보다 주소 하나를 공유하는 편이 빠르기 때문입니다.
또한 기획 초기에 답을 알 수 없는 질문이 두 개 있었습니다. 둘 다 실제로 조작해 봐야 확인되는 종류였습니다.
- 태그 자동 생성 vs. 유저가 원하는 시점의 수동 생성
- 유사한 상품까지 태그 vs. 확실한 상품만 태그
단순한 시안으로는 확인할 수 없는 질문이었습니다. 정지된 화면을 보여주고 신뢰 여부를 물으면 답에 의미가 없습니다. 실제 사진을 올려 결과를 직접 받아 보는 경험이 필요했습니다. 그래서 프로토타입을 만들고 사진 업로드 경험이 있는 유저를 대상으로 UT를 진행했습니다.
프로토타입 구성
프로토타입은 세 부분으로 구성했습니다. 앱의 업로드 화면을 옮긴 웹 화면, 상품을 벡터로 색인한 검색 인덱스, 그리고 추론 API를 담당하는 FastAPI 서버입니다. 사진을 받으면 객체 탐지 모델로 상품의 위치를 찾고, 탐지된 영역을 OpenCLIP 계열 모델로 임베딩합니다. 미리 상품 카탈로그를 임베딩해 둔 ChromaDB에서 코사인 유사도가 높은 상품을 찾아 태그 후보로 반환했습니다. 탐지와 검색을 한 응답에 담아 돌려주므로 사진 한 장에 요청 한 번으로 끝납니다.
화면은 HTML 파일 하나로 만들었습니다. 빌드 과정이 없어서 파일을 고치고 새로고침하면 바로 반영됩니다. 아이폰 사진이 HEIC로 들어오므로 JPEG로 바꾸는 단계가 필요했고, 사진과 태그 상태는 IndexedDB에 담아 새로고침해도 작업이 남게 했습니다.
미리 정해 둔 결과를 보여주는 것이 아니라 참가자가 올린 사진에 대해 실제 추론이 동작합니다.
UT 회차를 나눈 이유
UT는 총 네 번의 세션으로 나누어 진행했는데, 이는 표본을 늘리기 위한 것만이 아닙니다. 회차 사이에 고치고, 고친 결과를 다음 회차에서 확인하기 위함이었습니다.

2차에서 3차로 넘어가면서 상품 추천 방식을 크게 바꿨습니다. 2차까지는 탐지된 객체와 전체 상품을 비교했습니다. 3차에서는 유저가 구매하거나 스크랩하거나 태그한 상품을 검색 대상으로 함께 넣었습니다. 자기 공간에 있는 물건은 이미 한 번 본 물건일 가능성이 높다는 가정입니다. 그랬더니 추천 결과의 적중률이 올라가면서 오토태깅 후 삭제되는 태그 비율도 낮아졌습니다.
3차에서는 다른 종류의 반응이 나왔습니다.
"태그가 사진에 너무 많으니까, 사진도 잘 안 보이고 태그도 잘 안 보여요."
추천이 많을수록 유리할 것으로 예상했으나, 사진 위에 태그가 누적되면 사진과 태그가 서로를 가렸습니다. 정확도 문제가 아니라 배치 문제였습니다.
그래서 4차 전에 사진 위 툴팁을 사진 아래 캐러셀로 바꿨습니다. 현재 서비스에 반영된 상품 캐러셀은 최초 기획 단계에서 도출된 UI가 아니라 이 회차에서 결정된 것입니다.
UT를 한 번만 진행했다면 1차의 탐지 정확도 문제만 들고 개발에 진입했을 것입니다. 회차 사이에 고치고 다시 확인했기 때문에, 예상하지 못했던 문제를 발견하는 동시에 고친 결과가 실제로 개선됐는지도 확인할 수 있었습니다.
중요한 건 자동·수동이 아니라 대기 시간
자동으로 붙일지, 유저가 원할 때 붙게 할지. 답은 자동이었습니다. UT 전 예상대로 대부분의 참가자가 자동을 선호했습니다. 그런데 놓친 것이 있었습니다.
"수동으로 태그하는 게 나을 수도 있어요. 자동 검색은 감지 시간이 걸리니까, 빠르게 올리고 싶을 때는 오히려 불편할 수 있어요."
처음 설계는 올린 사진을 한 번에 모두 감지하는 방식이었습니다. 사진 한 장이면 문제가 없습니다. 그런데 참가자들은 한 번에 세 장부터 열 장까지 올렸고, 그러면 마지막 사진까지 감지가 끝날 때까지 기다려야 했습니다.
기다림이 길어지니 자동·수동을 떠나 태그 입력 화면으로 들어가는 게 부담스럽다는 참가자도 있었습니다. 결국 문제는 자동으로 태그하느냐 수동으로 태그하느냐가 아니었습니다. 유저가 얼마나 기다려야 하느냐가 문제였습니다.
처음의 기획은 사진을 많이 올리는 유저를 충분히 고려하지 않은 설계였습니다. 1차 세션에서 바로 드러났습니다.
그래서 처리 단위를 바꿨습니다. 지금 보고 있는 사진 한 장만 감지하고, 사진을 넘기면 그때 그 사진을 감지합니다. 유저는 첫 사진의 결과를 바로 보고, 나머지는 사진을 넘기는 동안 처리됩니다.
불완전한 정확도를 받아들이는 방식
2차 세션에서는 정확도가 낮아 답답하다는 반응이 나왔습니다. 그러나 정확도를 개선한 뒤에는 결이 다른 의견이 나왔습니다.
"정확도가 떨어져도 비슷한 상품을 보여주는 거니까 괜찮아요. 오히려 상품 추천이 될 수도 있겠네요."
"매치가 잘못됐다고 해서 크게 불쾌하진 않았어요."
정확도가 높아진 것도 한 이유였지만, 더 중요한 차이는 같은 결과를 유저가 어떻게 받아들이느냐에 있었습니다. 어떤 유저에게는 잘못된 상품이 오답이었지만, 다른 유저에게는 비슷한 상품을 발견할 수 있는 추천이었습니다.
그래서 정확도만 높이는 것으로는 충분하지 않았습니다. 무엇을 정답으로 볼 것인지, 그리고 유저가 그 결과를 어떻게 사용할 것인지까지 함께 봐야 한다는 것을 알게 됐습니다.
진입하면 자동, 누르면 추천
UT에서 얻은 결론을 구조로 옮기면 두 개의 흐름이 됩니다.
흐름 1은 화면 진입 시 자동으로 실행되는 경로입니다. 사진을 전환하면 객체 탐지가 실행되고, 객체마다 상품을 검색한 뒤 유사도가 높은 상품만 태그 후보로 제시합니다.
흐름 1이 찾아낸 탐지 영역은 캐시에 저장되어 흐름 2에서도 사용됩니다.
흐름 2는 유저가 사진의 한 곳을 눌렀을 때 동작합니다. 원하는 순간에, 원하는 물건만 찾는 경로입니다. 자동으로 태그가 붙지 않은 영역에도 유저가 원하면 태그를 빠르게 붙일 수 있습니다.
이를 구현하기 위해 흐름 1에서 탐지 영역을 캐시해두고, 유저가 좌표를 눌렀을 때 누른 좌표가 캐시된 탐지 영역에 포함되는지 확인했습니다. 유저는 자신이 누른 영역이 AI가 탐지한 영역인지 알 필요가 없습니다. 자동으로 AI 추천을 보여주기 때문에 수동으로 클릭한 영역에서도 쉽게 태그를 붙일 수 있습니다.

경계를 넘어 내린 결정
01장의 두 질문은 UT로 답이 나왔습니다. 그런데 답이 정해질 때마다 고쳐야 하는 곳이 한 군데가 아니었습니다. 화면만 바꾸면 되는 게 아니라, 서버와 앱이 주고받는 내용까지 함께 바뀌었습니다.
서버와 앱을 나눠 맡고 있으면 변경이 한 번 생길 때마다 합의도 한 번 더 필요합니다. “이렇게 바꾸려는데 그쪽은 괜찮나요”를 묻고 답을 기다리는 시간이 매번 생깁니다. 그래서 이 프로젝트에서는 한 플랫폼에만 머물지 않고 양쪽을 넘나들었습니다.
앱에 남겨둔 탐지 결과
흐름 2, AI 추천에서 유저가 사진의 한 영역을 누르면, 그 자리에 물건이 있는지 판정해야 합니다. 탐지는 흐름 1에서 이미 끝났으니, 흐름 2에서는 기존에 탐지된 영역 목록과 누른 좌표를 비교하면 됩니다.
문제는 그 탐지 목록을 누가 들고 있을지입니다.
- 서버가 들고 있기 — 앱은 좌표만 보내고, 서버가 비교해 결과를 돌려줍니다. 앱은 아무것도 기억하지 않아도 됩니다.
- 앱이 들고 있기 — 서버가 첫 응답에 탐지된 영역 목록을 함께 담아주고, 앱이 그것을 보관해 직접 비교합니다.
서버가 들고 있는 쪽이 구조는 단순합니다. 대신 유저가 누를 때마다 서버에 다녀와야 합니다. 누르고 기다리고, 다른 곳을 누르고 또 기다립니다.
UT에서 확인한 문제가 바로 이 대기 시간이었습니다. 누를 때마다 기다리게 만들면 유저는 태그를 붙이는 일이 번거로워집니다. 그래서 탐지 목록은 앱이 들고 있는 쪽으로 정했습니다. 누른 즉시 판정하고, 물건이 있는 자리면 바로 추천을 띄웁니다.
하지만 이 결정은 앱 혼자 내릴 수 없습니다. 서버가 첫 응답에 탐지 영역 목록을 담아주지 않으면 앱은 보관할 것이 없습니다. 그리고 영역을 찾은 뒤에는 그 영역의 상품을 검색해야 합니다. 앱이 그 영역을 가리키는 값을 서버에 보낼 수 있어야 하고, 서버는 그 값으로 검색해 답할 수 있어야 합니다.
앱과 서버의 규약을 함께 조정할 수 있었기 때문에, 사진을 누르면 기다림 없이 추천이 뜨도록 만들 수 있었습니다.
에러가 아니라 빈 목록
사진에서 객체를 탐지했는데, 탐지된 객체와 닮은 상품을 찾지 못하는 경우도 있습니다. 이때 서버가 앱에 어떻게 응답을 내려줄지도 고민이 필요했습니다.
- 에러 — “요청을 처리하지 못했다”고 답하기
- 빈 목록 — “찾아봤는데 결과가 없다”고 답하기
서버 입장에서는 에러로 처리할 수도 있습니다. 추천할 상품을 찾지 못했으니 정상적인 결과가 아니라고 볼 수 있기 때문입니다.
하지만 앱에서는 에러를 받으면 실패 화면을 보여줍니다. 네트워크가 끊기거나 서버에 장애가 생겼을 때를 위한 처리입니다.
문제는 상품을 찾지 못한 경우까지 에러로 처리하면, 유저에게 실패 화면이 보인다는 것입니다. 다시 시도해도 사진이 바뀌지 않았으니 결과는 같습니다. 유저는 무엇이 잘못됐는지도 모른 채 같은 동작을 반복하게 됩니다.
유저 입장에서는 실패한 것이 아닙니다. 요청은 정상적으로 처리됐고, 찾아본 결과가 없을 뿐입니다.
그래서 빈 목록으로 응답하기로 했습니다. 화면에서도 “사진에서 추천할 상품을 찾지 못했어요”라고 결과를 그대로 알려주고, 다시 불러오기 버튼은 비활성화했습니다. 다시 요청해도 같은 결과가 나올 것이기 때문입니다.
서버의 응답 방식부터 앱에서 보여줄 문구와 동작까지, 별도의 핸드오프 없이 하나의 결정으로 이어졌습니다.
두 결정에는 공통점이 있습니다. 문제가 먼저 보이는 쪽은 앱이었습니다. 누를 때마다 기다리게 되는 것도, 에러 화면이 유저에게 어떻게 보이는지도 앱을 만들면서 알게 됩니다.
고치는 쪽은 달랐습니다. 둘 다 서버가 무엇을 주고 무엇을 받을지 바꿔야 끝나는 문제였습니다. 앱만 맡고 있었다면 문제를 정리해 전달하고 반영을 기다렸을 것입니다.
양쪽을 다 맡고 있었기 때문에 문제를 발견한 곳에서 바로 다음 결정을 내릴 수 있었습니다. 불편한 지점을 앱에서 찾고, 그 자리에서 서버 응답을 바꾸고, 바뀐 응답에 맞춰 화면을 정리했습니다.
경계를 넘는 비용
경계를 넘기 위한 '비용'도 있었습니다. 서버와 iOS는 빌드하는 방법부터 새로 배워야 했고, 저장소마다 브랜치와 리뷰, QA 방식도 달랐기 때문입니다.
가장 많은 시간을 쓴 건 iOS 빌드 실패였습니다. 에러 메시지는 전혀 다른 곳을 가리키고 있었는데, 실제 원인은 컴퓨터 메모리 부족이었습니다. 한 컴퓨터에서 iOS, 서버, Android를 모두 빌드하다 보니 생긴 일입니다. 하지만 이 비용은 일회성입니다. 다음에 같은 경계를 넘어야 할 때는 처음만큼 오래 걸리지 않습니다.
출시하고 나서 알게 된 것
실제로 대신한 것은 반복 입력
먼저 오토태깅으로 붙은 태그 중 어떤 상품이 선택됐는지를 출처별로 나눠봤습니다.
AI가 관여해 붙은 태그는 전체의 3분의 1이었습니다. 그런데 그중 대부분은 유저가 이미 구매했거나 예전에 태그한 상품이었습니다. 사진만 보고 처음 찾아낸 상품은 6.5%에 그쳤습니다. 오토태깅 전후를 비교해도 이력에 없는 상품이 태그되는 비율은 거의 움직이지 않았습니다.
가장 많은 태그는 예전에 태그했던 상품이었습니다. 유저에게 태그는 자기 물건의 목록이었습니다. 같은 소파, 같은 조명을 계속 다시 태그합니다. AI가 사진에서 찾아낸 것도 대부분 그 목록 안에 있던 물건이었고, 실제로 대신해 준 일은 그 반복 입력이었습니다.
받아들이고, 편집한다
먼저 세 가지를 봤습니다. 기능이 잘 동작했는지, 유저가 받아들였는지, 붙은 태그가 남았는지.
오토태그를 본 사람 세 명 중 두 명은 제안된 태그를 적어도 하나 그대로 남겼습니다. 다만 전체 태그 중 44%는 삭제됐습니다. 기능을 안 쓰는 것이 아니라, 쓰면서 골라냅니다.
그런데 지워지는 비율은 물건에 따라 크게 달랐습니다.

형태와 브랜드가 뚜렷한 물건은 잘 남았고, 커튼이나 침구처럼 무늬와 색으로만 구분되는 물건은 절반 이상 지워졌습니다. 형태와 브랜드가 뚜렷하지 않은 상품일수록 더 많이 지워졌습니다.
이 분포가 다음 방향의 힌트가 됐습니다. 모든 물건에 같은 기준을 적용하는 대신, 잘 지워지는 물건군에서는 조금 더 확신이 있을 때만 제안하도록 기준을 달리 두는 것입니다.
태그가 늘고, 클릭도 늘었다
오토태깅이 적용된 유저와 적용되지 않은 유저를 나눠서 봤습니다.

오토태깅을 통해 태그 수와 클릭은 함께 늘었습니다. 하지만 업로드 시간도 늘었습니다. 오토태깅은 입력 시간을 줄이지는 못했지만, 유저가 콘텐츠에 더 많은 상품 정보를 공유하게 하는 데는 효과가 있었습니다. 이제 남은 숙제는 개수가 아니라 정확도입니다. 지울 태그가 줄어들면 검수에 드는 시간도 함께 줄어들 수 있을 것입니다.
경계를 넘어 문제를 해결하다
이 프로젝트를 통해 얻은 것은 태그에 대한 성과 뿐만이 아니었습니다. 기획부터 UT, 그리고 플랫폼을 넘나드는 개발까지. 만들면서 답을 찾아가는 과정에서 다음을 알게 됐습니다.
앱을 만들기 전에 웹을 만들어보길 잘했습니다. 태그를 자동으로 붙일지 유저가 고르게 할지는 작업의 큰 틀을 결정짓는 질문이었습니다. 그 답을 프로덕션 서버도 앱 빌드도 없이 며칠 만에 얻었습니다. 다른 방향으로 개발을 끝낸 뒤에 답을 얻었다면 되돌리는 데 훨씬 많은 시간을 썼을 것입니다.
앱에서 발견하고, 서버까지 바꿨습니다. 누를 때마다 기다리게 되는 것도, 에러 화면이 잘못 뜨는 것도 앱에서 먼저 보입니다. 다만 고치려면 서버가 무엇을 주고받을지를 바꿔야 합니다. 이 프로젝트에서는 제가 앱과 서버를 함께 맡고 있었기 때문에, 문제를 발견한 자리에서 필요한 규약까지 바로 바꿀 수 있었습니다. 대신 빌드와 배포 절차를 새로 익히는 데 시간을 썼고, 그 시간은 한 번만 들었습니다.
이번 프로젝트에서 중요한 것은 새로운 기술을 익힌 것보다, 문제를 발견한 자리에서 해결 방법까지 직접 이어갈 수 있었다는 점이었습니다. 필요한 곳에서 경계를 넘고, 가장 빠르게 확인할 수 있는 방법을 선택하면서 제품을 만들어갔습니다.
제품에 필요한 일이라면 역할의 경계보다 문제를 먼저 보는 것. 이번 프로젝트를 통해 제가 생각하는 PE의 역할도 조금 더 선명해졌습니다.
