Next.jsSaaSAI 코딩 어시스턴트제품 설계Claude Code

AI 코딩 어시스턴트와 SaaS 만들기 ①: URL 구조부터 다시 잡은 이야기

2026년 7월 4일6 분 읽기

시작하며: 이 시리즈를 쓰는 이유

요즘 사이드 프로젝트로 MatchDa라는 서비스를 만들고 있다. 호주·뉴질랜드 IT 채용공고를 자동으로 모아서, AI로 매칭 점수를 매기고, 공고에 맞춘 맞춤 이력서·커버레터까지 자동 생성해주는 잡 매칭 플랫폼이다.

이 프로젝트의 특이한 점은, 코드를 거의 다 Claude Code(AI 코딩 어시스턴트)와 함께 짰다는 것이다. 개발을 다시 시작한 입장에서 "AI랑 페어 프로그래밍하면 진짜 SaaS를 완성할 수 있을까?"를 실험해보고 싶었다.

결론부터 말하면 — 된다. 그런데 사람이 개발할 때와는 다른 종류의 함정들이 있다. 이번 세션 하나에서만 URL 구조 리팩토링, 기능 분리, 사용자 피드백에 따른 플로우 재설계, 그리고 (부끄럽지만) AI가 실제 계정을 삭제해버린 사고까지 있었다. 이 경험들을 시리즈로 정리해두려고 한다.

  • 1편(이 글): URL 구조 정리 + 대시보드/지원현황 분리 + 잡 탐색 플로우 재설계
  • 2편: 데이터 유실 사고와 복구 — AI에게 DB를 맡길 때 반드시 알아야 할 것
  • 3편: 이력서 자동분석 + RAG로 맞춤 이력서 품질 올리기
  • 4편: 추천 기업 원클릭 수집 + 고객센터 챗봇 + Stripe 구독 결제

이번 편은 "화면과 URL을 정리하는" 비교적 조용한 리팩토링처럼 보이지만, 사실 SaaS를 만들 때 가장 자주 흔들리는 부분이기도 하다.

왜 URL부터 정리해야 했나

MatchDa는 원래 /matchda, /matchda/dashboard 같은 경로 아래 화면들을 붙여나가는 식으로 개발됐다. 초반 프로토타입 단계에서는 편했지만, 기능이 늘어나면서 문제가 드러났다.

  • 로그인 여부와 무관하게 /matchda가 대시보드 취급을 받아서, 비로그인 사용자가 들어오면 화면이 어중간했다
  • 랜딩 페이지(마케팅용 소개 화면)와 실제 서비스 화면이 URL상 구분되지 않았다
  • 공유 링크나 북마크를 만들기 애매했다 (/matchda/dashboard를 사람들에게 보여줄 수는 없으니까)

그래서 이번 세션 초반에 URL 구조를 아예 다시 잡기로 했다.

Step 1. 원칙 정하기

리팩토링에 들어가기 전에 원칙을 먼저 정했다. AI 어시스턴트에게 코드를 맡길 때는 "무엇을 어떻게 바꿀지"를 먼저 문장으로 합의해두는 게 훨씬 중요하다는 걸 느꼈다. 그렇지 않으면 AI가 그럴듯하지만 원하지 않는 구조로 짜버릴 수 있다.

정한 원칙은 단순했다.

경로역할로그인 필요
/공개 랜딩(서비스 소개) 페이지아니오
/dashboard로그인 후 첫 화면(요약)
/applications지원 현황(칸반/리스트)
/discover잡 탐색(공고 수집)
/workspace공고별 이력서·커버레터 작업 공간
/profile이력서 스튜디오

핵심은 "/는 항상 랜딩"이라는 규칙이다. 로그인한 사용자든 아니든 /에 들어오면 같은 소개 페이지를 보되, 로그인 상태라면 헤더의 CTA만 "회원가입"에서 "대시보드로 가기"로 바뀌는 식이다.

Step 2. 커밋 단위로 나눠서 진행

실제 커밋 로그를 보면 이렇게 나눠서 진행했다.

048f69d refactor: URL 구조 정리 — 루트는 랜딩, 대시보드는 /dashboard
63e8f11 refactor: 워크스페이스를 /workspace 로 이전 — URL 구조 통일 완료
6d3b2df feat: 지원 현황(/applications) 분리 + 대시보드 요약화 + 리스트 토글·잡탐색 검색/정렬

한 커밋에 모든 걸 다 넣지 않고 "URL 이동" → "워크스페이스 이동" → "기능 분리"로 쪼갠 이유는, AI 어시스턴트가 만든 큰 diff를 한 번에 리뷰하기 어렵기 때문이다. 커밋을 작게 쪼개면 문제가 생겼을 때 git revert 한 번으로 되돌릴 수 있는 범위가 좁아진다.

048f69d 커밋의 실제 변경 파일 목록을 보면 이런 식이다.

src/app/auth/callback/route.ts                     |  2 +-
src/app/dashboard/page.tsx                         | 78 +++++++++++++++++++
src/app/login/LoginForm.tsx                        |  2 +-
src/app/matchda/dashboard/page.tsx                 | 30 +-------
src/app/matchda/page.tsx                           |  6 +-
src/app/page.tsx                                   | 90 +++-------------------
src/components/matchda/landing/LandingHeader.tsx   | 28 ++++---
src/middleware.ts                                  |  4 +-

/app/page.tsx가 90줄이나 줄어든 이유는, 원래 여기에 "로그인 여부에 따라 대시보드 or 랜딩을 조건부 렌더링"하던 로직이 들어있었기 때문이다. 이걸 걷어내고 순수하게 랜딩만 남기니 코드가 확 가벼워졌다. 그 대신 실제 대시보드 로직은 /app/dashboard/page.tsx로 옮겼다.

옛 경로로 들어오는 사용자를 위한 리다이렉트도 잊지 않았다.

/matchda            → /
/matchda/dashboard   → /dashboard

북마크나 캐시된 링크로 들어오는 사용자가 깨진 화면을 보지 않도록 하는 최소한의 배려다.

Step 3. 대시보드와 "지원 현황"을 분리하기

URL을 정리하고 나니 대시보드 하나가 너무 많은 역할을 하고 있다는 게 보였다. 보드/리스트 토글, 통계 카드, 활동 내역, 공고 추가까지 전부 대시보드 한 화면에 몰려 있었다.

그래서 6d3b2df 커밋에서 **"지원 현황"**이라는 새 메뉴/페이지(/applications)를 만들어 무거운 기능들을 옮기고, 대시보드는 요약 화면으로 단순화했다.

src/app/applications/page.tsx                      |  76 +++++++++++++
src/app/dashboard/page.tsx                         |   9 +-
src/components/matchda/dashboard/ApplicationsScreen.tsx | 73 +++++++++++++
src/components/matchda/dashboard/DashboardScreen.tsx     | 118 ++++++++++++++-------

역할 분담을 정리하면 이렇다.

화면보여주는 것
대시보드 (/dashboard)통계 카드(번역 이력서·저장 공고·진행 중 지원·평균 매칭률) + 최근 공고 요약
지원 현황 (/applications)칸반 보드(준비 중·지원 완료·면접·오퍼) + 리스트 뷰 + 일괄 매칭

같은 김에 오래된 버그도 하나 잡았다. 리스트 뷰 토글 버튼이 있는데 눌러도 아무 반응이 없던 문제였다. 원인은 "로그인했는데 저장된 공고가 하나도 없는" 상태에서 리스트 뷰 자체를 비활성화해버리는 조건문 때문이었다. 빈 리스트도 엄연히 유효한 상태인데, 없는 것과 안 되는 것을 구분하지 못한 전형적인 버그였다. 6d1c38f 커밋에서 먼저 손을 댔고, 이번 세션에서 로그인 시 항상 리스트를 활성화하도록 정리했다.

Step 4. 잡 탐색 플로우 — 내가 잘못 설계했던 부분

이번 세션에서 가장 배운 게 많았던 부분이다. 처음에 나는 이렇게 설계했다.

잡 탐색(/discover)에서 채용페이지를 등록해서 공고를 수집하면, 그 공고가 바로 지원 현황(/applications)에 들어간다.

그럴듯해 보였다. 그런데 실제로 써보니 이상했다. "탐색"과 "관리 대상"이 구분이 안 되니까, 그냥 둘러만 보고 싶은 공고까지 전부 지원 현황에 쌓였다. 사용자(나 자신이지만 제품 관점에서는 최종 사용자) 입장에서 피드백은 명확했다.

"잡 탐색에 들어가야 하고, 거기서 '관리 보내기' 버튼을 눌러야 지원현황에 들어가야 한다."

핵심은 탐색(browse)과 관리(track)를 분리해야 한다는 것. 이건 기능 명세서를 아무리 잘 써도 실제로 화면을 눌러보기 전까지는 잘 안 보이는 종류의 문제였다. AI 어시스턴트와 일할 때도 마찬가지다 — 처음 설계가 틀렸다는 걸 알아채는 건 결국 사람이 써보고 나서다.

그래서 e4ee239 커밋에서 플로우를 다시 짰다.

feat: 잡 탐색에 전체 수집 공고 풀 + '관리 보내기' → 지원 현황

- 잡 탐색에 공유 jobs 풀(아직 지원현황에 없는 공고) 섹션 추가, 검색·정렬 지원
- '관리 보내기' 버튼 → addJobToApplications 로 match 생성 → 지원 현황에 노출, 풀에서 제외
- 기존 채용페이지 수집 공고(discovered_jobs)는 '내 채용페이지 수집 공고' 섹션으로 구분

바뀐 데이터 흐름을 그림으로 정리하면 이렇다.

[채용페이지 등록/수집]           [잡 탐색 화면]                [지원 현황]
      │                              │                            │
      ▼                              ▼                            │
 discovered_jobs / jobs 풀  ──▶  검색·정렬해서 둘러보기            │
      │                              │                            │
      │                    "관리 보내기" 클릭 시                   │
      │                              └──▶ addJobToApplications ──▶ matches 테이블에 추가
      │                                                             (풀에서는 제외됨)

서버 액션 하나만 봐도 이 구조가 명확하다.

// src/app/discover/actions.ts
export async function addJobToApplications(jobId: string): Promise<{ error?: string }> {
  // ... 로그인/프로필 체크 후
  // jobs 테이블의 공고를 matches 테이블에 연결 → 지원 현황에서 보이게 됨
}

"보는 것"과 "관리하는 것"을 테이블 레벨에서부터 분리(공유 jobs/discovered_jobs 풀 vs 사용자별 matches)해두니, 이후에 추천 기업 프리셋(4편에서 다룰 예정)을 추가할 때도 자연스럽게 같은 구조에 얹을 수 있었다.

자주 쓴 패턴 요약

상황패턴
옛 URL 대응리다이렉트 라우트를 남기고 내부 링크는 전부 새 경로로 일괄 치환
큰 리팩토링"이동" → "분리" → "기능 추가" 순으로 커밋을 쪼갬
로그인 여부에 따른 화면 분기페이지 자체를 분기하지 말고, 같은 페이지 안에서 헤더 CTA만 조건부 렌더링
탐색 vs 관리 데이터공유 풀 테이블 + 사용자별 연결 테이블(match) 구조로 분리

정리

이번 편의 핵심은 세 가지다.

  1. URL은 나중에 고치려면 비용이 커진다. /matchda 같은 임시 경로가 오래 남아있으면, 리다이렉트·내부 링크·OAuth 콜백까지 다 손봐야 한다. 초반에 "루트 = 랜딩, 나머지는 역할별 경로"라는 원칙을 세워두는 게 편하다.
  2. 화면 하나가 너무 많은 역할을 하면 나눠야 할 타이밍이다. 대시보드가 통계+칸반+리스트+활동내역을 다 갖고 있었을 때, 사용자는 어디를 봐야 할지 헷갈렸다. 요약(대시보드)과 관리(지원 현황)를 나누니 각 화면의 목적이 분명해졌다.
  3. AI가 짠 첫 설계를 그대로 믿지 말고 실제로 써봐야 한다. 잡 탐색→지원현황 자동 편입은 이론적으로는 그럴듯했지만, 실사용 피드백 한 마디("관리 보내기 버튼이 있어야 한다")로 전체 플로우가 바뀌었다. AI 어시스턴트는 요구사항을 정확히 코드로 옮기는 데는 강하지만, "이 요구사항 자체가 맞는가"는 결국 사람이 써보고 판단해야 한다.

다음 편에서는 이번 세션에서 가장 아찔했던 사건 — AI의 테스트 정리 스크립트가 실제 사용자 계정을 삭제해버린 사고와 복구 과정을 다룬다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.