프로젝트 개요
1. 프로젝트명
- 무빙 (Moving)
- 개발 기간: 2025.07.01 ~ 2025.08.14
- 개발 인원: 6명
2. 프로젝트 목적
기존의 번거로운 이사 준비 과정을 혁신하고 사용자에게 합리적인 비용 선택권을 제공하는 것을 목표로 한다. 고객이 여러 이사 업체에 개별적으로 연락하며 발생하는 정보 비대칭과 가격 비교의 어려움을 해결하고자, 검증된 이사 업체들이 고객의 요청에 경쟁적으로 견적을 제시하는 역경매 방식의 플랫폼을 구현했다. 이를 통해 투명하고 공정한 이사 시장을 조성하고 사용자의 편의성과 경제적 효율성을 극대화한다.
3. 핵심 기능
- 사용자/기사님별 인증 시스템
일반 사용자와 이사 기사님으로 역할을 분리하여, 각 타입에 맞는 회원가입, 로그인 및 프로필 관리 기능을 제공한다.
- 경쟁 견적 요청 및 비교
사용자가 한 번의 이사 정보 입력으로 여러 기사님으로부터 견적을 받고, 이를 한눈에 비교하여 합리적인 업체를 선택할 수 있다.
- 기사님 찾기 및 지정 요청
사용자는 지역, 서비스, 평점 등 다양한 조건으로 기사님을 검색하고, 마음에 드는 기사님을 '찜'하거나 직접 지정하여 견적을 요청할 수 있다.
- 실시간 알림 및 리뷰 시스템
새로운 견적 도착, 이사 확정 등 주요 이벤트에 대한 실시간 알림을 제공하며, 이사 완료 후에는 리뷰를 작성하여 다른 사용자에게 정보를 공유하고 기사님을 평가할 수 있다.
4. 주요 기술 스택
- Frontend
- Next.js
- React Query
- TypeScript
- Backend
- Database
- Deployment & Infra
담당한 작업 & 기술적 성과
1. 프로젝트에서 본인의 주요 역할
- 프론트엔드 개발 (Next.js, React, TypeScript)
- 백엔드 API 개발 (Express, Prisma, PostgreSQL)
- 발표자료 제작 및 발표
- 성능 최적화 및 접근성 구현
2. 개발한 핵심 기능
- 기사님 찾기 페이지의 검색/필터링 및 무한 스크롤 기능
- 기사님 상세 페이지 및 찜한 기사님 조회 기능
- 랜딩 페이지 및 반응형 이미지 최적화
3. 구체적인 작업 내용
Frontend
-
기사님 찾기 페이지
- 검색/필터/정렬 시스템
- 실시간 디바운싱 검색:
setTimeout
으로 100ms 지연 후 검색 실행, 검색어 초기화 버튼 지원
- 드롭다운 필터:
useRegions
, useServiceTypes
(React Query)로 옵션 데이터 로딩, CustomDropdown
컴포넌트 사용
- 정렬:
useSearchMoverStore
(Zustand)로 "리뷰/평점/경력/완료건수" 기준 상태 관리
- 무한 스크롤 목록
- 구현:
react-intersection-observer
의 useInView
훅과 useInfiniteQuery
사용
- 페이지네이션: 페이지당 4개 아이템 표시,
fetchNextPage
로 다음 페이지 자동 로딩
- "맨 위로" 버튼:
window.scrollY > 500
조건으로 하단에 표시, scrollTo
로 부드러운 스크롤
- UI/UX:
MovingTruckLoader
스켈레톤, 에러 상태 처리, 검색 결과 없음 메시지
-
기사님 카드 정보
- 표시 정보:
MoverCard
컴포넌트로 프로필 이미지, 닉네임, 한줄 소개, 평점/리뷰수/경력/완료건수 표시
- 전문가 배지:
completedCount > 30
조건으로 "전문 이사 기사님" 배지 표시
- 레이아웃:
useWindowWidth
훅으로 모바일/데스크톱 반응형, 찜하기 버튼 포함
-
찜하기/공유 기능
- 찜하기:
useAddFavorite
(React Query)로 로그인 필요, 캐시 자동 업데이트
- PC 미리보기:
FavoriteMoverList
컴포넌트로 우측 사이드바에 찜한 기사님 최대 3명 미리보기
- 공유 메시지: 기사님 정보와 추천 텍스트가 포함된 공유 메시지 자동 생성
- 위치: 모바일/데스크톱 공유 기능 차별화된 위치 배치
- 기사님 상세 페이지
- 기사님 소개
- 프로필 정보:
MoverIntro
컴포넌트로 프로필, 경력, 평점, 찜 수 표시
- 상세 소개: "더보기/접기" 토글 기능으로 긴 소개 텍스트 처리
- 전문가 배지:
completedCount > 30
조건으로 "전문 이사 기사님" 배지 표시
- 서비스 및 지역 정보
- 서비스 타입:
Chip
컴포넌트로 제공 서비스 타입을 태그 형태로 표시
- 서비스 지역:
getRegionTranslation
유틸리티로 지역명 다국어 처리
- 정보 칩:
CircleTextLabel
컴포넌트로 시각적 구분
- 리뷰 및 평점
- 평점 요약: 평균 평점과 총 리뷰 수를 통계 형태로 표시
- 리뷰 목록: 고객 리뷰를 시간순으로 정렬하여 표시
- 별점 표시:
react-simple-star-rating
컴포넌트로 시각적 평점 표현
- 액션 버튼
- 지정 견적: 특정 기사님에게 지정 견적 요청 버튼
- 찜하기: 로그인 상태에 따른 모달 표시
- 소셜 공유
- 공유 옵션: 카카오톡, 페이스북, 링크 복사 공유
- 공유 메시지: 기사님 정보와 추천 텍스트가 포함된 메시지 자동 생성
- 위치: 모바일/데스크톱 환경에 따른 차별화된 배치
- 랜딩 페이지
- 페이지 구조
- 메인 헤더: 트럭 이미지와 메인 타이틀, 서브타이틀로 구성
- 서비스 안내: 4단계 이사 서비스 이용 과정을 인포그래픽으로 설명
- 견적 요청 배너: 간편하고 빠른 견적 요청 서비스 안내
- 정보 안내: 무빙 서비스에 대한 추가 정보 제공
- 반응형 이미지 처리
- Next.js Image: 최적화된 이미지 로딩과 성능 향상
- Picture 태그:
source
와 media
속성으로 디바이스별 이미지 최적화
- 우선순위:
priority
속성으로 LCP 이미지 우선 로딩
- 품질 최적화:
quality={75}
로 이미지 품질과 용량 균형
- CSS 애니메이션
- 트럭 슬라이드인:
.truck-slide-in
클래스로 트럭 이미지 등장 효과
- 느린 펄스:
.slow-pulse
클래스로 부드러운 펄스 애니메이션
- 공통
- 접근성
- 스킵 링크: 각 페이지별 주요 섹션으로의 빠른 이동
- ARIA 라벨:
aria-label
, aria-describedby
등 접근성 속성
- 시맨틱 HTML:
main
, section
, header
등 의미있는 태그 구조
- 성능 최적화
- 깜빡임 방지:
useMemo
로 쿼리 파라미터 메모이제이션과 placeholderData
로 구현
- 에러 추적:
Sentry
를 사용한 에러 추적 및 컨텍스트 설정
- 렌더링:
useWindowWidth
훅으로 디바이스 타입에 따른 조건부 렌더링 구현
Backend
-
기사님 정보 조회 API
- 라우트:
mover.routes.ts
에서 GET /api/movers
(목록), GET /api/movers/:id
(상세), GET /api/movers/favorite
(찜한 목록)
- 컨트롤러:
mover.controller.ts
에서 optionalAuth
미들웨어로 로그인/비로그인 사용자 모두 지원
- 서비스:
mover.service.ts
에서 페이지네이션, 필터링, 정렬 로직 처리
- 레포지토리:
mover.repository.ts
에서 Prisma를 사용한 데이터베이스 쿼리 최적화
-
검색 및 필터링
- 검색: 닉네임과 지역 기반 텍스트 검색, Prisma의
OR
조건으로 구현
- 필터링: 지역별, 서비스 타입별 필터링, 데이터베이스 인덱스 활용
- 정렬: 리뷰 수, 평점, 경력, 완료 건수 기준 정렬,
ORDER BY
절로 구현
-
다국어 적용
- 미들웨어:
translationMiddleware.ts
로 ?lang=ko|en|zh
쿼리 파라미터 지원
- 번역: 지역명, 서비스 타입명 등 동적 번역 처리
- 에러 처리: 번역 실패 시 원본 데이터 응답 + Sentry 에러 기록
-
에러 처리 및 모니터링
- Sentry: 각 API 단계에서 에러 추적 및 컨텍스트 설정
- 검증: 입력 데이터 검증, 사용자 권한 확인, 비즈니스 규칙 검증
- 로깅: 액션 로그 생성, 사용자 활동 추적
-
유닛테스트
- 필터링, 정렬, 페이지네이션
- 찜하기/해제, 상세정보, 리뷰 목록
- 권한, 파라미터, NULL 값 처리
트러블 슈팅
1. 상태 관리 최적화 (Zustand)
-
문제
- 기사님 찾기 페이지에서 지역 및 서비스 필터, 정렬, 검색어 등 다양한 필터/정렬/검색어 상태를 여러 컴포넌트(검색바, 필터바, 정렬바 등)에서 공유하고 변경해야 한다.
- 각 컴포넌트가 서로의 상태에 의존하거나, props로 상태를 계속 전달해야 해서 코드가 점점 복잡해지고, 관리가 어려워졌다.
-
원인 분석
- useState, useContext, props drilling 등으로 상태를 관리하면, 컴포넌트가 많아질수록 상태 전달이 복잡해지고, 유지보수/확장 시 버그가 발생하기 쉽다.
-
해결
Zustand
를 활용하여 모든 컴포넌트가 전역 store에서 직접 상태를 읽고, 변경할 수 있게 하였다.
- props 전달 없이도 상태를 공유할 수 있어 코드가 훨씬 간결해지고, 컴포넌트 간 결합도가 낮아져 유지보수와 확장성이 크게 향상되었다. (상태관리에 대한 코드 75%절감)
const [region, setRegion] = useState("");
const [serviceType, setServiceType] = useState("");
const [search, setSearch] = useState("");
const { region, serviceType, search, setRegion, setServiceType, setSearch } = useSearchMoverStore();
2. 무한 스크롤 구현
-
문제
- 초기에는 window.addEventListener('scroll')을 사용하여 스크롤 위치를 감지했지만, 스크롤할 때마다 이벤트가 발생하여 성능이 크게 저하되었다.
- 기사님 찾기 페이지에서 무한 스크롤 구현 시 스크롤 이벤트가 너무 자주 발생하여 성능 저하 및 메모리 누수 위험이 발생하였다.
-
원인 분석
- 스크롤 이벤트는 사용자가 스크롤할 때 매우 빈번하게 발생하여 불필요한 연산을 반복한다.
- useEffect와 useState만으로는 스크롤 성능을 최적화하기 어렵고, 메모리 누수 방지도 어렵다.
-
해결
- react-intersection-observer의 useInView 훅을 활용하여 스크롤 이벤트 대신 Intersection Observer를 사용하였다.
- rootMargin: "100px" 설정으로 스크롤이 끝에 도달하기 전에 미리 다음 페이지를 로딩하여 부드러운 사용자 경험을 제공하였다.
- React Query의 useInfiniteQuery와 조합하여 메모리 효율적인 데이터 관리가 가능해졌다.
- 결과적으로 스크롤 성능이 크게 향상되고, 메모리 사용량도 최적화되었다.
const { ref, inView } = useInView({ threshold: 0, rootMargin: "100px" });
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({...});
3. React Query 도입으로 데이터 페칭 최적화
-
문제
- 기사님 찾기 페이지에서 각 컴포넌트마다 useState와 useEffect를 사용하여 개별적으로 API를 호출하고 상태를 관리하였다.
- 로딩, 에러, 데이터 상태를 각각 관리해야 하여 코드가 복잡해지고, 컴포넌트 간 상태 동기화가 어려웠다.
- 캐시가 없어서 같은 데이터를 반복적으로 요청하거나, 컴포넌트가 언마운트된 후에도 API 응답을 처리하려고 시도하는 문제가 발생하였다.
-
원인 분석
- 수동으로 상태를 관리하면 로딩, 에러, 데이터 상태를 각각 useState로 관리해야 하여 코드가 길어지고 복잡해진다.
- useEffect의 cleanup 함수를 제대로 구현하지 않으면 메모리 누수나 경쟁 상태(race condition)가 발생할 수 있다.
- 컴포넌트가 언마운트된 후에도 API 응답을 처리하려고 시도하여 React 경고가 발생하고, 불필요한 상태 업데이트가 일어난다.
-
해결
- @tanstack/react-query를 도입하여 데이터 페칭을 중앙화하고 useMoverData.ts 훅으로 통합 관리하였다.
- useMoverList, useMoverDetail, useFavoriteMovers 등 각 API 호출을 위한 전용 훅을 구현하여 코드 재사용성을 높였다.
- React Query의 자동 캐싱, 백그라운드 업데이트, 에러 재시도 등의 기능을 활용하여 사용자 경험을 크게 개선하였다.
- 결과적으로 데이터 페칭 관련 코드가 60% 단축되고, 캐싱으로 인한 불필요한 API 호출이 크게 감소하였다.
- 컴포넌트 간 데이터 동기화가 자동으로 이루어져 상태 관리 복잡성이 크게 줄어들었다.
const [movers, setMovers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => { }, []);
const { data: movers, isLoading, error } = useMoverList(params);
4. 검색 디바운싱 구현 문제
- 문제
- 기사님 검색 기능에서 사용자가 타이핑할 때마다 API 호출이 발생하여 서버 부하가 증가하고, 불필요한 네트워크 요청이 반복되었다.
- 초기에는 onChange 이벤트에서 바로 검색 API를 호출하여 사용자 경험이 좋지 않았다.
- 원인 분석
- 사용자가 타이핑할 때마다 즉시 API를 호출하면 서버 리소스를 과도하게 사용하고, 네트워크 트래픽이 증가한다.
- useState만으로는 타이핑 중간에 발생하는 불필요한 API 호출을 제어하기 어렵다.
- 해결
- setTimeout과 useRef를 활용하여 100ms 디바운싱을 구현하였다.
- 사용자가 타이핑을 멈춘 후 100ms가 지나야 실제 검색이 실행되어 불필요한 API 호출을 방지하였다.
- 컴포넌트 언마운트 시 useEffect의 cleanup 함수에서 타이머를 정리하여 메모리 누수를 방지하였다.
- 결과적으로 API 호출 횟수가 크게 감소하고 서버 부하가 줄어들었다.
협업 및 피드백
- 팀원들과의 협업 과정
- GitHub 프로젝트 관리: PR뿐만이 아니라 코드 리뷰, 이슈, 프로젝트 등 깃허브의 여러 기능들을 이용하면서 일정 및 작업 관리를 한 눈에 파악할 수 있어서 효율적인 작업이었다.
- 느낀 점
- 중복 코드 최소화: 중복 코드를 줄이기 위해 프로젝트 초반에 더 많은 이야기를 나눴으면 좋았을 것 같다는 생각을 했다. 앞으로의 프로젝트에 있어 공통적인 로직에 대한 코드의 중복성을 최소화하여 작업해보고싶다.
- 개발 과정의 성장: 7개월이 언제 지나갈까하면서 묵묵히 개발만 해오다보니 벌써 끝이 나고 결과적으로는 개발 실력 향상과 공부 방식을 개척할 수 있었던 시간이었다.
- 배운 점
- 협업 도구 활용: GitHub의 다양한 기능을 활용한 프로젝트 관리 방법을 배웠고, 이를 통해 팀원들과의 소통이 더욱 원활해졌다.
- 기술 스택 선택의 중요성: Zustand 적용한 작업에 대해 기술 스택 선택의 중요성을 가장 크게 느꼈다. 각 기술의 장단점을 이해하고 프로젝트 요구사항에 맞게 선택하는 것이 중요하다는 것을 배웠다.
- 피드백
- 검색 UX 개선: 기사님 찾기 검색이 실시간 반영이 저 좋을 것 같다는 피드백을 받아 수정했다. 이를 통해 setTimeout과 useRef를 활용한 100ms 디바운싱 구현의 필요성을 더욱 깊이 이해할 수 있었다.
- 찜한 기사님 UI 개선: 스크롤을 해도 동적으로 움직일 수 있도록 약간 따라오는 느낌을 주면 좋을 것 같다는 피드백을 받아 position: fixed와 z-index를 활용한 고정 위치 UI를 구현했다. 이를 통해 사용자가 어느 위치에서든 찜한 기사님 목록에 쉽게 접근할 수 있게 되었다.
- 맨 위로 올라가기 버튼: 스크롤해서 매번 위로 올라가는 번거로움 해소하기 위해서 맨 위로 올라가기 버튼 추가하면 좋을 것 같다는 피드백을 받아 window.scrollY > 500 조건으로 버튼 표시/숨김을 제어하고, window.scrollTo({ top: 0, behavior: 'smooth' })를 활용하여 부드러운 스크롤 애니메이션을 구현했다.
코드 품질 및 최적화
랜딩페이지
1. 이미지 최적화
- Next.js Image 컴포넌트:
priority
속성으로 LCP(Largest Contentful Paint) 이미지 우선 로딩
- 반응형 이미지:
picture
태그와 source
속성으로 디바이스별 최적화된 이미지 제공
media="(min-width: 1024px)"
→ 데스크톱용 고해상도 이미지
media="(min-width: 768px)"
→ 태블릿용 중간 해상도 이미지
- 기본값 → 모바일용 저해상도 이미지
2. 성능 최적화
- 이미지 품질 조정:
quality={75}
로 이미지 품질과 용량의 균형점 설정
- 지연 로딩: 중요하지 않은 이미지에
loading="lazy"
적용
- 이미지 크기 최적화:
sizes
속성으로 반응형 이미지 크기 조정
- 메인 배경:
sizes="100vw"
(전체 뷰포트 너비)
- 서비스 안내:
sizes="(max-width: 768px) 400px, (max-width: 1024px) 677px, 693px"
3. CSS 애니메이션 최적화
- 트럭 슬라이드인:
@keyframes truck-slide-in
으로 1.2초 동안 부드러운 등장 효과
- 느린 펄스:
@keyframes slow-pulse
로 2.2초 주기의 미묘한 확대/축소 효과
- 애니메이션 성능:
cubic-bezier(0.4, 0, 0.2, 1)
이징 함수로 자연스러운 움직임
4. 접근성 및 SEO 최적화
- 시맨틱 HTML:
main
, header
, article
, section
, aside
등 의미있는 태그 구조
- ARIA 라벨:
aria-labelledby
, role
, aria-describedby
등 접근성 속성
- 이미지 설명:
alt
속성과 figcaption
으로 이미지 내용 명확화


향후 개선 사항 및 제안
기사님 찾기
- 고급 검색 필터: 현재 지역/서비스 타입 기반 필터에서 평점 범위, 경력 연차, 완료 건수 범위, 예산 범위 등 세분화된 필터 추가
- 검색 히스토리: 사용자별 검색 기록 저장 및 빠른 재검색 기능, 인기 검색어 트렌드 표시
- 맞춤 추천: 사용자 행동 패턴 분석을 통한 개인화된 기사님 추천 시스템
- 지도 기반 검색: Google Map이나 Kakao Map API 연동으로 지도상에서 기사님 위치 및 서비스 지역 시각화
- 검색 결과 캐싱: 인기 검색어 결과 Redis 캐싱, 검색 성능 향상
기사님 상세
- 실시간 상태: 기사님 현재 작업 상태, 예약 가능 시간 실시간 표시
- 비교 기능: 여러 기사님을 나란히 비교할 수 있는 비교 테이블
- 예약 시스템: 직접 예약 및 일정 확인 기능
- 리뷰 신뢰성: 리뷰 작성자 인증, 사진 첨부 리뷰 가중치 부여
- 평점 정확성: 이상치 제거, 가중 평점 시스템 도입
랜딩페이지
- A/B 테스트: 다양한 레이아웃 및 콘텐츠 조합 테스트