법률 전문가 매칭 플랫폼을 운영하다 보면, 기능 완성도만큼이나 사용자 경험(UX)의 디테일이 신뢰도를 좌우한다는 걸 매일 체감합니다. 이번 스프린트에서는 SvelteKit 5 + FastAPI 스택으로 구성된 플랫폼에 채팅 UI 통일, 닉네임 시스템 구축, TipTap 에디터 도입, 모바일 최적화, 비밀번호 변경 API 등 30여 건의 개선 작업을 진행했습니다. 이 글에서는 그 중 핵심 구현을 코드 중심으로 정리합니다.
1. 닉네임 시스템 구축 — 중복 체크 API 설계
법률 상담 플랫폼 특성상 개인정보 보호가 중요합니다. 채팅에서 실명 대신 닉네임을 표시하고, 회원가입·마이페이지에서 중복 확인을 제공합니다. 비로그인용과 로그인용 두 가지 엔드포인트를 분리했습니다.
# FastAPI — 닉네임 중복 체크 (로그인 유저, 본인 제외)
@router.get("/user/check-nickname")
async def check_nickname_auth(
nickname: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
if len(nickname.strip()) < 2:
return {"available": False, "message": "닉네임은 2자 이상이어야 합니다."}
if len(nickname.strip()) > 20:
return {"available": False, "message": "닉네임은 20자 이하여야 합니다."}
existing = db.query(User).filter(
User.nickname == nickname.strip(),
User.id != current_user.id # 본인 닉네임은 통과
).first()
return {
"available": not bool(existing),
"message": "이미 사용 중인 닉네임입니다." if existing else "사용 가능한 닉네임입니다."
}
SvelteKit 프론트엔드에서는 닉네임 입력 후 중복 확인 버튼을 누르면 실시간으로 API를 호출하고, 결과에 따라 폼 제출 가능 여부를 제어합니다.
// SvelteKit — 닉네임 중복 체크 함수
async function checkNickname() {
if (!user.nickname.trim()) return;
nicknameCheckLoading = true;
const token = $authStore.token;
const res = await fetch(
`${apiUrl}/api/user/check-nickname?nickname=${encodeURIComponent(user.nickname)}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
nicknameCheckResult = await res.json();
isNicknameChecked = true;
nicknameCheckLoading = false;
}
2. 채팅 UI 통일 — 오픈채팅 vs 1:1 채팅 스타일 일관성
플랫폼에는 전문가와 1:1로 대화하는 개인 채팅과 누구나 참여하는 오픈채팅 두 가지 채널이 있습니다. 기존에는 두 화면의 메시지 레이아웃이 달라 사용자가 혼란스러울 수 있었습니다. 핵심 변경 포인트는 세 가지입니다.
- 발신자 이름: 각 메시지 버블 위에 이름 표시 (1:1 채팅에도 동일 적용)
- 전문가 배지: 전문가 메시지에 회색
전문가뱃지 노출 - 프로필 이미지: 모바일에서도 아바타 표시 (
hidden md:block제거)
<!-- SvelteKit — 1:1 채팅 상대방 메시지 (개선 후) -->
<div class="flex items-start gap-2">
<!-- 프로필 이미지 (모바일 포함) -->
<svelte:element
this={otherPerson.expertId ? 'a' : 'div'}
href={otherPerson.expertId ? `/experts/${otherPerson.expertId}` : undefined}
>
{#if otherPerson.image}
<img src={otherPerson.image} class="w-9 h-9 rounded-full object-cover" />
{:else}
<div class="w-9 h-9 bg-gradient-to-br from-gray-600 to-gray-700 rounded-full" />
{/if}
</svelte:element>
<div class="flex-1 min-w-0">
<!-- 이름 + 전문가 배지 -->
<div class="flex items-center gap-1.5 mb-1">
{#if otherPerson.expertId}
<a href="/experts/{otherPerson.expertId}" class="font-medium text-sm hover:underline">
{otherPerson.name}
</a>
<span class="bg-gray-500 text-white text-[10px] px-1.5 py-0.5 rounded font-medium">전문가</span>
{:else}
<span class="font-medium text-gray-900 text-sm">{otherPerson.name}</span>
{/if}
</div>
<!-- 메시지 버블 + 시간 -->
<div class="flex items-end gap-2">
<div class="bg-white rounded-2xl rounded-tl-sm px-3.5 py-2.5 shadow-sm">
<p class="text-gray-900 text-[15px] whitespace-pre-wrap">{message.content}</p>
</div>
<span class="text-[11px] text-gray-600">{formatTime(message.created_at)}</span>
</div>
</div>
</div>
3. 비밀번호 변경 API — FastAPI + 해시 검증
기존 마이페이지의 “비밀번호 변경” 버튼에는 on:click 핸들러조차 없었습니다. 백엔드 API부터 신규 작성하고, 프론트에는 모달 UI를 추가했습니다.
# FastAPI — 비밀번호 변경 엔드포인트
class PasswordChangeRequest(BaseModel):
current_password: str
new_password: str
@router.put("/user/password")
async def change_password(
data: PasswordChangeRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 현재 비밀번호 검증
if not verify_password(data.current_password, current_user.hashed_password):
raise HTTPException(status_code=400, detail="현재 비밀번호가 올바르지 않습니다.")
# 최소 길이 체크
if len(data.new_password) < 8:
raise HTTPException(status_code=400, detail="새 비밀번호는 8자 이상이어야 합니다.")
# 해시 후 저장
current_user.hashed_password = get_password_hash(data.new_password)
db.commit()
return {"message": "비밀번호가 변경되었습니다."}
4. 모바일 최적화 — Tailwind CSS 반응형 클래스 전략
법률 상담 서비스 특성상 모바일 사용 비율이 높습니다. 마이페이지 사이드바 내비게이션, 프로필 이미지 크롭 모달, 닉네임 입력 영역 등 여러 곳에서 모바일 레이아웃을 개선했습니다. Tailwind의 반응형 프리픽스를 적극 활용했습니다.
대표적인 패턴은 사이드바 탭을 모바일에서 가로 스크롤 탭으로 전환하는 것입니다. 데스크탑에서는 세로 목록이지만 모바일에서는 수평으로 스크롤되어 화면 공간을 효율적으로 씁니다.
<!-- 마이페이지 탭 — 모바일 가로 스크롤, 데스크탑 세로 목록 -->
<nav class="flex overflow-x-auto gap-1 sm:flex-col sm:overflow-x-visible
pb-2 sm:pb-0 scrollbar-hide">
<button class="flex-shrink-0 flex items-center gap-2 px-4 py-3 rounded-xl
font-medium text-sm whitespace-nowrap transition
{activeTab === 'profile'
? 'bg-gray-800 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-100'}">
내 정보
</button>
<!-- 더 많은 탭... -->
</nav>
5. 요약 자동 생성 — 본문에서 메타 데이터 추출
성공사례·칼럼·Q&A 작성 시 "요약" 항목을 별도로 입력하는 불편함을 없앴습니다. 제출 시 본문 HTML에서 태그를 제거하고 앞 200자를 자동으로 요약으로 활용합니다. 간단하지만 실사용에서 체감 효율이 높은 개선입니다.
// 본문 HTML → 요약 자동 추출 (SvelteKit)
function extractSummary(htmlContent) {
return htmlContent
.replace(/<[^>]*>/g, '') // HTML 태그 제거
.replace(/\s+/g, ' ') // 연속 공백 정리
.trim()
.substring(0, 200); // 최대 200자
}
// 폼 제출 시 자동 적용
async function handleSubmit() {
formData.summary = extractSummary(formData.content);
// ... API 호출
}
마치며
이번 스프린트를 통해 법률 O2O 플랫폼의 기능 완성도와 UX 디테일이 한 단계 올라갔습니다. 작은 불편함 하나하나를 해소하는 것이 사용자 신뢰로 이어지고, 결국 플랫폼의 경쟁력이 됩니다. 닉네임 시스템, 채팅 UI 통일, 모바일 최적화, 편집기 교체—어느 것 하나 쉬운 작업이 없었지만, SvelteKit 5의 반응형 렌더링과 FastAPI의 빠른 개발 속도가 큰 도움이 됐습니다.
코드벤터는 이처럼 기획 단계부터 배포까지 전 과정을 AI 코딩과 글로벌 협력 네트워크를 통해 빠르고 정교하게 구현합니다. 법률, 의료, 교육 등 전문가 매칭 플랫폼이나 O2O 서비스 개발을 고려하신다면 언제든지 문의해 주세요.