대학생 맞춤형 장학금 · 공모전 매칭 서비스 — FitMe 프론트엔드 레포지토리입니다.
흩어져 있는 장학금·공모전 정보를 한곳에 모으고, 사용자의 거주지역 · 소속대학 · 학점 · 소득구간 · 관심분야를 기반으로 맞춤 공고를 추천합니다. 관심 공고를 찜하고, 지원 이력을 상태별로 관리하며, 마감 임박 알림을 받아볼 수 있습니다.
🔗 배포 링크: https://fit-me-front-smoky.vercel.app
| 구분 | 사용 기술 | 선택 이유 |
|---|---|---|
| 코어 | React 19, TypeScript, Vite | 타입 안정성 확보 및 빠른 개발 서버·빌드 |
| 스타일링 | Tailwind CSS v4 | 디자인 시안 기반 UI를 빠르게 구성, 클래스 단위 일관성 유지 |
| 서버 상태 | TanStack Query | 캐싱·동기화·로딩/에러 상태를 표준화하여 관리 |
| 클라이언트 상태 | Zustand | 최소한의 보일러플레이트로 인증·모달·토스트 전역 상태 관리 |
| 네트워크 | axios | 인터셉터 기반 토큰 자동 주입 및 401 재발급 처리 |
| 라우팅 | react-router-dom | 인증 상태 기반 라우트 가드 구성 |
| 폼 | react-hook-form + Zod | 스키마 기반 유효성 검증과 실시간 에러 피드백 |
| 배포 | Vercel | main 브랜치 자동 배포 |
- 이메일 회원가입: 이름·생년월일·이메일 인증번호·비밀번호 검증(react-hook-form + Zod), 이용약관·개인정보처리방침 동의
- 소셜 로그인: 카카오 · 네이버 (백엔드 주도 OAuth, 콜백에서 토큰 수신 후 저장)
- 계정 연동: 이미 이메일로 가입된 주소로 소셜 로그인 시, 연동 확인 후 기존 계정과 통합
- 로그인 상태 유지: 새로고침 후에도 세션 유지
- 토큰 자동 재발급: Access Token 만료 시 Refresh Token으로 재발급 후 원래 요청 자동 재시도
- 거주지역(전국 시·군·구 검색), 소속대학(전국 대학·캠퍼스 검색), 학점, 소득구간, 관심분야를 단계별로 입력
- 입력 정보를 기반으로 맞춤 공고 추천에 활용
- 홈 피드: 실시간 인기 공고, 최근 조회 공고, 마감 임박 공고(장학금/공모전 탭)
- 공고 상세: 요약 정보, 접수·혜택·자격 탭, 원본 링크 이동, 지원 완료 처리
- 찜: 하트 토글 시 즉시 반영(낙관적 업데이트), 실패 시 이전 상태로 복구
- 탐색: 키워드 검색(입력 디바운스), 카테고리·정렬 필터, 무한 스크롤
- 지원 이력: 결과 대기 / 최종 합격 탭, 상태 변경, 메모 자동 저장
- 마이페이지: 프로필·활동 요약, 핏 프로필 수정, 프로필 이미지 업로드, 알림 설정, 공지사항, FAQ·1:1 문의, 회원 탈퇴
- 알림: 마감 임박 알림 목록 및 미읽음 배지
워크북에서 학습한 개념을 실제 서비스에 어떻게 적용했는지 정리했습니다.
| 주차 | 학습 개념 | 적용 위치 | 적용 내용 |
|---|---|---|---|
| 1~2주차 | 컴포넌트 기반 설계 · 제어 컴포넌트 · useState | shared/components/Button·Input·Chip·Dropdown |
하나의 컴포넌트를 variant·size props로 변형해 전 화면에서 재사용. 입력값은 모두 제어 컴포넌트로 관리 |
| 2~3주차 | 조건부 렌더링 · TypeScript props 타입 | BottomSheet, Input, Modal |
if (!isOpen) return null 형태의 early return, 에러 상태에 따른 조건부 스타일링, 모든 컴포넌트 props 인터페이스 정의 |
| 4주차 | react-hook-form + Zod (zodResolver, refine) |
pages/SignupPage.tsx, shared/utils/validation.ts |
스키마 기반 유효성 검증, refine으로 비밀번호 일치 여부 검증, mode: 'onChange'로 실시간 에러 표시 |
| 5주차 | react-router 라우팅 · Protected Route · axios 인터셉터 | routes/router.tsx, routes/ProtectedRoute.tsx, apis/axiosInstance.ts |
createBrowserRouter로 라우팅 구성, 인증 상태에 따른 3단계 라우트 가드, 요청 인터셉터로 토큰 자동 주입 |
| 6주차 | TanStack Query · 서버 상태 · 캐시 정책 · Mock→API 전환 | lib/queryClient.ts, apis/*.ts |
staleTime 1분 / gcTime 5분 정책 설정, 데이터 요청을 API 함수로 격리해 컴포넌트 수정 없이 Mock→실제 API 교체 |
| 6주차 | useIntersectionObserver 무한 스크롤 |
shared/hooks/useIntersectionObserver.ts, pages/ExplorePage.tsx |
목록 바닥 감지 후 useInfiniteQuery로 다음 페이지 요청. 요청 진행 중에는 옵저버를 비활성화해 중복 호출 방지 |
| 7주차 | useMutation · 낙관적 업데이트 · 롤백 |
shared/hooks/useToggleSave.ts |
찜 토글 시 서버 응답 전 UI를 먼저 갱신하고, 관련 캐시를 함께 동기화. 실패 시 저장해 둔 스냅샷으로 롤백 후 토스트 안내 |
| 8주차 | useDebounce 커스텀 훅 |
shared/hooks/useDebounce.ts, shared/components/SearchBar.tsx |
검색어 입력이 멈춘 뒤 요청이 나가도록 지연시켜 불필요한 API 호출 최소화 |
| 9주차 | Zustand 전역 상태 관리 | store/authStore.ts, modalStore.ts, toastStore.ts |
인증 정보·모달·토스트를 역할별 스토어로 분리. alert 대신 전역 토스트/모달로 피드백 일원화 |
| 항목 | 내용 |
|---|---|
| 401 자동 재발급 (single-flight) | 워크북의 axios 인터셉터 개념을 확장해, 동시에 여러 요청이 만료되어도 재발급은 한 번만 실행되도록 처리하고 무한 재시도를 차단했습니다. |
| 소셜 로그인 · 계정 연동 | 백엔드 주도 OAuth 흐름에 맞춰 리다이렉트 및 콜백 파싱을 구현하고, 이메일 중복 시 계정 연동 흐름을 추가했습니다. |
| 로그인 상태 유지 | Zustand persist를 적용해 새로고침 후에도 세션이 유지되도록 했습니다. |
서버에서 받아오는 데이터와 앱 자체의 UI 상태를 성격에 따라 분리해 관리합니다.
- 서버 상태 — TanStack Query:
staleTime1분,gcTime5분,retry1회로 캐시 정책을 통일해 불필요한 재요청을 줄였습니다. 조회는useQuery, 변경은useMutation, 무한 스크롤은useInfiniteQuery를 사용합니다. - 클라이언트 상태 — Zustand:
authStore(토큰·온보딩 여부·사용자 이름),modalStore,toastStore로 역할을 나눴습니다.
ProtectedRoute에서 인증 상태에 따라 접근을 제어합니다.
| 조건 | 처리 |
|---|---|
| 토큰 없음 | /login으로 리다이렉트 |
| 토큰 있음 + 온보딩 미완료 | /onboarding으로 리다이렉트 |
| 토큰 있음 + 온보딩 완료 | 요청 화면 렌더 |
- 요청: 저장된 Access Token을
Authorization: Bearer헤더에 자동 주입 - 응답: 401(또는 토큰 만료 코드) 감지 시 Refresh Token으로 재발급 후 원요청 재시도
- 여러 요청이 동시에 만료되어도 재발급은 한 번만 실행되도록 처리(single-flight)
- 재시도한 요청이 다시 실패할 경우를 대비해 플래그로 무한 루프 차단
- 재발급 실패 시 로그아웃 처리 후 로그인 화면으로 유도
데이터 요청을 apis/*.ts로 격리해, 백엔드 연동 시 컴포넌트 수정 없이 API 함수 내부만 교체하면 되도록 설계했습니다. 응답 공통 래핑 { isSuccess, code, message, result }에서 result만 반환합니다.
찜 토글은 서버 응답을 기다리지 않고 UI를 먼저 갱신하며, 관련된 여러 쿼리 캐시를 함께 동기화합니다. 요청이 실패하면 저장해 둔 이전 상태로 롤백하고 사용자에게 토스트로 알립니다.
src/
├ apis/ # 도메인별 API 함수 (auth, posting, history, mypage, explore ...)
│ └ axiosInstance.ts # baseURL · 토큰 주입 · 401 재발급 인터셉터
├ assets/ # 아이콘, 일러스트, 로고
├ constants/ # 지역/대학/약관 등 정적 데이터
├ features/ # 도메인 단위 화면
│ ├ history/
│ ├ mypage/
│ └ notification/
├ lib/ # queryClient 등 라이브러리 설정
├ pages/ # 라우트 단위 페이지
├ routes/ # router, ProtectedRoute
├ shared/
│ ├ components/ # 공통 UI 컴포넌트 (Button, Input, Modal, PostingCard ...)
│ ├ hooks/ # useDebounce, useIntersectionObserver, useToggleSave
│ ├ layouts/ # AuthLayout
│ └ utils/ # date, file, validation
├ store/ # authStore, modalStore, toastStore
└ types/ # 도메인별 타입 정의
| 구분 | 화면 | 경로 | 주요 기능 |
|---|---|---|---|
| 인증 | 소셜 로그인 | /login |
카카오·네이버·이메일 로그인 진입 |
| 인증 | 이메일 로그인 | /login/email |
이메일·비밀번호 로그인 |
| 인증 | 회원가입 | /signup |
유효성 검증, 이메일 인증, 약관 동의 |
| 인증 | 온보딩 | /onboarding |
거주지·대학·학점·소득·관심분야 입력 |
| 인증 | OAuth 콜백 | /oauth2/callback |
소셜 로그인 결과 처리 |
| 공고 | 홈 피드 | / |
인기·최근 조회·마감 임박 공고 |
| 공고 | 공고 상세 | /postings/:postingId |
상세 정보, 지원·찜 |
| 공고 | 최근 본 공고 | /recent-postings |
최근 조회 목록 |
| 탐색 | 탐색 | /explore |
검색·필터·정렬, 무한 스크롤 |
| 개인화 | 찜 목록 | /saved |
찜한 공고 관리 |
| 개인화 | 이력 목록 | /history |
지원 이력, 상태 변경 |
| 개인화 | 이력 상세 | /history/:id |
이력 상세, 메모 저장 |
| 개인화 | 알림 | /notifications |
마감 임박 알림 |
| 마이 | 마이페이지 | /my |
프로필·활동 요약 |
| 마이 | 프로필 수정 | /my/profile |
핏 프로필·이미지 수정 |
| 마이 | 알림 설정 | /my/notifications |
알림 수신 설정 |
| 마이 | 고객센터 | /my/support |
FAQ, 1:1 문의 |
| 마이 | 공지사항 | /my/notices |
공지 목록 |
graph TD
A[시작] --> B{로그인 여부}
B -- 미인증 --> C[로그인 / 회원가입]
B -- 인증됨 --> D{온보딩 여부}
C --> D
D -- 미완료 --> E[온보딩 단계]
E --> F[홈 피드]
D -- 완료됨 --> F
F --> G[탐색 / 검색]
F --> H[공고 상세]
F --> I[찜 보관함]
F --> J[마이페이지]
F --> K[이력]
G --> H
I --> H
K --> H
- Node.js 20 이상
- pnpm
pnpm install프로젝트 루트에 .env 파일을 생성합니다. (.env는 gitignore 대상이며, 배포 환경에는 Vercel 환경 변수로 별도 등록합니다.)
pnpm dev # http://localhost:5173pnpm build # 타입 체크 + 프로덕션 빌드 (PR 전 필수)
pnpm preview # 빌드 결과물 로컬 확인pnpm lint # ESLint 검사
pnpm format # Prettier 포맷팅 자동 적용소셜 로그인은 백엔드에 등록된 리다이렉트 URI가 배포 주소 기준이므로, 배포 환경에서 테스트해야 정상 동작합니다.
main: 프로덕션 배포 브랜치.develop에서만 병합하며, 병합 시 Vercel 자동 배포가 실행됩니다.develop: 개발 통합 브랜치. PR을 통해서만 병합 가능.feature/기능명: 기능 개발 브랜치. (develop에서 분기)- e.g:
feature/login,feature/posting-list
- e.g:
fix/...,hotfix/...: 버그 수정 및 긴급 수정 브랜치
커밋 메시지는 타입: 메시지 내용 형식을 준수합니다.
| 타입 | 설명 |
|---|---|
feat |
새로운 기능 추가 |
fix |
버그 수정 |
docs |
문서 수정 |
style |
코드 포맷팅, 세미콜론 누락 등 |
refactor |
프로덕션 코드 리팩토링 |
test |
테스트 코드 추가 및 수정 |
chore |
빌드 업무, 패키지 매니저 설정 등 |
feature/기능명브랜치에서 작업 완료 후develop브랜치로 PR을 보냅니다.- PR 전
pnpm build통과는 필수입니다. (타입 에러로 인한 배포 실패 방지) - 패키지가 추가/변경된 경우 PR 내용에 명시하고 팀에 공지합니다.
- 병합은 한 번에 하나씩 진행하고, 병합 후 팀 채널에 공유합니다.
| 이름 | 역할 | 담당 기능 |
|---|---|---|
| 서은호 | 앱 골격 · 인증 (파트장) | 로그인·회원가입·온보딩, 소셜 로그인 및 계정 연동, 라우터·라우트 가드, axios 인터셉터·토큰 재발급, 전역 상태 설계 |
| 김경섭 | 공고 발견 흐름 | 홈 피드, 공고 상세, 찜 목록·낙관적 업데이트, 최근 본 공고 |
| 정종욱 | 탐색 · 개인 기록 | 검색·필터·무한 스크롤, 지원 이력 관리, 마이페이지·프로필 설정, 알림·공지·고객센터 |