글 쓰고 고치는 화면을 따로 두어

2025.12 ~ 2026.04 (운영 중)

블로그 글과 포트폴리오 작업물을 작성·관리하는 어드민 SPA입니다.

과제

  • 콘텐츠를 저장소 밖 API로 옮긴 뒤 글을 쓰고 고칠 도구가 필요해짐
  • 어드민 Mantine과 블로그 shadcn이 갈라져 같은 폼을 두 번 구현

담당 범위

  • 화면 12개 라우트: Vite + React, TanStack Router / Query (1인)
  • Monaco 에디터 작성 폼, Recharts 통계 대시보드
  • 2단계 인증 프론트 플로우와 세션 타이머

설계 판단

  • 어드민만 Vite + React SPA로 구성 → 로그인 뒤 화면이라 SSR 불필요, 빌드 단축
  • Mantine을 걷어내고 shadcn 기반 공용 패키지로 통일 → 컴포넌트·토큰 중복 제거
  • 만료 전 남은 시간을 표시하고 연장을 제안 → 긴 글 편집 중 본문 유실 방지

결과 지표

  • TypeScript/TSX 103개 파일, 6,625줄
  • 라우트 12개
  • 어드민 앱에 닿은 커밋 23건
원문 자세히 보기

과제

블로그 콘텐츠를 저장소 밖 API로 옮기고 나니 글을 쓰고 고칠 도구가 필요해졌다. 동시에 어드민은 로그인 뒤에서만 도는 화면이라 SEO나 초기 렌더 비용보다 '편집 중 상태를 얼마나 잘 붙들고 있느냐'가 중요했고, 블로그·포트폴리오가 이미 쓰던 shadcn과 어드민의 Mantine이 갈라져 같은 폼을 두 번 만들고 있었다.

담당 범위

라우트 12개 · 소스 6,625줄 · 리소스 8종 CRUD

1인 개발. (1) TanStack Router 파일 기반 라우팅으로 로그인·포스트·포트폴리오 관리 화면 12개 라우트 구성, (2) TanStack Query 기반 서버 상태 관리와 entities 단위 쿼리 옵션/훅 캡슐화, (3) TOTP 2단계 인증의 프런트 플로우(코드 입력 폼, 실패 처리, 세션 수립)와 남은 세션 시간을 표시·연장하는 세션 타이머 훅 구현 — 서버 측 TOTP 검증은 별도 저장소의 API 서버에 있고 그쪽 항목에서 다룬다, (4) Monaco 에디터를 붙인 글·작업물 작성 폼(react-hook-form + Zod 검증), (5) Recharts 기반 애널리틱스 대시보드(유입 경로 파이 차트 등), (6) 어드민에 남아 있던 Mantine을 걷어내고 shadcn/Radix 기반 자체 공통 패키지 공용 컴포넌트로 통일, (7) FSD 레이어(app → pages → features → entities → shared)로 소스 구조 정리.

보기

선택안 · 대안 · 근거 · 비용

어드민만 Next.js가 아니라 Vite + React SPA로 만들고, 라우팅은 TanStack Router 파일 기반으로 잡았다

대안블로그·포트폴리오와 같이 Next.js App Router로 통일하거나, 어드민을 블로그 앱 안의 /admin 경로로 흡수하는 안근거어드민은 전부 로그인 뒤 화면이라 SSR·RSC·SEO에서 얻는 게 없고, 오히려 서버 컴포넌트 경계가 편집 폼의 클라이언트 상태를 쪼개는 비용만 남는다. 빌드도 세 앱 중 어드민만 정적 산출물이라 Vite 쪽이 훨씬 빠르다. 라우팅은 TanStack Query와 같은 계열이라 로더·캐시 연동이 자연스러웠다.비용세 앱 중 하나만 번들러·라우터·렌더링 모델이 달라, 공용 패키지가 Next와 Vite 양쪽 빌드에서 모두 동작해야 한다는 제약이 생겼다(packages/ui·packages/shared가 두 환경을 다 만족해야 한다). 앱 간 코드 이동도 그대로는 안 되고 라우팅·데이터 패칭을 다시 써야 한다.

어드민에서 Mantine을 전부 걷어내고 shadcn/Radix 기반 packages/ui 공용 컴포넌트로 통일했다 (커밋 b624987, 이어서 블로그의 중복 shadcn 컴포넌트도 00087bc에서 공용 패키지로 흡수)

대안어드민은 Mantine을 유지하고 공용 패키지는 타입·API 클라이언트만 공유하는 안근거어드민(Mantine)과 블로그(shadcn)가 서로 다른 컴포넌트 체계를 쓰면 같은 폼·다이얼로그·테이블을 두 번 만들게 되고, 디자인 토큰도 두 벌이 된다. shadcn은 컴포넌트 소스를 저장소가 소유하는 방식이라 공용 패키지로 끌어올리기가 쉬웠다.비용Mantine이 기본 제공하던 것들(날짜 입력, 폼 상태, 알림 등)을 직접 조립하게 되어 초기 작업량이 늘었고, 이전 작업 자체가 어드민 화면 전반을 건드리는 큰 diff라 그 시점에 다른 기능 작업을 병행할 수 없었다.

세션 만료를 요청 실패 시점에 처리하지 않고, 남은 시간을 초 단위로 세는 useSessionTimer 훅을 두어 만료 5분 전부터 경고와 연장 버튼을 노출했다

대안401 응답을 받으면 그때 로그아웃시키거나, 사용자 조작이 있을 때마다 조용히 세션을 연장하는 안근거어드민의 주 작업이 '긴 글을 Monaco 에디터에서 편집하는 것'이라, 저장 버튼을 누르는 순간 401이 나면 작성 중이던 본문을 잃는다. 남은 시간을 보여주고 만료 전에 연장을 제안하면 사용자가 저장 타이밍을 스스로 정할 수 있다.비용만료 시각을 클라이언트가 들고 세므로 탭이 오래 백그라운드에 있거나 기기 시계가 어긋나면 표시가 실제와 벌어질 수 있다. 표시가 정확해도 서버가 세션을 먼저 끊으면 소용없어서, 401 처리 경로를 없앤 게 아니라 그 위에 얹은 안전장치라는 점은 그대로 남는다.

사용 기술

TypeScriptReactViteTanStack RouterTanStack QueryTanStack Table