Skip to content

Yanolza-Miniproject/Heynolja_FE

Repository files navigation

✨미니프로젝트 4조 - 아버지날보고있다면정답을알려조✨

🏖️ 행복한 여행, 즐거운 시간. 'HEY놀자!' 🏖️

바쁜 일상에 지친 여러분, 다음 목적지는 휴식입니다. 나의 소중한 하루를 선사할 곳을 'HEY놀자'에서 만나보세요!

* 테스트 계정 정보
ID : admin@admin.com
PW : qwert1234

'HEY놀자'는 이런 곳이에요.

  • 내 위치 기반/ 인기순 /주차 가능 숙소 등 다양한 카테고리의 숙소를 보실 수 있어요.
  • 지역별, 숙소 타입별, 추가 옵션 여부 등 내 입맛에 맞는 숙소를 찾아볼 수 있어요.
  • 숙소별 상세 안내, 일자별 객실 가능 여부 등의 다양한 정보를 확인할 수 있어요.
  • 마음에 쏙 드는 숙소는 내 카트에 담아두거나, 결제하여 예약할 수 있어요.
  • 언제라도 꼭 가고 싶은 숙소, ❤️[찜하기] 기능을 이용해보세요.

프로젝트 세부 정보

📍개발 환경

visualstudiocode git github eslint prettier husky vite

📍배포

vercel

📍사용 기술

react typescript reactrouter recoil emotion reactquery axios msw jest

📍협업 툴

notion figma slack zep

👪 아버지날보고있다면정답을알려조 팀원 소개

이용훈 프로필 박수연 프로필 이승연 프로필 김민섭 프로필 김미정 프로필
이용훈
Frontend & 팀장
박수연
Frontend
이승연
Frontend
김민섭
Frontend
김미정
Frontend

💻 팀원 별 구현 사항

이름 역할
개발 내용
이용훈 팀장 장바구니
결제 완료
박수연 팀원 숙소 상세
방 타입 상세
이승연 팀원 주문하기
마이페이지
김민섭 팀원 좋아요 기능
검색 결과 페이지
배포
김미정 팀원 공통 컴포넌트 제작
검색(필터) 페이지
메인 페이지

🎁 프로젝트 아키텍처

mini

🪄 주요 구현 내용

⭐ 테마별 숙소 추천 로직 구현

  • 사용자의 빠른 선택을 도울 수 있도록 GPS 기준 지역별 숙소 추천, 인기 숙소 안내 등의 기능을 제공합니다.

⭐ 회원가입, 로그인 기능 및 인증, 404페이지

  • 이메일과 비밀번호 기준으로 회원가입을 진행하며, 이를 기준으로 로그인 할 수 있습니다.
  • 인증이 필요한 페이지의 경우 미인증 회원이 접근할 시 로그인 페이지로 이동됩니다.
  • 접근이 불가한 주소로 이동 시 404페이지로 이동합니다.

⭐ 숙소 조회를 위한 다양한 검색 필터 지원, 개별 상품 페이지 제공

  • 숙소 타입, 장소, 추가 옵션별로 원하는 카테고리를 선택하여 숙소를 조회할 수 있습니다.
  • 미선택시 전체 상품 조회가 가능합니다.
  • 특정 숙소 클릭시 해당 숙소에 대한 상세 정보와 품절여부를 확인할 수 있으며 장바구니 담기 및 바로 주문하기를 진행할 수 있습니다.

⭐ 장바구니, 결제하기

  • 숙소 상세페이지를 통해 희망하는 날짜/인원수를 설정하여 장바구니에 추가할 수 있습니다.
  • 장바구니에서 결제 진행할 숙소를 선택하고, 결제하기 페이지로 이동할 수 있습니다.
  • 결제하기 페이지내에서 주문하려는 상품을 다시 한 번 확인하고, 만 14세 이상 이용 동의 체크박스를 필수로 입력 받은 후 결제할 수 있습니다.
  • 결제 완료된 경우 완료 페이지내에서 결제한 정보를 확인할 수 있습니다.

⭐ 회원의 경우 결제 이력, 장바구니, 찜목록 관리 기능 제공

  • 여태 주문한 모든 주문 이력을 건별로 상세하게 확인할 수 있습니다.
  • 장바구니에 담아둔 상품의 데이터를 보여주며, 실제 결제할 상품을 선택하고 진행할 수 있습니다.
  • 하트 아이콘을 통해 찜목록에 추가했던 숙소들을 확인할 수 있습니다.

⚒️ 주요 문제 및 해결 방법

MSW를 사용하면서 프론트엔드 프로젝트 파일의 복잡성이 증가

문제: MSW를 사용하면서 프론트엔드 프로젝트 파일의 복잡성이 증가하고, 초기 설정 오류로 인한 시간적인 소요가 컸습니다.

해결: 프론트엔드 프로젝트 파일의 깔끔함을 유지하기 위해 MSW를 사용하지 않고, express로 따로 간단한 목 서버를 만들어 각자 로컬에서 실행시켜 확인하는 방법을 시도해 봤습니다.

결과:

1. 프론트엔드 코드에 MSW 설정에 대한 코드를 넣지 않아도 돼서 코드 파일을 관리하는데 효율성이 증가하였습니다.
2. MSW 코드와 express 코드의 유사성이 커서 express를 새로 배워야 한다는 부담이 적었고, 실제로 MSW로 API코드를 작성하는 시간보다 express로 코드를 작성하는 시간이 더 적었습니다.
3. 목 데이터가 따로 관리하는 express 목 서버 프로젝트에 있기 때문에 프론트엔드 파일에서 목 데이트를 이용해 테스트를 진행할 때 데이터를 다시 만들어 줘야 한다는 단점이 있었습니다.
onError 처리시 event의 target 설정이 되지 않는 문제

문제: img태그의 onError로 에러 이미지 처리시 event의 target을 인식하지 못하는 문제 발생

해결: 기존에 사용하던 방식은 아래와 같이 event의 타입을 React.ReactEventHandler 로 설정 후 event타겟의 src를 에러 이미지로 설정하는 형태였다.

  const handleError = (e: React.ReactEventHandler<HTMLImageElement>) => {
    e.target.src = Empty;
  };

그러나 위의 방식으로 진행시 target 적용이 되지 않아 event 타입을 React.SyntheticEvent<HTMLImageElement, Event>로 변경하였으며, SyntheticEvent에는 target대신 이벤트가 부착된 부모의 위치를 반환할 수 있는 currentTarget을 활용하였다. 최종적으로 아래와 같이 코드 수정하여 문제 해결 하였다.

  const handleError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
    e.currentTarget.src = Empty;
  };
Axios Interceptor로 로딩 애니메이션 추가시 발생한 문제

문제: Axios Interceptor를 활용하여 로딩 애니메이션을 추가하려고 하였으나, 시도해본 방법으로는 로딩 애니메이션이 제대로 출력되지 않았음. Axios Interceptor에 instance 외에도 setIsLoading 상태 값을 필수 parameter로 설정하고 전역적으로 관리하는 등의 방법을 활용해야 했지만, 이미 작업된 컴포넌트 구조 및 시간 관계상 기존 로직 추가 및 변경이 복잡해지는 문제가 있어 다른 방법을 찾아보기로 함.

해결: 모든 컴포넌트가 react-query를 이용해 api data fetching을 진행하고 있다는 점을 활용하기로 하였음. React-query에서 제공하는 기능 중 현재 fetching 중인 쿼리의 개수를 리턴하는 훅인 "useIsFetching"을 이용해, 현재 데이터가 로딩 중인 경우 로딩 애니메이션이 출력될 수 있도록 아래와 같이 코드를 작성함.

export const Loader = () => {
  const isLoading = useIsFetching();

  return isLoading > 0 ? (
    <LoaderContainer>
      <LoaderWrapper>
        <PacmanLoader color="#FF5000" size={30} />
        <p>잠시만 기다려주세요.</p>
      </LoaderWrapper>
    </LoaderContainer>
  ) : null;
};
객체 내의 여러 값에 접근할 땐 destructuring을 활용

문제 : 객체 내의 여러 값에 접근할 때 너무 많은 프로토타입 체인이 필요하다.

해결 : axios의 data를 destructuring을 활용하여 작성하니 코드가 한결 깔끔해진 것 같음 앞으로 axios의 return data에서 많은 프로퍼티에 접근할 때는 destructuring을 사용

export const fetchSignin = async ({ email, password }: SignInInputs) => {
  const { data, headers } = await baseInstance.post("members/login", {
    email,
    password,
  });

  const returnData = {
    accessToken: headers["access_token"],
    refreshToken: headers["refresh_token"],
    message: data.message,
    memberId: data.data.memberId,
    nickname: data.data.nickname,
  };

  return returnData;
};
쿼리에 조건부 데이터를 넣어야 할 때

문제 : 필요한 쿼리 파라미터만 보내야 하기 때문에 조건부로 query parameter를 넣어주어야 겠다고 생각했습니다. 문자열에 어떤 값이 들어갈 지 도 모르고, 추적도 어려운 것 같습니다. 혹여나 다른 사람이 코드를 수정한다 하더라도 정말 너무 어렵고 주먹구구식으로 작성된 코드라는 것을 알 수 있었습니다. 따라서 axios의 params object를 활용하였습니다.

const data = await authInstance.get(
  `accommodations?page=${pageParam}${regionUrl}${typeUrl}${categoryParkingUrl}${categoryCookingUrl}${categoryPickupUrl}`,
);
  1. 객체의 Spread Operator를 사용

가장 먼저 params 객체 자체를 만들어주는 방법에 대해서 생각해보았습니다. 조건부에 따라서 params 객체를 반환하는 함수를 만들어서 관리하자는 것이 처음 생각이었습니다.

const validParams = (param: string, value: number | boolean) => {
  return value === false ? {} : { [param]: String(value) };
};

export const fetchCatgory = async (
  pageParam: number,
  props: CategoryFilterParams,
) => {
  const params = {
    ...validParams("region01", props.region01),
    ...validParams("type", props.type),
    ...validParams("categoryParking", props.categoryParking),
    ...validParams("categoryCooking", props.categoryCooking),
    ...validParams("categoryPickup", props.categoryPickup),
  };

  const baseUrl = `accommodations?page=${pageParam}`;

  try {
    const data = isLoggedIn()
      ? await authInstance.get(baseUrl, { params })
      : await baseInstance.get(baseUrl, { params });

    return data.data;
  } catch (error) {
    return error;
  }
};

해당 과정을 거치니 기존 URL에 잘못된 정보가 들어가거나, 코드의 가독성이 불편한 문제는 어느정도 해결된 것 같았습니다. 그러나 코드가 길어지고 객체를 6개 만들어서 spread operator를 통해 푸는 형식이다 보니까 가독성도 안 좋고, 코드도 길어지는 단점이 있었던 것 같습니다. 또한 코드가 반복되니까 불필요한 코드 작성도 들어간 것 같았습니다..

  1. for in을 사용해서 객체에 넣기

객체의 key 값을 순회할 수 있는 for in을 사용해서 객체를 만들기로 하였습니다. props를 분리하고, 필요한 props를 for in으로 순회하면서 유효한 프로퍼티만 params 객체에 추가하려고 하였습니다.

export const fetchCatgory = async (
  pageParam: number,
  props: CategoryFilterParams,
) => {

  type paramsType3 = Record<keyof CategoryFilterParams, string>;

  const params: Partial<paramsType3> = {};

  for (const key in props) {
    if (props[key as keyof CategoryFilterParams] !== false) {
      params[key as keyof paramsType3] = String(
        props[key as keyof CategoryFilterParams],
      );
    }
  }

  const baseUrl = `accommodations?page=${pageParam}`;

  try {
    const data = isLoggedIn()
      ? await authInstance.get(baseUrl, { params })
      : await baseInstance.get(baseUrl, { params });

    return data.data;
  } catch (error) {
    return error;
  }
};

다음과 같이 코드를 작성하니 불필요한 코드의 수가 줄어들고 가독성 또한 좋아진 느낌이었습니다. 그러나 코드에서 반복문을 사용하면 코드에서 테스트할 경우들이 많아지고 side effect가 발생할 수 도 있다는 것을 배웠습니다. 따라서 이 점을 개선해볼 필요가 있었습니다.

  1. Object.entries().reduce 활용

중점은 반복을 돌면서 하나하나 축적해 나아가는 기능이었습니다. 이를 위해서는 reduce 고차함수가 필요했고 reduce를 적용하기 위해서 Object.entries()라는 메서드를 사용할 수 있다는 것을 알 수 있었습니다. 따라서 props를 Object.entries()를 사용해서 배열로 만들고 reduce로 원하는 객체를 만들었습니다.

export const fetchCatgory = async (
  pageParam: number,
  props: CategoryFilterParams,
) => {
  type paramsType3 = Record<keyof CategoryFilterParams, string>;

  const initialParams: Partial<paramsType3> = {};

  const params: Partial<paramsType3> = Object.entries(props).reduce(
    (acc, [key, value]) => {
      if (value !== false) {
        acc[key as keyof paramsType3] = String(value);
      }
      return acc;
    },
    initialParams,
  );

  const baseUrl = `accommodations?page=${pageParam}`;

  try {
    const data = isLoggedIn()
      ? await authInstance.get(baseUrl, { params })
      : await baseInstance.get(baseUrl, { params });

    return data.data;
  } catch (error) {
    return error;
  }
};

코드가 훨씬 더 가독성이 있고 간결해진 것 같습니다. 고차함수를 사용하다보니 반복문에 대한 부작용을 걱정하지 않아도 되는 것 같습니다.

🎞️시연 영상

⭐ 로그인, 메인페이지

default.mp4

⭐ 전체 상품 목록 조회

default.mp4

⭐ 조건에 맞는 상품 검색

default.mp4

⭐ 개별 상품 조회 + 상품 옵션 선택

default.mp4

⭐ 장바구니

default.mp4

⭐ 마이페이지 주문 결과 확인

default.mp4

개인 역량 회고

이용훈
  • 느낀점
    • 백엔드 개발자와 처음 협업해보면서 프론트엔드, 백엔드 개발자가 같이 작성하는 API 문서에 대한 중요성이 크게 느껴졌습니다.
    • 처음 사용해보는 라이브러리를(jest, msw) 팀원과 같이 공유하며 빠르게 공부해 나아갈 수 있어서 매우 좋았습니다.
    • 프론트엔드에서의 테스트의 중요성을 느꼈음에 좋았지만, 쓸모없거나 잘 동작하지 않는 테스트들이 많아서 아쉬움이 많이 컸고, 테스트에 관한 공부의 필요성이 크게 느껴졌습니다.

박수연
  • 느낀점
    • 백엔드 팀과의 협업을 통해 API 명세서 조정, 커뮤니케이션 방법 등 프로젝트 관리의 다양한 측면을 경험할 수 있었습니다.
    • 특히 MSW를 활용한 개발 방식을 통해 백엔드 API의 준비 상태에 관계없이 프론트엔드 개발을 지속할 수 있었으며, 이를 통해 유연하고 효율적인 협업 및 개발 방식을 배우고 적용할 수 있었습니다.

이승연
  • 느낀점
    • 필요한 API가 무엇인지 파악하는 능력과 이를 기반으로 백엔드팀과의 소통하는 방법을 터득할 수 있어 매우 뜻깊었습니다.
    • msw, jest와 같이 처음 사용해보는 기술들이 있어 넓은 방면으로 지식을 쌓을 수 있었다고 생각합니다.

김민섭
  • 느낀점
    • 테스트 코드를 왜 작성을 해야하는가에 대해 고민을 해보는 것이 이번 프로젝트의 목표였습니다. 도대체 왜 어려운 TDD를 사용해서 작업을 하는 지 잘 이해가 되지 않았고 막상 테스트 코드를 작성할 때도 같은 의미로 받아들였습니다. 허나 멘토링을 받고 처음 그 이유를 찾았을 때는 "테스트 코드는 미리 에러를 발생시키기 위해서 작성한다." 였습니다. 미리 에러를 발생시켜서 어느 부분에서 문제가 발생할 수 있는 지 확인하는 용도로 사용된다고 느껴졌습니다. 이후 리펙토링을 진행하면서 테스트 코드의 작성의 도움을 상당히 많이 받았습니다. 타입이 틀리거나 잘못된 데이터가 들어갔을 때, 그리고 로직이 제대로 실행되지 않은 체 돌아가는 코드들을 기존보다 쉽게 찾을 수 있었고 상당한 도움이 되었습니다. 이를 통해 테스트 코드가 왜 필요한 지 알 수 있었습니다. 그러나 아직 테스트 코드가 1차원적이라서 이를 위해 더 많은 공부를 해야할 것 같습니다.
    • 백엔드분들과의 협업을 하면서 어느 부분에서 CORS에러가 나는 지 이해할 수 있었고, 직접 이를 실행시켜봐야 어느 부분에서 이러한 문제가 나는 지 알 수 있었습니다. 미리 이러한 에러들을 마주칠 수 있어서 정말 뜻깊은 시간이었습니다.

김미정
  • 느낀점
    • 프로젝트 초기부터 작업 방식, 컨벤션 등을 꼼꼼하게 세팅해놓은 덕분에, 팀원들간에 서로 일관성 있는 코드 작성을 할 수 있어 정말 좋았습니다.
    • 이전에는 프로젝트 기능 구현에만 급급하여 한 컴포넌트 안에 대량의 코드를 쌓아두는 안좋은 습관이 있었는데, 이번 프로젝트를 계기로 커스텀 훅 생성이라던가 작은 컴포넌트 단위로 분리하는 연습을 하면서 코드의 효율성 및 가독성을 향상시킬 수 있었습니다.
    • 함께한 좋은 팀원분들 덕분에 매 순간 새로운 것을 즐겁게 배워나가고 저 스스로도 성장할 수 있는 소중한 계기가 되었습니다. 모두 감사합니다!

✅ 리팩토링 이후 변경점

  1. 기존 MSW로 사용하던 목 서버 이외에 express를 통한 목 서버를 추가하였습니다.
    주소: https://github.com/2YH02/mini-mock-server
  2. 유틸 함수를 추가하여 코드의 재사용성을 높였습니다.
  3. 컴포넌트 로딩 방식 변경 및 스크롤 기능 보완을 통해 앱의 성능을 개선하였습니다.
  4. 불필요한 주석 삭제, 기존 코드의 전반적인 로직 정리로 코드의 가독성을 높였습니다.

About

사용자 위치 기반 숙소 예약 서비스

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published