Fullstack 107

heo4·2026년 9월 16일

Fullstack

목록 보기
62/70

풀스택

CareMatch 프로젝트 회고

요양보호사·간병인·가사도우미와 요양시설을 연결하는 구인구직 매칭 웹 서비스를 만들며 남기는 정리 글.
기간: 2026-09-07 ~ (발표 2026-09-21)


1. 프로젝트 개요

CareMatch는 요양보호사·간병인·가사도우미(구직회원)와 요양시설(시설회원), 그리고 개인적으로 요양보호사를 찾는 소비자(보호자회원)를 연결하는 구인구직 매칭 플랫폼이다. 여기에 운영을 위한 관리자 계정까지 더해 4종의 회원 유형이 존재한다.

10일 남짓한 기간 동안 331개 커밋, 146건의 PR 머지가 쌓였다. 매일 새 기능이 붙고, 그만큼 버그도 그날그날 잡아나간 프로젝트였다.

2. 기술 스택

구분기술
프론트엔드React 19 · Vite · TypeScript · Tailwind CSS · React Router (Vercel 배포)
백엔드Spring Boot 3 · Spring Security(JWT/OAuth2) · Spring Data JPA (Render 배포, Docker)
DBPostgreSQL(Neon) — 로컬은 H2 인메모리
마이그레이션Flyway (prod만 자동 적용, local/test는 H2 create-drop)
파일 스토리지Cloudflare R2 (S3 호환, presigned URL)
결제포트원(PortOne) V2 — 서버가 결제 결과를 재조회해 검증
지도카카오맵 SDK — 반경 검색 + 클러스터링

배포 스택은 처음에 "여러 서비스를 써봤다"는 방향으로 흩어놓기보다, Cloudflare R2를 실제로 구현하는 깊이를 포트폴리오 카드로 삼기로 팀 안에서 정리했다. 그 결과 프론트는 Vercel, 백엔드는 Render, DB는 Neon으로 각자 역할이 뚜렷한 스택이 됐다(파일 스토리지만 Cloudflare R2로 실제 presigned URL 흐름까지 구현).

3. 팀 구성과 역할

이름역할GitHub
혁 (팀장)백엔드@heo-hyuk
신영프론트엔드@syyu21b
경수백엔드@HurKyungsoo
동한프론트엔드@Kim-dong-han

역할별로 브랜치 접두사(feature/be-*, feature/fe-*)를 나누고, main은 직접 push를 막아 PR로만 병합하는 GitHub Flow를 초반부터 고정했다. 배포 시점은 별도 브랜치 대신 main에 태그(vX.Y.Z)로만 남기기로 했다.

4. 주요 기능

  • 회원: 구직/시설/보호자(GENERAL) 3종 + 관리자, 아이디·비밀번호 로그인(JWT), 카카오 소셜 로그인(백엔드는 네이버·구글도 지원하는 3사 공용 구조지만 프론트는 카카오만 노출), 로그인 실패 잠금, 비밀번호 찾기, 회원 탈퇴
  • 구인공고: 등록·수정·마감, 추천순/최신순/매칭점수순 정렬, 내 주변 일자리(반경 검색 + 지도 클러스터링), 임시저장, 스크랩
  • 인재정보: 구직자 프로필, 인재 검색, 공고↔인재 매칭도 계산
  • 인증구직자 마크: 자격증·경력인증 각 1건 이상 보유 시 신청 가능, 관리자 승인(또는 수동 부여/해제)으로 부여
  • 구직신청: 온라인 지원/취소/수락·반려
  • 포인트/결제: 포트원 V2 실 결제 연동, 서버 측 결제 재검증으로 클라이언트 위변조 방지
  • 알림: 지원 결과·시설 승인·문의 답변 시 생성, 헤더 배지 폴링(30초)
  • 관리자 대시보드: 회원 검색, 포인트 내역, 시설 승인/반려, 1:1 문의 답변, 공지 CRUD
  • 접근성: 쉬운 화면 모드 / 글자 크기(기기+서버 동기화)

5. 개발 흐름

초기 며칠은 모노레포 구조 전환, CODEOWNERS·CI 세팅, 회원/인증 도메인처럼 다른 모든 기능이 딛고 설 기반을 다지는 데 썼다. 이후로는 구인공고 → 인재정보/구직신청 → 포인트·결제 → 인증구직자 마크 → 알림/관리자 순으로 도메인이 하나씩 늘었고, 마지막 며칠은 매칭점수 정렬, 모바일 반응형, 회원가입 중복 알림 같은 다듬기 작업과 README 갱신이 이어졌다.

PR은 총 146건, 거의 매 기능 단위로 쪼개 올렸고 리뷰 승인 후 머지하는 흐름을 지켰다. wip: 커밋으로 미완성 작업을 중간 저장했다가, main으로 올라갈 때는 Squash and merge로 압축해 히스토리를 깔끔하게 유지했다.

6. 트러블슈팅 — 겪고 넘은 것들

기록해둘 만한 이슈만 추리면 이렇다.

  1. JDK 버전 & Gradle wrapper 누락 — wrapper 미커밋으로 clone 직후 빌드 불가. build.gradle의 toolchain 고정을 걷어내고 options.release = 17로 전환, wrapper 커밋 + .gitattributes로 개행 문자 고정.
  2. SecurityConfig 순환참조 — SecurityConfig → OAuth2SuccessHandler → AuthService → PasswordEncoder(SecurityConfig 내부 빈)로 자기 자신을 참조. PasswordConfig로 분리해 해결.
  3. CORS 차단 — Vercel 프리뷰 서브도메인이 매번 바뀌어 고정 origin 목록이 무용지물. setAllowedOriginPatterns로 전환.
  4. 모노레포 전환 경로 조정 — Render Root Directory를 backend로, IntelliJ도 backend/만 Gradle 프로젝트로 재임포트.
  5. Render 포트 바인딩 실패 — $PORT 동적 주입을 못 받아 배포 실패. server.port: ${PORT:8080} 추가.
  6. 스키마 검증 실패 & @Lob→oid 문제 — 운영 DB가 비어 있는데 ddl-auto: validate였던 문제, 그리고 @Lob String이 Hibernate 6+PostgreSQL에서 oid 컬럼으로 생성되던 문제(H2에선 안 드러나 로컬 테스트로는 못 잡음). @Column(columnDefinition = "TEXT")로 전환하고 Flyway를 도입해 이후 스키마 변경을 마이그레이션 파일 추가만으로 처리하게 정리.
  7. 병합 충돌 마커가 그대로 커밋된 사고 (2회) — 자동 검사가 없던 시절, 사람이 화면을 직접 열어보기 전엔 main이 깨진 걸 몰랐던 사고. GitHub Actions CI(ci-frontend.yml, ci-backend.yml)를 PR 트리거로 붙이고서야 재발이 멈췄다.
  8. 카카오 로그인 Redirect URI 등록 위치 — 콘솔 UI 개편으로 로그인용 Redirect URI가 로그아웃 항목이 아니라 "REST API 키 카드" 안으로 이동해 있었음. account_email 동의항목은 카카오 심사(수일)가 필요해, 승인 전까지는 이메일 없이도 로그인이 되도록 임시 이메일 채움 처리로 우회.

7. 협업 규칙에서 실제로 도움이 됐던 것

  • CI 도입 시점: 이슈 #8/#10처럼 사람이 놓친 걸 CI가 대신 잡아준 이후로는 같은 유형의 사고가 재발하지 않았다. 규칙은 사고가 난 다음에 강화됐다는 점도 그대로 남겨둘 만하다.
  • PR 단위를 작게 쪼갠 것: 기능 하나, 버그 하나 단위로 PR을 올리다 보니 리뷰 부담이 크지 않았고, 머지 후 문제가 생겨도 원인 커밋을 찾기 쉬웠다.
  • wip 커밋 → squash 규칙: 작업 중간에 부담 없이 push해둘 수 있으면서도, main 히스토리는 기능 단위로 깔끔하게 남았다.

8. 남은 과제 / 향후 계획

  • 발표(2026-09-21)까지는 계속 활성 개발 중 — 매칭점수 정렬, 인증구직자 마크 관리자 토글 등도 발표 직전까지 반영됐던 것처럼, 마지막까지 다듬기 작업이 이어질 예정

0개의 댓글