levish
levish
이전으로
Scenfit

Scenfit

All Things Fragrance

Organization scenfit
Period Feb '26 ~ Present
Categories Full-Stack Development · Fragrance · Community Platform

Leading engineering at Scenfit, a fragrance encyclopedia and community, from its API server to the web app and an upcoming native Android app.

Scenfit은 향수 백과사전과 커뮤니티를 하나로 합친 서비스입니다. 2026년 2월 첫 커밋을 시작으로, 지금은 웹에서 한국어와 영어 커뮤니티를 함께 운영하며 Android 앱 출시를 준비하고 있습니다. 저는 CTO로 Scenfit의 기술 전반을 맡고 있습니다.

All Things Fragrance: 향수의 모든 것을 한 곳에

향수 하나를 고르려 해도 여러 곳을 돌아다녀야 합니다. 어떤 노트가 들어갔는지는 정보 사이트에서, 실제로 써 본 사람들의 평은 커뮤니티에서, 같은 브랜드의 다른 향수는 또 다른 곳에서 찾게 됩니다. 정보는 정보대로, 후기는 후기대로 흩어져 있으니 향수 하나를 제대로 알기가 쉽지 않습니다.

Scenfit은 이것들을 한 곳에 모으는 데서 시작했습니다. 향수와 브랜드, 조향사, 노트를 백과사전처럼 찾아보고, 같은 페이지에서 평가와 리뷰를 읽을 수 있습니다. 마음에 드는 향수는 컬렉션에 담고, 직접 시향해 본 향수는 Fit 카드로 기록해 둘 수 있습니다. 향수에 대한 정보와 사람들의 경험이 한 페이지에 함께 쌓이는 것. Scenfit이 처음부터 지켜 온 원칙입니다.

사라지지 않는 기록

커뮤니티 서비스에서 가장 피해야 할 일은 사용자가 남긴 글이 사라지는 것입니다. 그런데 글 하나를 저장하는 일은 생각보다 여러 곳에 걸쳐 있습니다. 데이터베이스에 저장하고, 검색 엔진에 색인하고, 향수의 평가 요약을 다시 계산하고, 알림을 보내야 합니다. 이 저장소들은 하나의 트랜잭션으로 묶을 수 없어서, 중간에 하나만 실패해도 데이터베이스와 검색 결과가 조용히 어긋나기 시작합니다.

그래서 Scenfit은 원본을 데이터베이스 한 곳에만 두고, 나머지는 모두 원본에서 다시 만들 수 있는 사본으로 다룹니다. 글이 저장될 때는 “이 글의 검색 색인을 갱신해야 한다”는 할 일도 같은 트랜잭션 안에서 함께 기록됩니다. 글이 저장됐다면 할 일도 반드시 남아 있고, 글 저장이 취소되면 할 일도 함께 사라집니다.

할 일에는 요청된 시점을 나타내는 리비전이 붙습니다. 작업자는 리비전을 확인한 뒤 색인을 다시 만들고, 그사이 리비전이 바뀌지 않았을 때만 할 일을 지웁니다. 색인을 만드는 도중에 글이 또 수정되면 그 수정은 새 할 일로 남아 다음 차례에 반영됩니다. 작업자가 중간에 멈추거나 메시지 큐가 끊겨도, 할 일은 데이터베이스에 남아 있다가 다시 처리됩니다. 장애는 반영을 늦출 수는 있어도, 기록을 잃게 만들지는 못합니다.

마지막 안전장치로, 매일 새벽 검색 색인 전체를 새 색인에 처음부터 다시 만든 뒤 한 번에 교체합니다. 교체가 끝나기 전까지는 기존 색인으로 검색되기 때문에, 다시 만드는 동안에도 검색은 멈추지 않습니다.

이미지도 기록이다

글은 데이터베이스에서 다시 읽으면 되지만, 사용자가 올린 이미지는 한 번 잃으면 다시 만들 방법이 없습니다. 게다가 파일 저장소는 데이터베이스와 함께 롤백되지 않습니다. 이미지는 저장됐는데 글 저장이 실패하면 주인 없는 파일이 남고, 반대로 정리 작업이 엇갈리면 아직 쓰이는 파일이 지워질 수 있습니다.

Scenfit은 이미지를 쓰기 전에 같은 저장소에 “이 위치에 이미지를 쓰려 한다”는 작은 기록을 먼저 남깁니다. 데이터베이스 저장이 끝나면 이 기록을 지우고, 미처 지우지 못한 기록은 정리 작업이 주기적으로 확인합니다. 데이터베이스가 그 이미지를 참조하고 있으면 기록만 지우고, 아무도 참조하지 않으면 그때 삭제 대상으로 넘깁니다. 저장소 전체를 뒤질 필요 없이, 남은 기록만 보면 됩니다.

이미지 주소는 한 번 쓰면 다시 쓰지 않습니다. 이미지를 바꾸면 항상 새 주소에 저장하고, 이전 이미지는 같은 트랜잭션 안에서 삭제 대상으로 등록됩니다. 그래서 삭제 작업이 도중에 실패해 다시 실행되더라도, 같은 주소에 새로 올라온 파일을 잘못 지울 일이 없습니다. 글 안에 들어간 이미지는 임시 저장본이나 이전 리비전이 쓰고 있는 동안 보호되고, 어디에서도 쓰이지 않는 이미지만 하루의 유예를 둔 뒤 정리됩니다.

순위는 시간이 아니라 반응이 바꾼다

커뮤니티의 인기 글이나 홈의 인기 리뷰처럼, Scenfit에는 지금 사람들이 주목하는 것을 보여 주는 순위가 여러 곳에 있습니다. 흔한 방법은 글의 나이만큼 점수를 깎는 것입니다. 하지만 그렇게 하면 시간이 흐르는 것만으로 순서가 계속 바뀌어 다음 페이지를 넘기는 사이 목록이 뒤섞이고, 오래된 글은 다시 주목받아도 위로 올라오지 못합니다.

Scenfit은 글이 아니라 반응 하나하나를 시간에 따라 줄어들게 합니다. 좋아요나 댓글이 달릴 때마다 점수가 더해지고, 그 점수는 반감기에 따라 서서히 줄어듭니다. 이때 모든 글에 똑같이 적용되는 감소분은 저장하지 않고 읽을 때 곱하도록 해, 저장된 점수는 새 반응이 들어올 때만 바뀌게 했습니다.

  • 시간이 흐르는 것만으로는 순서가 바뀌지 않아, 페이지를 넘겨도 목록이 흔들리지 않습니다.
  • 오래된 글도 다시 반응을 얻으면 언제든 위로 올라올 수 있습니다.
  • 좋아요를 취소하면 그 좋아요가 더했던 값을 정확히 뺍니다. 눌렀다 취소하기를 반복해 점수를 부풀릴 수 없습니다.
  • 점수는 글마다 숫자 하나로 저장되어, 순위를 위해 주기적으로 전체를 다시 계산할 필요가 없습니다.

인기 글의 반감기는 일주일, 인기 리뷰는 30일입니다. 작성자 본인의 조회나 좋아요는 점수에 넣지 않습니다.

사람들이 맡은 향

향수 상세 페이지의 어코드 차트는 브랜드가 발표한 공식 구성이 아니라, 실제로 향을 맡아 본 사람들의 투표로 그려집니다. 사람들이 가장 많이 느꼈다고 답한 노트들을 골라, 각 노트가 어떤 어코드에 얼마나 속하는지를 곱해 어코드별로 더합니다. 이 비중은 클라이언트가 정할 수 없고, 노트에 매겨진 어코드의 순서와 주된 어코드인지 여부로부터 서버가 계산합니다. 아직 투표가 없는 향수는 공식 구성으로 대신 채우지 않고, 비어 있는 그대로 보여 줍니다.

향수를 찾을 때 쓰는 필터에도 같은 고민이 들어가 있습니다. 어코드 필터 옆의 숫자는 “지금 결과 중 이 어코드가 몇 개인가”가 아니라 “이 어코드를 고르면 몇 개가 나오는가”를 보여 줍니다. 필터를 하나 고를 때마다 다른 선택지의 숫자가 0으로 사라지지 않아, 사용자가 막다른 길에 빠지지 않습니다.

번역이 아니라 커뮤니티

해외 사용자를 받는 가장 쉬운 방법은 화면을 번역하는 것입니다. 하지만 같은 향수를 두고도 한국어 사용자와 영어 사용자가 나누는 이야기는 다릅니다. 그래서 Scenfit은 언어마다 화면만 바꾸는 대신, 언어마다 커뮤니티를 따로 두었습니다.

  • 함께 쓰는 것: 계정, 향수 정보, 평가, 컬렉션, 알림
  • 커뮤니티마다 따로 쌓이는 것: 게시판, 리뷰, 큐레이션, 이벤트, 신고와 제재

리뷰는 커뮤니티마다 따로 쓸 수 있지만, 평가는 한 사람당 한 번만 셉니다. 한국어로 리뷰를 쓰고 영어로 또 써도, 그 향수의 평가에는 한 표만 들어갑니다.

권한도 커뮤니티 안에 머뭅니다. 커뮤니티 운영진은 자기 커뮤니티의 글과 사용자만 관리할 수 있고, 향수 정보를 고치거나 계정 전체를 정지할 수는 없습니다. 경고가 쌓여도 그 커뮤니티에서의 활동만 정지되고, 계정 정지는 따로 내려야 하는 결정으로 남겨 두었습니다. 전체 운영자라도 한국어 커뮤니티의 주소로 영어 커뮤니티의 글을 열 수는 없습니다. 웹에서 커뮤니티를 바꾸면 페이지를 새로 불러와, 작성 중이던 에디터나 권한 상태가 다른 커뮤니티로 넘어가지 않게 했습니다.

고쳐 쓰는 동안에도

큐레이션은 공개 전에 검토를 거치는 긴 글입니다. 이미 공개된 큐레이션을 고칠 때도 마찬가지입니다. 그래서 큐레이션은 하나의 글 안에 공개본과 작업본을 따로 둡니다. 작업본을 고치는 동안 독자는 승인된 공개본을 그대로 읽고, 승인이 나는 순간 작업본이 공개본으로 바뀝니다. 반려되어도 작업본은 남아 있고, 누가 언제 어떤 의견으로 검토했는지는 리비전마다 기록됩니다.

커뮤니티 글을 수정할 때는 다른 문제가 있습니다. 수정 중인 임시 저장본이 있는데 그사이 원래 글이 바뀌었다면, 그대로 덮어쓰는 순간 그 변경은 사라집니다. Scenfit은 임시 저장본이 어느 버전의 글에서 출발했는지 기록해 두고, 발행하는 순간 원래 글이 그 뒤로 바뀌었는지 확인합니다. 바뀌었다면 아무것도 저장하지 않고 알려 줍니다. 자동으로 합치거나 강제로 덮어쓰지 않고, 사용자가 지금의 글을 확인한 뒤 다시 고치도록 했습니다. 조회수나 좋아요, 댓글이 늘어난 것만으로는 글이 바뀐 것으로 보지 않습니다.

네이티브로, 그러나 따로 놀지 않게

출시를 앞둔 Android 앱은 웹을 감싸는 대신 네이티브로 만들었습니다. 웹을 감싼 앱은 만들기는 빠르지만, 스크롤 하나, 터치 하나에서 앱다운 느낌이 나지 않습니다.

대신 웹과 앱이 서로 다른 서비스처럼 느껴지지 않도록 했습니다. 앱의 문구는 웹의 번역 파일에서 그대로 가져오고, 둘이 달라지면 검사에서 바로 드러납니다. 글쓰기 에디터는 웹의 에디터를 그대로 가져와 본문 영역에만 씁니다. 제목과 게시판 선택, 툴바, 이미지 선택은 네이티브 화면이 맡고, 에디터 안의 코드는 로그인 정보를 전혀 보지 못한 채 이미지 업로드를 앱에 맡깁니다. 임시 저장도 웹과 같은 서버 저장본을 쓰기 때문에, 앞에서 말한 수정 충돌 확인도 웹과 똑같이 동작합니다.

앱이 쓰는 API 클라이언트는 서버가 직접 만들어 배포하고, 앱은 검증된 버전을 고정해서 씁니다. 앱을 처음 켤 때와 향수 목록을 넘겨 볼 때의 실행 경로는 미리 측정해 앱에 담아 두어, 첫 실행부터 빠르게 동작하도록 했습니다.

Scenfit: Live

Scenfit은 지금 scenfit.com에서 운영되고 있고, Android 앱은 출시를 준비하고 있습니다.

첫 커밋을 올린 지 8개월이 지났습니다. 아직 완성이라고 부르기는 이르지만, 향수에 대한 정보와 경험을 한 곳에 모은다는 처음의 방향은 그대로입니다.

© 2026 levish