커뮤니티 글쓰기 기능 개발기
앱에 커뮤니티 글쓰기 기능을 붙이고 오래 유지보수했다. “글 하나 쓰는 기능”이 뭐 얼마나 복잡하겠나 싶지만, 자동차 커뮤니티라 글에 딸려오는 게 많다. 차량 태그, 차량 비교, 이미지 여러 장, 링크 프리뷰, 투표까지. 글쓰기 하나가 사실상 작은 에디터였다. 개발하면서 겪은 것과 리팩토링한 흔적을 정리한다.
자유주제/구매질문/시승후기 등 글 종류가 여러 개고, 종류마다 입력 필드가 다르다. 이 다양성을 어떻게 한 화면에서 다루느냐가 이 기능의 핵심이었다.
글쓰기는 사실 “혼합 콘텐츠 에디터”였다
가장 먼저 부딪힌 건, 글 본문이 단순 텍스트가 아니라는 것. 텍스트 사이사이에 이미지, 링크 프리뷰가 끼어드는 혼합 콘텐츠(mixed content) 구조였다. 그래서 본문을 하나의 문자열이 아니라 타입이 붙은 아이템 배열로 모델링했다.
type MixedContentType = 'text' | 'link' | 'image';
interface MixedContentItem {
type: MixedContentType;
content?: string;
payload?: Record<string, { imagePath?: string } & Record<string, unknown>>;
}
interface InputData {
title?: string;
content?: string;
mixedContent?: MixedContentItem[];
}
본문을 mixedContent[]로 두면 렌더링도, 커서 위치 관리도 아이템 단위로 다룰 수 있다. “텍스트 중간에 이미지를 꽂는다”가 배열 중간에 {type:'image'}를 삽입하는 문제로 바뀐다. 커서 위치를 별도로 관리해야 해서 이걸 위한 상태 액션(handleMixedContentCursorSet)까지 따로 뒀다.
글 종류마다 다른 입력, 컴포넌트로 쪼개기
글 종류가 여러 개다. 자유주제(FreeTopic), 구매질문(PurchasingQuestion), 시승후기(TestDriveReview), 오너리뷰(OwnerReview), 한줄평(ShortReview)… 각각 필요한 입력이 다르다. 초기엔 이게 한 덩어리로 뭉쳐 있어서 유지보수가 힘들었다.
그래서 공통 입력 모듈을 잘게 쪼개서 조합하는 구조로 갔다.
InputContent— 본문 입력ImagePreviewer— 이미지 첨부/미리보기LinkPreviewer— 링크 프리뷰CarTagList/CarComparison— 차량 태그, 차량 비교OptionContent,Vote,Region— 옵션, 투표, 지역
글 종류 컴포넌트는 이 모듈들을 필요한 것만 골라 조립한다. 자유주제는 이미지·링크·투표를 쓰고, 구매질문은 차량 정보를 더 쓰는 식. 모듈로 쪼개니 종류 추가가 “새 조합 하나 만들기”가 됐다.
TypeScript 적용 — 가장 크게 재미 본 작업
이 기능은 원래 JS로 짜여 있었다. 입력 종류가 많고 각 종류의 payload가 달라서, JS일 때는 “이 글 종류엔 어떤 필드가 필요하더라”를 매번 코드를 뒤져 확인해야 했다. 그래서 커뮤니티 생성 컴포넌트 전체에 TypeScript를 적용하는 작업을 했다.
Write.tsx, CommunityWriteContainer.tsx, FreeTopic.tsx, PurchasingQuestion.tsx, 각종 모듈(ImagePreviewer, InputContent, OptionContent…)을 .js에서 .tsx로 전환했다. props에 타입을 박으니 두 가지가 바로 좋아졌다.
- 어떤 데이터가 오가는지 시그니처만 봐도 안다. 위
FreeTopicProps처럼 props에 뭐가 들어오는지 명시되니, 컴포넌트 조립할 때 뭘 넘겨야 하는지 헷갈리지 않는다. - 글 종류별 payload 누락을 컴파일 타임에 잡는다. JS일 때 런타임에서야 터지던 “필드 빠짐”이 편집기에서 바로 잡힌다.
💡 입력 필드가 많고 케이스별로 구조가 다른 기능일수록 타입의 이득이 크다. “이 케이스엔 이 필드가 필요하다”가 문서가 아니라 타입으로 강제되니까.
payload 만들기 — 케이스별 조립
최종적으로 서버에 보낼 payload는 글 종류에 따라 조립한다. 종류별 분기에서 필요한 필드만 채운다.
// createCommunityWritePayload (요지)
// 리뷰류 글: 차량 등급/가격/리뷰 항목 + 차량명
payloadData.id_cargrade = carData.gradeId;
payloadData.price = inputData.price;
payloadData.review_items = content;
payloadData.car_name = carData.carName;
이 car_name 필드는 나중에 차량 정보 없이도 글을 쓸 수 있게 하면서 추가됐다. 원래는 차량을 반드시 선택해야 글이 완성됐는데, 차량을 특정하지 않는 글도 허용하려니 문제가 생겼다. 차량과 제목이 둘 다 없으면 글에 표시할 이름이 없었던 것.
그래서 차량 미선택 시 id_grade=0으로 보내고, 차량·제목이 없을 때는 car_name으로 대체 표기되도록 했다. “필수였던 값을 선택으로 바꾸는” 흔한 변경인데, 이럴 때는 그 값이 없을 때의 대체 경로를 payload와 표시 양쪽에서 다 챙겨야 한다는 걸 다시 확인했다.
계측(로그)도 기능의 일부였다
커뮤니티는 사용자 행동 데이터가 중요해서, 글쓰기·상세에 이벤트 수집이 촘촘히 붙었다. 투표 결과 보기 클릭, 커뮤니티 상세의 투표 유무 같은 것까지 수집했다. 여기서 배운 건, 웹뷰 쪽 로그 스펙과 앱 로그 스펙이 안 맞으면 임시로 이전 방식으로 원상복구해야 하는 상황이 생긴다는 것. 계측은 프론트만의 일이 아니라 연동된 저장소들과 스펙을 맞춰야 하는 작업이었다.
정리
“커뮤니티 글쓰기”는 이름과 달리 작은 에디터였다.
- 본문을
mixedContent[]로 모델링해 텍스트·이미지·링크를 한 흐름에서 다뤘다. - 입력 모듈을 잘게 쪼개 글 종류별로 조합하는 구조로 갔다.
- 전체를 TypeScript로 전환해 케이스별 payload 누락을 컴파일 타임에 잡았다.
- “차량 필수 → 선택” 같은 변경에선 대체 경로(
car_name)를 payload·표시 양쪽에서 챙겼다.
입력 종류가 많은 기능일수록 “쪼개서 조합 + 타입으로 강제”가 유지보수를 살린다는 걸 이 기능에서 제일 많이 느꼈다.
관련 작업
- 커뮤니티 생성 컴포넌트 TypeScript 적용 / 글작성 리팩토링 (Write·CommunityWriteContainer·FreeTopic·PurchasingQuestion 등
.js→.tsx) - 차량정보 없는 커뮤니티글 생성 (
car_name,id_grade=0대체 처리) - 커뮤니티 글 작성 차량 할인률 수정
- 투표 결과 보기 클릭 이벤트·커뮤니티 상세 vote 유무 수집 추가