프론트엔드 개발 포트폴리오 | 국내 여행지 추천 서비스

내일배움캠프 4기 웹 개발 과정 React 트랙 수료생 최종 프로젝트 '트리픽'을 소개합니다.
Sep 04, 2023
프론트엔드 개발 포트폴리오 | 국내 여행지 추천 서비스

프로젝트명: TRIPICK

안녕하세요! 저희 TRIPICK은 국내 여행지와 주변 숙박/식당을 추천해드리는 사이트입니다. 😊
혼자 여행하거나 친구들과 함께 떠나려는 분들께 여행지 아이디어를 제공합니다. 🤝
또한 여행지 반경20km 내에있는 숙박과 맛집 정보를 찾고 계신 분들을 위해 서비스를 제공합니다. ✨
 

Github Repository

 

Architecture

notion image

트러블슈팅

유영재 (소셜로그인 Oauth2.0 모달분리)
문제점
  1. kakaologin 시에 oauth2.0을통한 인가토큰발급정상서비스도중
ui적문제로 로그인페이지에서 모달창으로 변경 그에따른 로그인 로직이 변경이되어야했음
 
notion image
 
notion image
 
notion image
해결방안 accesstoken이 발급되는 로직을 기존 함수에서 분리 하여 navbar에서
유저의 정보를 불러올수있게 변경 또한 소셜로그인중 네이버로그인도 구현하였기에
네이버로그인도 같은방식으로 처리
 
2. kakao로그아웃시 개인정보를 localstorage와 setionstorage에 유저의 정보가 남고 로그아웃을해도 주소창에 파라미터값이 남아있어 보안상 좋지않아 제거및 강제 navigate 하였음 또한 accessToken을 만료하여 재로그인이 불가능하게 구현
notion image
 
  1. Throttling을구현하면서 lodash가아닌 다른방법으로 구현할수있다는 방법을 찾음
버튼이 제한시간내에 클릭이 되면 안되는 구조로 라이브러리를 사용하지않고 구현하여
라이브러리의 갯수를줄여 프로젝트의 전체적볼륨에 부하를 덜줄수있지않을까?
notion image
 
4.slick 슬라이더베너 arrow 커스터마이징
이미지를사용하여 버튼의모양을 다른것으로 교체는 가능하였지만 기존 arrow가 사라지지않는 문제가생김
const StyledSlider = styled(Slider)` .slick-prev::before, .slick-next::before { opacity: 0; /* display: none; */ } `; const settings = { dots: false, lazyLoad: true, // 필요에 따라 또는 점진적으로 이미지를 로드하거나 구성 요소를 렌더링합니다. infinite: true, //무한으로돌것인가? speed: 1000, //1000 == 1s 슬라이드가 넘어가는 시간 slidesToShow: 1, //몇개를 보여줄것인가 slidesToScroll: 1, // 몇개를 넘길것인가 initialSlide: 1, //첫 번째 슬라이드의 인덱스 autoplay: 1000, //1000 == 1s 자동으로 넘어가는 시간 arrows: false, nextArrow: ( <NextTo> <ArrowImg src={nextImg} /> </NextTo> ), prevArrow: ( <Pre> <ArrowImg src={pervImg} /> </Pre> ), };
을사용하여 제거해주면 해결!
 
 
예재현 (불필요한 리랜더링 개선)
문제점1 : 마이페이지 나의 찜목록 화면이 나타나자마자 불필요한 통신 및 리렌더링이 계속되는 현상을 개발자 도구의 React Developer Tool과 Network를 통해 확인하였음
원인 : 내가 찜한 것에 따른 Data 변화를 감지하고자 useEffect의 의존성 배열에 해당 클릭 이벤트를 넣었었는데 이것이 위와 같은 버그를 유발하였음.
해결방법 : useEffect의 의존성 배열은 처음 화면에 마운트될 때만 실행되도록 비우고 최신 데이터 상태를 불러오는 함수를 각 클릭 이벤트 함수에 추가하여 문제를 해결하였음
//AS IS const delStayLiked = async (targetId: string) => { if (place && uid) { const docRef = doc(db, 'bookmarks', uid); const stayDocRef = doc(db, 'stay_recommendation', targetId); const TargetBookmark = place.bookmarks.find( (e: { contentid: string }) => e.contentid === targetId, ); await updateDoc(docRef, { bookmarks: arrayRemove(TargetBookmark), contentid: arrayRemove(targetId), }); await updateDoc(stayDocRef, { likeCnt: increment(-1), }); } }; useEffect(() => { getMyBookmarkList(); }, [delStayLiked]); //TO BE const delStayLiked = async (targetId: string) => { if (place && uid) { const docRef = doc(db, 'bookmarks', uid); const stayDocRef = doc(db, 'stay_recommendation', targetId); const TargetBookmark = place.bookmarks.find( (e: { contentid: string }) => e.contentid === targetId, ); await updateDoc(docRef, { bookmarks: arrayRemove(TargetBookmark), contentid: arrayRemove(targetId), }); await updateDoc(stayDocRef, { likeCnt: increment(-1), }); } getMyBookmarkList(); }; useEffect(() => { getMyBookmarkList(); }, []);
문제점2 : 상세페이지 좋아요 버튼 클릭 시 카운트 반영이 제대로 안되는 문제
원인 : firestore DB에 있는 값이 real-time으로 반영되지 않고 useState의 초기값(0)이 계속 영향을 미쳐 제대로 카운트가 안되고 -1과 같은 값이 들어오게 됨
해결방법 : 기존에 getDoc 함수를 통해 가져오던 좋아요 수(likeCnt) 데이터를 onSnapshot 함수로 대체함으로써 firestore의 좋아요 카운트 수를 즉각 반영하도록 수정됨
//AS IS const getLikeCnt = async () => { const res = await getDoc(doc(db, 'spot_recommendation', param.id), (doc) => { setLikeCnt(res.data().likeCnt); }); }; //TO BE const getLikeCnt = async () => { await onSnapshot(doc(db, 'spot_recommendation', param.id), (doc) => { setLikeCnt(doc.data().likeCnt); }); };
김혜진 (분리되어있는 api개선)
문제점(1) : 여러 API들을 한번에 불러오는 방법은? 이번 프로젝트에서는 관광,숙박,식당 3가지의 api를 사용하기때문에 2개 또는 3개의 api를 한번에 합쳐서 사용하는 경우가 많았다. (예를들어, 숙박시설을 기준으로 주변의 음식점 / 관광지의 위치를 구하는 경우)
 
해결방법 : 다른 props를 사용하듯이 API 또한 병합해서 사용하면 되는 부분으로 간단하게 해결할수있었다. 스프레드 연산자 또는 배열로 합친 데이터를 새로 만들어 넣어주면 해결 완료.
type StayInfoProps = { restaurantDetailData?: DetailDataTypes; spotData?: DetailDataTypes; }; const StayInfo: React.FunctionComponent<StayInfoProps> = (props) => { const { restaurantDetailData, spotData } = props; const navigate = useNavigate(); const combinedData = { ...restaurantDetailData, ...spotData, };
심대호 (서버데이터 patch에 대한 문제점)
  • 닉네임 수정시 화면에 잘 보이나 로그아웃시 닉네임이 수정 전으로 돌아가는 오류 발생
const useSaveEdit = async () => { // 닉네임 수정 updateProfile(auth.currentUser, { displayName: currentInput, }).catch((error) => { console.log(error.message); }); localStorage.setItem('id', currentInput); updateNickname(currentInput); alert('수정되었습니다.'); };
해결방안: 처음에 updateProfile(currentUser)만 써있는 상태에서 파이어베이스 공식 문서를 보면서 updateProfile을 쓸떄는 auth.currentUser를 써야한다고 되어있어서 고치니 잘 오류가 해결되었다.
 
  • 새 비밀번호를 입력했을때 input에 있던 전에값을 초기화시킬때 자꾸 alert창이 뜨기전에 초기화가 먼저되서 alert창이 뜨고난 후 input창을 초기화 시키고 싶을때
// 일정 시간 이후에 비밀번호 필드 초기화 setTimeout(() => { setPasswordInput({ ...passwordInput, updatePassword: '', updatePasswordCheck: '', }); }, 2000); };
settimeout을 이용해 일정시간 이후에 비밀번호 필드를 초기화 시켰다
송원석 (로그인,댓글 랜더링이안되는 문제점)
  • 로그인 유저가아닐경우 페이지 자체가 안보이는 오류 발생
{loginUser?.uid === review?.uid ? ( <button onClick={() => { handleDelete(review.id, i); }} > {!editBox ? '삭제' : null} </button> ) : null} {loginUser?.uid === review?.uid ? ( <button onClick={() => { handleUpdate(review.id); setEditBox(!editBox); }}
옵셔널 체이닝을 사용하지 않았을 경우 로그인 유저가 없는 상태이기 때문에 로그인 유저의 uid 값 자체가 들어오지 않는다.
로그인 유저가 없는 예외성의 경우까지 생각해야 함으로
옵셔널체이닝을 사용해 오류 해결!
 
카카오,네이버 소셜로그인 유저 댓글 등록
//리뷰 등록 const creatReview = async () => { const loginUser = auth.currentUser; if (loginUser) { const addRev = await addDoc(usersCollectionRef, { review: newReview, uid: loginUser?.uid, email: loginUser.email, displayName: localStorage.getItem('id', auth.currentUser.displayName), //loginUser?.displayName paramId: params.id, date: Date.now(), //파이어스토어 db, reviews 에 저장 }); setNewReview(''); } else { alert('로그인을 하세요'); } };
//삭제 const handleDelete = async (id, i) => { if (auth.currentUser.uid === reviews[i].uid) { const reviewDoc = doc(db, 'reviews', id); await deleteDoc(reviewDoc); } else { alert('작성자가 다릅니다.'); //작성가 다르거나 비로그인 유저에게 버튼이 보이지 않는다면 필요없어짐. } }; //업데이트 const handleUpdate = async (id) => { await updateDoc(doc(usersCollectionRef, id), { review: editValue, }); };
업데이트 이후
//리뷰 등록 const creatReview = async () => { const Kakaologinid = localStorage.getItem('uid'); const Naverloginid = localStorage.getItem('uid'); const loginUser = auth.currentUser; if (loginUser) { const addRev = await addDoc(usersCollectionRef, { review: newReview, uid: loginUser?.uid, email: loginUser.email, displayName: localStorage.getItem('id', auth.currentUser.displayName), //loginUser?.displayName paramId: params.id, date: Date.now(), //파이어스토어 db, reviews 에 저장 }); setNewReview(''); } else if (Kakaologinid) { const addRev = await addDoc(usersCollectionRef, { review: newReview, uid: localStorage.getItem('uid'), displayName: localStorage.getItem('id'), //loginUser?.displayName paramId: params.id, date: Date.now(), //파이어스토어 db, reviews 에 저장 }); setNewReview(''); } else if (Naverloginid) { const addRev = await addDoc(usersCollectionRef, { review: newReview, uid: localStorage.getItem('uid'), displayName: localStorage.getItem('id'), //loginUser?.displayName paramId: params.id, date: Date.now(), //파이어스토어 db, reviews 에 저장 }); setNewReview(''); } else { alert('로그인 해주세요'); } };
//삭제 const handleDelete = async (id, i) => { console.log(id); if (auth.currentUser?.uid === reviews[i].uid) { const reviewDoc = doc(usersCollectionRef, id); //파이어스토어, 안에있는 컬렉션 'reviews' 의 문서 id await deleteDoc(reviewDoc); } else if (KakaoAndNaverLoginid === reviews[i].uid) { const reviewDoc = doc(usersCollectionRef, id); await deleteDoc(reviewDoc); } else { alert('작성자가 다릅니다.'); //작성가 다르거나 비로그인 유저에게 버튼이 보이지 않는다면 필요없어짐. } }; //업데이트 const handleUpdate = async (id) => { await updateDoc(doc(usersCollectionRef, id), { review: editValue, }); };
카카오,네이버 로그인시 로컬스토리지에 키/벨류 값으로 아이템을 넣어주었는데 그 값들을 가지고 추가 삭제 수정 기능을 구현
소셜로그인을 하였을때 글을 삭제하는 기능 부분에서
auth.currentUser.uid === reviews[i].uid
부분에서 오류가 발생하였는데
auth.currentUser?.uid === reviews[i].uid
?. 옵셔널 체이닝을 사용하여 오류 해결
 
  • 페이지 네이션 페이지수 문제해결
const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(6); useEffect(() => { const getReviews = async () => { const q = query(usersCollectionRef, orderBy('date', 'desc')); const unsubscrible = onSnapshot(q, (querySnapshot) => { const newList = querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), })); setReviews(newList); }); return unsubscrible; }; getReviews(); }, []); </ReviewContainer>
이런식으로 구현하니 아래 사진과 같은 상황이 발생
notion image
totalItemsCount={reviews.length}
이 부분 때문에 댓글의 수 만큼 페이지가 생김.
현재 paramId로 ID와 맞는 댓글만 보여주고 있는 기능인데 모든 댓글들을 파이어 스토어 review 컬렉션에서 관리해서 그런것 같음.
 
수정후
 
const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(6); const [totalReviewCount, setTotalReviewCount] = useState(0); useEffect(() => { const getReviews = async () => { const q = query(usersCollectionRef, orderBy('date', 'desc')); const unsubscrible = onSnapshot(q, (querySnapshot) => { const newList = querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), })); const filteredList = newList.filter( (review) => review.paramId === params.id, ); setReviews(filteredList); setTotalReviewCount(filteredList.length); }); return unsubscrible; }; getReviews(); }, []);
newList에서 paramId가 params.id 값과 일치하는 모든 객체를 찾아서 새로운 배열인 filteredList에 저장 .
filteredList 배열은 newList 배열에서 paramId 속성이 parmas.id와 일치하는 모든 객체를 포함하는 새로운 배열인데 filteredList.length를 totalReviewCount의 갯수로 넣어주어서 해결하였다.
setTotalReviewCount(filteredList.length);
 
notion image
 

😘 팀원

팀원명
팀원역할
GITHUB
EMAIL
유영재(L)
전반적 프로젝트 일정 관리 , 소셜로그인, ChatBot, 관광지 슬롯머신, search 기능 전담/ UI,UX
예재현(BL)
API 데이터 가공, 스켈레톤 UI, 메인페이지 탭, 페이지네이션, 필터링, DB 설계, 토스트 메시지 기능 구현 전담 / UI, UX
김혜진
Kakao map, 상세페이지 API DATA 관리및 개선, 좋아요기능, 마이페이지 찜하기기능 전담 / UI, UX
송원석
상세페이지 댓글 CRUD ,소셜로그인 댓글 연동, 로그인 / FIREBASE 기능 전담 /UI, UX
심대호
API 데이터 가공, 스켈레톤 UI, 메인페이지 탭, 페이지네이션, 필터링, DB 설계, 토스트 메시지 기능 구현 전담 / UI, UX
소수현(D)
전체적인 UI, UX 디자인 전담
Share article
Subscribe Newsletter
Stay connected for the latest news and insights.
RSSPowered by inblog