상용 서비스를 목표로 바이브 코딩을 진행하다 보면, 가장 가볍게 다뤄지는데 가장 큰 비용을 만드는 결정이 두 개 있다. 웹 에디터를 어떻게 만들 것인가, 그리고 게시물의 이미지·영상을 어디에 저장할 것인가다. 결론부터 말하면 세 가지를 출시 전에 결정해야 한다. 에디터 코드의 분리 구조, 게시물에 포함되는 이미지의 저장 위치, 그리고 그 저장 위치에 대한 접근 통제 정책이다. 이 셋을 뒤로 미루면, 서비스가 굴러가기 시작한 뒤에 신형·구형으로 갈라진 게시물을 손으로 마이그레이션하는 일이 벌어진다
에디터는 콘텐츠가 흐르는 모든 통로에 들어간다
에디터를 가볍게 보는 이유는 단순하다. 처음에는 관리자가 공지를 쓸 때만 쓸 거라고 생각하기 때문이다. 그런데 막상 서비스를 운영해 보면 에디터가 들어가는 자리는 빠르게 늘어난다
강의 판매 서비스를 예로 들면 이렇다. 강의 소개 페이지의 본문, 강사의 자기소개, 사용자 프로필, 질문과 답변, 커뮤니티 글과 댓글까지 전부 에디터가 필요하다. 이 모든 곳의 입력 경험이 동일한 컴포넌트를 통해 만들어지기 때문에, 에디터의 직관성이 곧 서비스의 사용자 경험이다
에디터를 잘 만들어 두면 다음 서비스로 옮길 때 그대로 재사용할 수 있다는 부수 효과도 있다. 그래서 첫 서비스에서 에디터에 들이는 시간은 단발성 비용이 아니라 자산에 가까운 투자다
TipTap을 골랐다면 기본 기능만으로는 부족하다
Next.js 환경에서 가볍게 쓸 수 있는 에디터로 TipTap이 자주 추천된다. 직접 만드는 게 아니라 외부 라이브러리를 가져다 쓰는 구조라 에디터 자체를 화면에 붙이는 일은 어렵지 않다. 문제는 그다음이다
기본 툴바 버튼은 굵게·기울임·목록·링크 같은 최소 항목만 들어 있다. 콘텐츠 박스, 박스 안의 박스, 정렬, 테두리 색상, 배경색, 단축키, 컨트롤+S 자동 저장, 마우스 우클릭 메뉴, 버블 메뉴 같은 항목은 전부 직접 구현해야 한다. 이미지가 들어오기 시작하면 더 늘어난다. 크기 조절, 원형 출력, 텍스트 옆에 이미지를 띄우는 인라인 배치, 블록 배치, 위치 속성 같은 것들이 모두 커스텀 대상이다
또 하나 신경 써야 할 부분은 편집 시점과 게시 시점의 렌더링이 다를 수 있다는 점이다. 편집 화면에서는 잘 보이던 박스가 게시된 페이지에서는 다른 모양으로 그려진다. 두 시점의 스타일을 일관되게 유지하는 작업도 별도로 해야 한다
정리하면, TipTap을 붙였다는 것은 작업의 시작이지 끝이 아니다. 에디터 관련 코드는 서비스 운영 내내 계속 손이 가는 영역이다
Claude Code는 단일 파일이 1만 라인을 넘으면 멈춘다
에디터에 기능을 하나씩 추가해 달라고 Claude Code에게 시키다가, 어느 순간부터 모델이 응답을 못 하기 시작할수 있다. 새 기능 추가, 버그 수정, 어떤 지시를 내려도 토큰만 소진하고 결과물이 나오지 않았다. 결국 잘 동작하던 시점으로 git revert를 해야 할 수도 있다
Claude Code 멈추었다면 그 원인은 컨텍스트 한계이다. Claude Code의 컨텍스트 윈도우는 단일 고정 수치가 아니다. Pro 기본 세션은 200K 토큰이지만, Max·Team·Enterprise 플랜에서 Opus/Sonnet 4.6 이상을 쓸 때는 1M 토큰까지 확장된다. 가용분 역시 고정값이 아니라 시스템 프롬프트, MCP 서버, 커스텀 에이전트, 스킬, 자동 컴팩트 버퍼가 차지하는 양에 따라 변동한다. 200K 세션 기준으로 보통 100K~170K 사이를 오가며, 한도에 닿으면 자동 컴팩션이 오래된 대화를 요약으로 압축해 공간을 확보한다. 진짜 문제는 한계 수치 자체가 아니라, 별도 지시가 없을 때 Claude Code가 새로 만드는 코드를 기존 파일에 계속 쌓는 경향이 있다는 점이다
TipTap 커스터마이즈를 하다 보면 익스텐션이 빠르게 늘어난다. 자동 저장, 단축키, 버블 메뉴, 박스 메뉴, 이미지 처리, 툴바 버튼이 한 파일에 누적되면, 매 수정마다 그 파일 전체를 다시 들고 가는 비용이 가용 공간을 잠식한다. 어느 시점부터는 같은 세션이 새 변경을 수용하지 못하는 상태에 도달한다
해결은 extensions 디렉터리 분리와 CLAUDE.md 규칙
대응은 의외로 간단하다. 첫째, 에디터 관련 코드를 디렉터리 단위로 미리 분리한다. 둘째, 그 분리 규칙을 CLAUDE.md에 못 박아 둔다. Claude Code가 알아서 해 주길 기대하면 안 된다. 명시하지 않으면 다시 한 파일로 모은다
구조 예시는 다음과 같다
src/components/editor/ ├── core/ # 에디터 인스턴스 생성, 설정 ├── extensions/ # 기능별 익스텐션 1파일 1기능 │ ├── auto-save.ts │ ├── shortcut-save.ts │ ├── bubble-menu.ts │ ├── box-menu.ts │ └── image-resize.ts ├── toolbar/ # 툴바 버튼 단위 분리 └── styles/ # 편집 시점·게시 시점 공통 스타일
CLAUDE.md에는 다음과 같은 규칙을 넣어 둔다
# Editor Coding Rules 1. 에디터 관련 신규 기능은 반드시 src/components/editor/extensions/ 아래에 기능별 단일 파일로 분리한다. 2. 에디터의 설정과 비즈니스 로직은 같은 파일에 두지 않는다. 3. 툴바 버튼은 src/components/editor/toolbar/ 아래에 버튼 단위로 분리한다. 4. 익스텐션 파일명은 기능을 나타내는 케밥 케이스로 한다. 예) auto-save.ts, shortcut-save.ts, bubble-menu.ts 5. 단일 파일이 500라인을 넘으면 즉시 분리 후보다.
이 규칙이 있어도 가끔은 Claude Code가 한 파일로 몰려 한다. 그럴 때는 새 세션에서 “지금 어떤 파일에 코드가 집중되고 있는지, 어떤 기능을 어디로 분리하면 좋을지”를 먼저 묻고 정리시킨 다음에 작업을 이어간다. 기획 회의용 데스크톱 세션에서 분리 계획을 받아오고, Claude Code 세션에는 결과만 던지는 방식이 안정적이다
게시물에 들어가는 이미지를 DB에 넣지 않는다
에디터 다음에 결정해야 하는 것은 콘텐츠의 저장 구조다. 게시물의 본질은 글이지만, 그 안에 이미지와 영상이 함께 들어간다. 글은 DB에 넣어도 무리가 없다. 이미지와 영상은 다르다
이미지는 바이너리 데이터다. DB의 텍스트 컬럼에 그대로 못 들어간다. 그래서 흔히 Base64 인코딩으로 문자열로 변환해 넣는 코드가 만들어진다. 별도 지시가 없으면 AI도 이쪽으로 코드를 짜 주는 경우가 있다. 이 방식은 두 가지 측면에서 좋지 않다
첫째, 텍스트 컬럼의 길이 한도를 쉽게 넘긴다. 게시물 한 건이 글자 한도를 초과해 저장에 실패하는 일이 벌어진다. 둘째, DB는 클라우드 인프라에서 가장 비싼 자원이다. AWS의 RDS는 다른 컴퓨트 자원과 비교해 단가가 높은 편이다. 이미지 같은 멀티미디어 데이터를 DB에 넣는 순간, 페이지를 읽을 때마다 비싼 자원이 입출력에 동원된다. 트래픽이 늘면 그대로 비용 곡선이 가파르게 올라간다
결론은 명확하다. 글은 DB에, 이미지·영상은 별도 스토리지에 둔다
WAS의 로컬 파일 시스템도 답이 아니다
DB가 답이 아니라면 WAS의 로컬 디스크는 어떨까. 단기적으로는 동작한다. 장기적으로는 네 가지 문제가 발생한다
첫째, WAS가 송수신 부하를 떠안는다. API 서버는 비즈니스 로직을 처리하는 자원이다. 대용량 미디어를 직접 흘려보내는 일은 본업이 아니다. 둘째, 서버를 늘렸을 때 동기화가 안 된다. 인스턴스 A에 업로드된 파일을 인스턴스 B가 모른다. 셋째, 배포할 때마다 파일을 같이 옮겨야 한다. 컨테이너 기반 배포에서는 더 골치가 아프다. 넷째, 장애로 인스턴스가 날아가면 파일도 날아간다
게시물에 첨부된 이미지·영상은 처음부터 별도의 오브젝트 스토리지에 저장한다. 이게 디폴트 결정이다
이미지 저장 결정과 함께 묶어야 할 항목들
저장 위치만 결정하면 끝이 아니다. 함께 잠가 두지 않으면 나중에 비용이 커지는 항목이 몇 가지 더 있다
첨부파일 이름 중복 정책이다. 같은 이름의 파일을 두 번 올릴 수 있게 할지, 자동으로 해시·UUID를 붙여 충돌을 막을지, 그리고 그 검사를 프론트에서 할지 백엔드에서 할지 정해야 한다. 일반적으로 백엔드에서 UUID 기반의 키를 부여하고, 원본 파일명은 메타데이터로 별도 저장한다
용량 제한도 빼놓을 수 없다. 웹은 HTTP 기반이고, 대용량 파일 송수신을 위한 프로토콜이 아니다. 기가바이트급 파일은 별도 프로토콜이 필요한 영역이다. 일반적으로 게시물 첨부는 1GB 이하에서 한도를 정한다. 이미지·영상별로 다른 한도를 두는 경우도 많다
가장 중요한 것은 접근 통제다. URL만 알면 누구나 접근할 수 있게 둘지, 로그인 사용자만 접근하게 할지, 유료 구매 사용자만 접근하게 할지에 따라 구조가 달라진다. 공개 객체로 두면 CDN 캐싱이 단순해지지만 권한 통제가 약해진다. 비공개로 두려면 백엔드에서 일회성 서명 URL을 발급하는 방식이 표준에 가깝다. 이 결정은 첫 업로드 코드를 짜기 전에 끝나 있어야 한다
DB 스키마는 멀티미디어를 가정해서 다시 그린다
저장 위치를 외부로 옮기면 DB 스키마도 같이 손봐야 한다. 게시물 본문은 글이지만, 그 글에 어떤 이미지가 어디서 참조되는지를 알 수 있어야 한다. 첨부파일은 별도 테이블로 빼는 편이 운영하기 편하다
대략적인 모양은 다음과 같다
posts ├── id ├── content (글 본문 - 마크다운/HTML) └── created_at post_attachments ├── id ├── post_id (FK) ├── original_name (사용자가 올린 파일명) ├── storage_key (S3 객체 키, UUID 기반) ├── content_type ├── size ├── visibility (public / login / paid) └── created_at
storage_key는 절대 사용자가 추측할 수 없는 값이어야 한다. 그래야 비공개 객체에 대한 무단 접근 시도를 1차로 막을 수 있다
AWS 종속성과 비용은 결정의 일부다
여기까지 오면 한 가지 무거운 항목이 남는다. AWS 종속성이다. S3 하나로 시작하지만, 운영이 진행되면서 RDS, EC2, CloudFront, IAM, CloudWatch 같은 서비스가 차례로 붙는다. 이때부터는 다른 클라우드로 옮기는 비용이 빠르게 커진다
비용 감각도 미리 잡아야 한다. 가벼운 사이드 프로젝트라도 AWS에 모든 컴포넌트를 올리면 월 150~200달러가 기본선이다. 사용자 수가 1만 명에 근접하면 그 위로 곱하기 2~4배가 일반적이다. 월 1000달러 청구서가 낯설지 않다
대안은 두 가지다. 하나는 처음부터 사무실 또는 개인 서버에 MinIO를 올려서 핵심 인프라를 자가 보유하는 방식이다. 또 하나는 AWS의 1년 프리티어 안에서 실습을 진행하고, 트래픽이 의미 있게 늘어나는 시점에 본격적인 비용 의사결정을 다시 하는 방식이다. 어느 쪽이든 상관없지만, “AWS로 일단 가고 나중에 생각하자”는 선택은 사실상 AWS 종속을 받아들이는 결정이라는 점을 인지하고 가야 한다
TipTap 첫 적용 단계의 실전 포인트
이론은 여기까지로 두고, 실제로 TipTap을 처음 붙일 때 자주 부딪히는 포인트를 정리한다
첫째, 에디터를 적용할 영역의 폭이 좁다. 모달 안에 에디터를 넣는 경우 특히 그렇다. 폭을 조정해야 하는데, AI에게 “여기 폭을 늘려 줘”라고 모호하게 지시하면 엉뚱한 곳을 건드린다. 크롬 개발자 도구의 셀렉터로 대상 요소의 클래스명을 직접 확인하고, “이 클래스명을 가진 요소의 폭을 전체 목록 폭과 동일하게 맞춰 달라”는 식으로 클래스 단위로 지시한다. 클래스나 ID 같은 식별자는 한 요소를 정확히 가리키게 해 준다
둘째, TipTap을 처음 붙이면 SSL 관련 오류가 자주 난다. 화면에 노출되는 콜 스택과 메시지를 그대로 복사해서 프롬프트에 넣으면 거의 정형화된 패턴으로 답이 돌아온다. 임의로 요약하지 말고 원문 그대로 붙여 넣는 편이 빠르다
셋째, 다크 테마에서 목록 기호가 보이지 않는 일이 있다. 텍스트를 선택해서 불릿이나 번호 목록을 적용했는데 기호 자체가 화면에 나타나지 않는다. 출력이 안 된 게 아니라, 배경과 같은 색으로 그려져 안 보이는 경우가 대부분이다. 다크 테마용 마커 색상 스타일을 별도로 지정해 둔다
넷째, 첫 적용 단계에서는 이미지 처리를 미지원으로 두고 시작한다. 이미지가 들어오면 곧바로 저장 위치, 접근 통제, 스키마 같은 결정이 줄줄이 따라온다. 위에서 정리한 항목들이 모두 합의되기 전에는 텍스트 전용으로 띄워 두는 편이 안전하다
출시 전에 결정해야 할 것을 출시 전에 결정한다
정리하면, TipTap 같은 에디터를 붙이기 전에 다음을 끝내 둔다. 에디터 코드의 디렉터리 구조와 CLAUDE.md의 분리 규칙, 게시물 이미지·영상의 저장 위치(S3 또는 MinIO), 접근 통제 정책, 첨부파일 메타데이터 테이블 설계, AWS 종속성과 비용에 대한 의사결정. 이 다섯 개가 미정인 상태로 에디터 기능 추가에 들어가면, 결국 어딘가에서 한번은 git revert를 하게 된다. 한 줄로 요약하면, 콘텐츠가 흐르는 통로의 구조는 코드 한 줄을 짜기 전에 결정한다