브랜딩을 싫어하는 개발자를 위한 도구를 만들었습니다: Kurim 이야기

아이디어만 넣으면 로고, 브랜드 아이덴티티, 랜딩 페이지까지 만들어주는 AI 브랜딩 도구 Kurim을 만들면서, 정작 로고 품질이 아니라 보안 감사에 시간을 다 쓴 이야기. git 히스토리에서 제 비밀번호를 지운 날의 기록 포함.

브랜딩을 싫어하는 개발자를 위한 도구를 만들었습니다: Kurim 이야기

사이드 프로젝트를 만들다 보면 코드는 반나절이면 되는데 이름, 로고, 랜딩 페이지 문구 앞에서 매번 멈칫하게 됩니다.

그래서 만든 게 Kurim입니다. 랜딩 페이지 뱃지에도 그대로 써놨어요. “브랜딩을 싫어하는 개발자를 위해 만들었습니다.” 이 문구는 제품에 실제로 박혀 있는 태그라인이고, 이 글의 방향이기도 합니다.

이 글은 “AI로 로고 만드는 서비스를 만들었어요” 같은 자랑 글이 아닙니다. 오히려 로고 품질보다 보안 감사에 시간을 다 쓴, 좀 김빠지는 이야기에 가깝습니다. 중간에는 git 히스토리에서 제 비밀번호를 지우는 장면도 나옵니다.

뭘 하는 도구인가

한 줄로 말하면, 대충 던진 아이디어를 브랜드 한 세트로 바꿔주는 도구입니다. 로고, 브랜드 아이덴티티 문서, 랜딩 페이지, 컬러 팔레트까지 나옵니다.

Kurim 랜딩 페이지 상단. '브랜딩을 싫어하는 개발자를 위해 만들었습니다'라는 문구 아래 '10분 만에 완성하는 브랜드'라는 큰 제목과 'AI가 로고, 브랜드 아이덴티티, 랜딩 페이지를 한 번에 생성합니다'라는 설명, 무료로 시작하기·데모 보기 버튼이 보인다.

흐름은 세 단계입니다. 프로젝트를 설명하고, AI가 전부 생성하고, 원클릭으로 배포하는 것.

Kurim 사용 방법과 기능 소개. 위쪽에는 프로젝트 설명, AI가 모든 것 생성, 원클릭 배포의 3단계가 번호와 함께 있고, 아래쪽에는 AI 로고 생성, 브랜드 아이덴티티 문서, 원클릭 배포, 컬러 & 타이포그래피, 브랜드 킷 내보내기, 개발자 API의 6개 기능 카드가 격자로 배열되어 있다.

개발자를 대상으로 만든 티가 나는 부분이 몇 개 있는데, 저는 이런 걸 넣을 때가 제일 재밌었어요. 하나는 브랜드 문서를 BRAND.md 같은 마크다운으로 뽑아준다는 점입니다. 가이드라인을 PDF로 받아봤자 안 읽거든요. 레포에 .md로 들어와 있으면 얘기가 다르죠. 또 하나는 GitHub README를 읽어서 프로젝트 설명을 자동으로 채워주는 기능인데, 만들면서 “이건 나부터 쓰겠다” 싶었습니다.

값싼 모델로 시작했다가 DALL·E 3로 갈아탄 이유

처음 로고 생성은 Cloudflare Workers AI(flux-1-schnell)로 붙였습니다. 이유는 단순합니다. 쌌거든요. 백엔드에 계산 주석까지 남겨놨는데, 1024×1024 이미지 12장이 대략 691 Neuron, 그러니까 $0.0076 정도였습니다. 같은 12장을 DALL·E 3로 뽑으면 $0.48입니다. 63배 차이예요.

그런데 브랜딩 도구에서 로고는 첫인상 전부입니다. 값싼 모델은 빠르고 저렴한 대신 결과물이 “그럭저럭”에서 멈췄고, 그 첫인상이 서비스 신뢰도를 그대로 깎아먹었습니다.

그래서 provider를 팩토리로 추상화해두고(cloudflare / replicate / dalle), 실제 기본값은 DALL·E 3로 올렸습니다. 값싼 옵션을 지운 게 아니라 언제든 되돌릴 수 있게 남겨둔 채, 기본 경험만 비싼 쪽으로 밀어올린 거예요.

63배 비싼 모델을 쓰면서 비용을 감당한 방법은 좀 시시합니다. 로고를 더 적게 생성하도록 바꿨어요. 예전엔 컨셉을 잔뜩 뽑아 늘어놓는 식이었는데, 장당 단가가 오르니 그럴 수가 없더라고요. “많이 뽑아 고르는” 도구에서 “좋은 걸 몇 개 뽑는” 도구로 방향이 바뀐 셈입니다.

로고 생성의 현실

DALL·E 3로 바꿔도 이미지 생성 모델의 고질병은 남습니다. 심볼이나 추상적인 아이콘은 잘 나오는데, 글자가 들어가는 워드마크는 철자를 자주 틀립니다. 모델이 글자를 “그림”으로 그리기 때문이에요.

Kurim이 생성한 로고 시안 세 개. 왼쪽은 미니멀한 워드마크 스타일, 가운데는 보라색 계열의 기하학적 아이콘, 오른쪽은 역동적인 레터폼 형태의 로고로, 흰 배경 위에 나란히 배치되어 있다.

가운데 기하학적 아이콘이나 오른쪽 레터폼처럼 심볼 계열은 그대로 써도 될 만큼 나오는데, 텍스트가 정확히 박혀야 하는 워드마크는 여전히 복불복입니다. 그래서 Kurim의 로고 생성은 “컨셉을 빠르게 잡아보는 용도”로 정직하게 위치를 잡았고, 마케팅 카피도 이 현실에 맞춰 정리했습니다.

원래는 “15개 이상 로고 컨셉” 같은 숫자로 자랑하는 문구였는데, 리뷰 20라운드쯤에서 그냥 “로고 컨셉”으로 줄였습니다. 실제 생성 개수와 첫인상 품질을 둘 다 지키려면 그 숫자가 오히려 거짓말에 가까웠거든요. (아직 상세 화면 한 구석엔 12+ 라벨이 남아 있어서, 이 글 쓰면서 하나 더 발견했네요.)

프런트/백엔드를 왜 나눴나

프런트엔드는 Next.js 14(App Router) + TypeScript + Tailwind + shadcn/ui, 다국어는 next-intl로 한국어/영어를 지원합니다. 백엔드는 FastAPI + SQLAlchemy 2.0 + Pydantic v2, DB는 PostgreSQL 16, 캐시와 레이트 리밋은 Redis 7입니다.

“Next.js 하나로 다 하지 왜 백엔드를 따로 뒀냐”는 질문을 스스로도 했는데, 실제로 나눠보니 이유가 세 군데서 튀어나왔습니다.

로고 생성이 오래 걸리는 비동기 작업이라 요청을 던져놓고 WebSocket으로 진행 상황을 받아야 했고, 브랜드 킷 export는 ZIP을 통째로 메모리에 올린 뒤 로고를 하나씩 직렬로 내려받았고, 배포 엔드포인트는 Vercel/Netlify를 폴링하면서 HTTP 요청을 오래 잡고 있었습니다. 전부 서버리스 함수 위에 얹기엔 불편한 작업이라, 무거운 건 FastAPI 쪽으로 빼는 게 자연스러웠습니다. 나중에 리뷰에서도 export의 메모리 ZIP과 직렬 다운로드, 폴링으로 오래 잡는 배포 엔드포인트가 성능 지적으로 그대로 올라왔고요. 처음엔 감으로 나눈 경계였는데, 리뷰가 그 감이 틀리지 않았다고 확인해준 셈입니다.

첫 커밋은 2025년 11월 23일 새벽 2시 40분이었고, 그때는 정말 로고 생성기 하나였습니다. 사흘 뒤인 11월 26일 하루에 Phase 1(UX), Phase 2(수익화 기반), Phase 3(편집기·협업)라는 이름의 PR 세 개를 몰아서 머지한 기록이 남아 있어요. 이후로 커밋을 월별로 세보면 11월 30개, 12월 11개, 1월 68개, 그렇게 총 179개입니다. 1월의 68개는 대부분 기능이었습니다. 결제, 팀, Open API와 개발자 문서, 그리고 1월 13일에 일본어 지원을 붙였다가 1월 20일에 도로 뺀 흔적 같은 것들이요. 배포는 Kubernetes에 올려서 CI/CD로 이미지를 굴리고 HPA까지 붙였는데, 사이드 프로젝트치고 과하긴 하지만 어차피 클러스터는 이미 돌고 있었습니다.

문제는 2월입니다. 2월의 30커밋에는 기능이 거의 없습니다.

git 히스토리에서 제 비밀번호를 지운 날

발단은 1월 19일의 커밋 하나였습니다. feat(auth): add default user creation on startup. 배포할 때마다 로그인 계정을 새로 만드는 게 귀찮아서, 서버가 뜰 때 기본 계정을 자동으로 만들어 넣게 한 겁니다. 문제는 그 계정의 이메일과 비밀번호를 main.py에 평문으로 하드코딩했다는 것. “어차피 내 클러스터에서만 도는데”라는, 아마 세상에서 제일 흔한 변명과 함께요.

2월 5일 보안 점검에서 이게 걸렸습니다. 그날 오후 remove hardcoded credentials 커밋으로 코드에서는 지웠는데, 지운다고 끝이 아니더라고요. git은 히스토리를 기억합니다. 로그를 따라 올라가면 그 비밀번호가 그대로 있었습니다.

그래서 다음 날인 2월 6일 오후 1시 34분, BFG Repo-Cleaner로 저장소 히스토리 전체를 다시 썼습니다. BFG가 남긴 리포트의 changed-files 목록에는 파일이 딱 하나, main.py뿐이에요. 지금 히스토리에서 그 커밋을 열면 이메일과 비밀번호가 있던 자리에 ***REMOVED***만 남아 있습니다. 유출 규모 자체는 다행히 귀여운 수준이었지만(개인 프로젝트의 시드 계정 하나), “코드에서 지우는 것”과 “히스토리에서 지우는 것”이 완전히 다른 작업이라는 걸 손으로 배웠습니다.

그 2월 전체가 이런 식이었습니다. 2월 5일엔 DB에 평문으로 저장하던 OAuth 토큰을 Fernet 암호화로 바꿨고, 2월 6일과 7일엔 production hardening PR 네 개(#32~#35)를 연달아 머지했는데 그중 둘은 새벽 1시대에 들어갔습니다. 이어서 2월 9일부터 18일까지는 의존성 CVE 패치만 했어요. Pillow, cryptography, starlette, Next.js와 tar까지. 브랜딩 도구를 만든다면서 2월 한 달 동안 로고 관련 커밋은 하나도 없었습니다.

스물한 라운드의 점수표

수동으로 하는 점검에 한계를 느껴서, 2월 21일부터는 아예 페르소나 멀티-LLM 리뷰를 정례화했습니다. 보안, 아키텍처, 코드 품질, 데이터베이스, DevOps, 프런트엔드, 성능 — R01은 일곱 명의 리뷰어로 시작했고, R04부터 마케터와 PM이 합류했습니다. 3월 20일의 R21까지 스물한 라운드를 돌았고, R21이 짚은 문제(로고 메타데이터 누출, 웹훅 SSRF)의 수정이 머지된 건 5월 4일이었습니다.

결과는 냉정했습니다. R11에서 보안 리뷰어는 5.3/10에 BLOCK을 줬습니다. XSS, OAuth CSRF, SSRF가 서로 연결될 수 있어서(chainable) 이대로 공개 운영하면 안 된다는 이유였어요. 같은 라운드에서 데이터베이스는 4.4/10에 CRITICAL, 아키텍처는 4.5/10, 제품은 5.9/10, 개발자-마케팅 관점은 64/100이었습니다. 내 손으로 만든 서비스에 이런 점수표가 붙는 걸 보는 건 꽤 겸허해지는 경험이었습니다. “보안 감사를 배웠다”는 건 멋있는 말이 아니라, 이 점수들을 하나씩 올리느라 시간을 다 썼다는 뜻이에요.

점수보다 뼈아팠던 건 지적의 결이었습니다. 백엔드 테스트는 인증, 기본 프로젝트 CRUD, 가격 정책 정도만 덮고 있었고, OAuth state 처리나 공유 링크, 생성 HTML 실행, 웹훅 남용 같은 정작 위험한 경로에는 자동화된 검사가 없었습니다. “테스트 32개 통과”라는 초록불이 안심의 근거가 되지 못한다는 걸 이때 배웠어요.

iframe을 왜 그렇게까지 가뒀나

가장 기억에 남는 지적은 랜딩 페이지 미리보기였습니다. AI가 생성한 HTML을 브라우저에서 보여줘야 하는데, 처음엔 이걸 Blob으로 만들어 URL.createObjectURL로 열었습니다. 문제는 이 blob URL이 같은 오리진에서 실행된다는 점이었어요.

우리 앱은 JWT를 localStorage에 들고 있었습니다. 그런데 AI가 생성했거나 남이 공유한 랜딩 페이지 HTML을 같은 오리진에서 실행하면, 그 안의 스크립트가 localStorage의 토큰을 그대로 읽어갈 수 있습니다.

말 그대로 토큰을 훔치는 XSS 통로가 열려 있던 거죠. 생성된 HTML을 “그냥 보여주는” 기능이, 사용자가 만든 브랜드일수록 더 위험한 실행 가능한 콘텐츠라는 걸 이때 실감했습니다. “미리보기 버그” 정도가 아니라 인증 경계가 뚫린 문제였습니다.

그래서 지금은 생성 HTML을 sandbox="" 속성을 준 iframe 안에서만 렌더링합니다. allow-*를 하나도 주지 않은, 가장 강하게 가둔 상태예요. 3월 20일 R19 라운드의 커밋 메시지가 그대로 fix: sandbox public landing page iframe to prevent XSS입니다. 처음엔 그냥 “미리보기 예쁘게 보여주기”였던 게, 리뷰를 거치면서 “신뢰할 수 없는 코드를 최대한 격리하기”로 바뀌었습니다.

삽질과 죽은 코드

리뷰가 잡아준 것 중엔 창피한 것도 많았습니다.

가장 대표적인 게 brand_system이라는 서브시스템입니다. 1월 14일 커밋 메시지에는 “Complete Brand Asset Management System (BAMS) implementation”이라고 자신 있게 적어놨는데, 두 달 뒤 데이터베이스 리뷰어가 보니 이 “완성된” 시스템은 라이브 API에 제대로 연결되지 않은 채 죽어 있었어요.

게다가 ORM 모델과 Alembic 마이그레이션이 서로 달랐습니다. 모델은 brand_systems.user_id를 요구하는데(그것도 다른 곳은 전부 UUID인데 여기만 Integer로), 마이그레이션은 그 컬럼을 아예 만들지 않았어요. 로컬에서 create_all()로 띄운 DB와 마이그레이션으로 만든 프로덕션 DB의 스키마가 서로 다른, 전형적인 스키마 드리프트였죠. 로컬에서 잘 돌아가니 배포 전까지 몰랐던 겁니다. 데이터베이스 리뷰가 4.4/10에 BLOCK을 준 이유의 절반이 여기 있었습니다.

그 밖에도 OAuth state 검증이 fail-open이라 Redis가 없거나 state가 비어 있어도 콜백이 그냥 통과했고, 웹훅 URL 검증은 도메인을 IP로 풀어보지 않아 DNS 리바인딩으로 뚫렸고, 여러 테넌트가 공유하는 배포 크레덴셜이 지나치게 노출돼 있어서 아무 인증된 사용자나 플랫폼 소유의 Vercel/Netlify 계정을 쓸 수 있었습니다. 하나같이 “로고 만드는 앱”에서 나올 거라고 예상 못 한 문제들이었어요.

아키텍처 쪽에서는 브랜드와 프로젝트를 표현하는 모델이 서로 경쟁하듯 두 갈래로 자라 있었고, Project 하나에 워크플로가 너무 많이 매달려 프런트/백엔드 양쪽에서 비대해져 있다는 지적을 받았습니다. 앞서 말한 죽은 brand_system도 결국 이 “무엇이 브랜드의 진짜 원본인가”를 두 번 정의한 결과였고요.

UI가 구현보다 앞서 있던 흔적도 있었습니다. 로그인 화면은 새 OAuth provider 세 개를 광고하는데 백엔드에는 그에 대응하는 계약도 검증도 테스트도 없었고, 온보딩 투어는 실제로 존재하지 않는 사이드바를 설명하고 있었으며, 커맨드 팔레트에는 아무 데도 가지 않는 항목이 섞여 있었습니다. 화면을 먼저 그리고 뒤를 못 채운, 전형적인 사이드 프로젝트의 자국이었어요.

재밌는 건, 리뷰가 매번 같은 결론에 도달했다는 겁니다. “제품이 마케팅보다 강하다.” README import, 공개 공유 페이지, 배포/내보내기 워크플로 같은 진짜 차별점은 이미 만들어져 있는데, 정작 랜딩 페이지 카피는 그걸 제대로 못 보여주고 너무 일반적이라는 지적이었죠. 개발자가 흔히 하는 실수를, 브랜딩 도구를 만들면서도 똑같이 하고 있었던 겁니다.

아직 부족한 것

솔직히 Kurim은 “완성된 제품”이라기보단 “잘 돌아가는 실험”에 가깝습니다. 로고 텍스트 문제는 남아 있고, 브랜드 탐색 페이지는 아직 공개된 프로젝트가 없어 비어 있고, 결제나 팀 협업은 붙여놨지만 실사용자로 검증된 상태는 아니에요.

그래도 만들길 잘했다고 생각하는 이유는, 적어도 새 프로젝트를 시작할 때 이름과 로고 앞에서 며칠씩 멈추지는 않게 됐다는 것, 그리고 브랜딩 도구를 만들려다 프로덕션 보안을 몸으로 배웠다는 것입니다. 로고를 자동으로 뽑는 것보다 그쪽이 훨씬 값진 소득이었던 것 같아요.

돌아보면 이 프로젝트에서 기억에 남는 순간은 로고가 잘 나온 날이 아닙니다. 2월 6일 오후, BFG가 히스토리를 다시 쓰는 걸 지켜보던 순간, 그리고 R11 점수표에서 5.3/10 옆의 BLOCK을 읽던 순간입니다. 다음 사이드 프로젝트는 화면을 그리기 전에 이 두 장면부터 떠올릴 것 같아요.

궁금하신 분은 kurim.jiun.dev에서 직접 던져보셔도 됩니다. 여러분의 프로젝트 이름은 제 것보단 제대로 렌더링되길 바랍니다.

Comments

Loading comments...