POSCOxCODINGON 웹 풀스택 13기
[ 코딩온 ] KDT 웹 풀스택 13기 / 3차 React 개인 프로젝트 회고
write3027
2024. 9. 23. 00:17
"백일몽"
온라인 숙박 공유 플랫폼
서버 주소 : http://43.203.247.141:3000/
사용 기술 : React, Next.js, Typescript
개발 기간 : 2024.09.02.월 ~ 2024.09.12.목
참여 인원 : 1인. 개인 프로젝트
주요 기능 :
1. 검색 : 여행지, 날짜, 인원 선택
2. 필터링 : 해시태그 기반 검색
3. 지도 : React google maps api. 위치 표시 및 길찾기
이번 React 개인 프로젝트에서는 사용자 중심의 UI/UX 구현을 목표로 하였습니다.
또한, 프로젝트를 통해 프론트엔드의 다양한 기술을 학습하고, 실제 서비스에 가까운 환경을 구축함으로써 웹 개발의 실질적인 기술력을 향상하는 데 목적을 두었습니다.
페이지 설명
1. 메인페이지 : 헤더
로고 : 로고클릭하면 전체 페이지를 다시 로드하여 초기화된 사용자 경험을 제공하기위해 라우팅이 아닌 리디렉션 처리를 했다.
/*로고 : 리디렉션*/ <div className="logoBox" onClick={() => {window.location.href = "/";}}> ... </div>
여행지 검색 팝업창 : 사용자가 검색어를 입력하지 않고도 선택할 수 있는 옵션을 제공해 검색 과정을 편리하게 만든다.
//팝업창 const [isPopupOpen, setIsPopupOpen] = useState(false); //Input 클릭 시 팝업 열기 const handleInputClick = () => { setIsPopupOpen(true); setAutoComplete(placeData); }; ... return ( ... {/* 자동 완성 창 */} <div className="placeRefBox"> {isPopupOpen && ( <ul> {autoComplete.map((item: any, index: number) => ( <li key={index} onClick={() => { setPlace(item); setIsPopupOpen(false); }} > {item} </li> ))} </ul> )} </div> );
여행지 검색 팝업 초성 자동완성const getChosung = (str: string) => { const cho = [ "ㄱ", "ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ", ]; let result = ""; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i) - 44032; if (code > -1 && code < 11172) { result += cho[Math.floor(code / 588)]; } else { result += str.charAt(i); } } return result; }; const includesChosung = (target: string, search: string) => { return getChosung(target).includes(search); }; //onChange시 실행하는 findPlace함수 const findPlace = (e: ChangeEvent<HTMLInputElement>) => { const newPlace = e.target.value; setPlace(newPlace); if (newPlace.length > 0) { const newData = placeData.filter( (x) => x.includes(newPlace) || includesChosung(x, newPlace) ); setAutoComplete(newData); } else { setAutoComplete(placeData); } };
여행지 검색 팝업창 close.event.target as Node : 이벤트가 발생한 클릭 대상 요소를 가리킨다. 여기서 클릭한 요소가 inputRef.current에 포함되지 않았는지 여부 확인
const inputRef = useRef<HTMLDivElement>(null); // Input과 팝업을 감싸는 ref useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( inputRef.current &&b!inputRef.current.contains(event.target as Node) ) {setIsPopupOpen(false);} }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); ... return ( <PlaceStyledComponent> <div ref={inputRef}> 여행지 ...
날짜 검색Antd “RangePicker” disabledDate속성 : 오늘 이전의 날짜 모두 비활성화
diffDays : 선택한 날짜 일수 계산// 날짜 차이를 계산하여 선택된 일 수를 저장 const diffDays = dates[1].diff(dates[0], "day"); // dayjs의 diff 함수를 사용해 일수 계산 setSelectedDays(diffDays); <RangePicker className="selectDate" ref={rangePickerRef} // RangePicker의 ref 추가 onChange={onRangeChange} // 날짜 변경 시 호출 value={dates} disabledDate={(current) => current.isBefore(moment().subtract(1, "day")) } />
인원 검색 : 팝업에서 선택한 값 useState에 저장. 성인 최소 1인 이상.//people 컴포넌트. const [peopleNum, setPeopleNum] = useState(1); const [adultCount, setAdultCount] = useState(1); const [childCount, setChildCount] = useState(0); const [infantCount, setInfantCount] = useState(0); const [petCount, setPetCount] = useState(0); // 인원 조절 핸들러 const decrementCount = (type: string) => { if (type === "adult" && adultCount > 1) { setAdultCount(adultCount - 1); setPeopleNum(peopleNum - 1); } else if (type === "child" && childCount > 0) { setChildCount(childCount - 1); setPeopleNum(peopleNum - 1); } else if (type === "infant" && infantCount > 0) setInfantCount(infantCount - 1); else if (type === "pet" && petCount > 0) setPetCount(petCount - 1); };
1. 메인페이지 : 필터링
allData 배열을 필터링. hash 배열 안에 tag가 title 변수와 일치하는 항목이 있는 경우만 남김.
const [clickFilter, setClickFilter] = useState("최고의 전망"); const [newFilterData, setNewFilterData] = useState<any>(initialFilterData); const clickFilterItem = (title: string) => { setClickFilter(title); const newData = allData.filter((item) => item.hash.some((hashItem) => hashItem.tag === title) ); setNewFilterData(newData); };
이미지 : react Swiper 사용.{/* 이미지 swiper로 보여주기 */} <Swiper onSwiper={(swiper) => { swiperRef.current = swiper; }} onSlideChange={(swiper) => { setIsBeginning(swiper.isBeginning); // 첫 슬라이드 여부 업데이트 setIsEnd(swiper.isEnd); // 마지막 슬라이드 여부 업데이트 }} modules={[Navigation, Pagination, Scrollbar, A11y]} // 필요한 모듈 추가 spaceBetween={50} // 슬라이드 간의 간격 slidesPerView={1} // 한 번에 보여줄 슬라이드 수 pagination={{ clickable: true, // 페이지네이션 점을 클릭할 수 있음 dynamicBullets: true, // 동적 페이지네이션 }} scrollbar={{ draggable: true }} // 스크롤바 추가 navigation > {data.src.map((image: any, index: number) => ( <SwiperSlide key={index}> <img src={image.src} alt={`image${index + 1}`} /> </SwiperSlide> ))} </Swiper>
제목 : 한줄만 나오게. 나머지는 ...//css .contentBox { .title { white-space: nowrap; /* 텍스트를 한 줄로 */ overflow: hidden; /* 넘치는 텍스트를 숨김 */ text-overflow: ellipsis; /* 넘치는 텍스트에 '...' 표시 */ } }
가격 : 세자리수마다 컴마표시. toLocaleString()
2. 검색페이지
- 헤더에서 검색결과 라우팅.
- query로 받아온 정보로 filter.
- 지도 : react-google-maps/api사용.
MarkerF :검색 지역 지도에서 마커
item hover : 우측 이미지 컴포넌트에서 지도 컴포넌트로 props로 hover상태 전달. MarkerF 속성 변경
position: sticky 를 사용해 스크롤 해도 map위치 고정
3. 상세페이지
- Antd “Image” : click해서 사진 크게 띄우도록
- 날짜 : 숙박 일수로 가격 계산하는 함수 만듦
- 인원수 : 여행자 수 선택 최대인원보다 적게 제한
- 숙소 설명 : 더보기, 간략히보기. “스크롤 함수” -> 부드럽게 열고 닫히도록
- 가격표시 position:sticky로 고정.
- 마커 클릭시 구글맵 길찾기 검색
MarkerF onClick : findRoad함수. lat, lng url에 넣는다. &hl=ko 한국어버전
개발중 페이지
로그인, 예약하기
: 버튼이 동작하지 않는 상황에서 사용자가 혼란을 겪지 않도록 antd “Modal” 띄워줌.
반응형 Header
@media(max-width:768px)
Input창에 숨겼다가 위에서 아래로 내려오도록. 팝업창
translateY, transition 사용.
반응형 Main Page
Swiper 화살표 삭제
-> 모바일에서는 터치로 밀어서 Swiper 사용하므로
반응형 Search Page
모바일 hover 동작 안하므로 지도 고정X
반응형 Detail Page
각 사진 디폴트로 크게 볼 수 있도록 Swiper로 변경.
더보기 : 화면 팝업 대신 모달
개발 과정
1. Dummy Data 생성
2. 주요 기능 개발 : 검색 및 필터, 지도 API 연동
3. 반응형 UI/UX 구현: 모바일 최적화 작업
향후 계획
- 로그인 및 회원가입 기능 추가
- 숙소 예약 기능 개발
- Socket 채팅: 게스트-호스트 간 실시간 소통 기능 개발
느낀점
프로젝트를 시작하고 약 1주일간은 헤더를 고정하는것도 생각하지 못할만큼 UI/UX를 몰랐다.
하지만 실제 서비스중인 웹사이트를 약 2주간 분석하면서 UI/UX측면에서 필요한 기능을 많이 배울 수 있었다.
평소 웹사이트를 사용하면서 기능을 디테일하게 관찰하고 사용하는 습관을 가지면 역량높은 프론트엔드 개발자로 성장하는데 도움 될 것같다.
이번 플젝은 처음으로 기획부터 서버배포까지 혼자헸다.
혼자 어려움을 다 헤쳐나간만큼 공부가 많이 되었고, 많은 성장을 한 것 같다고 느낀다.
역량을 더 갖추기 위해 앞으로도 많이 굴러보자..@_@