[카우치코딩] 6주차 진행 회고

jun gwon·2022년 4월 25일
0

카우치코딩

목록 보기
6/6
post-custom-banner

6주차 진행

  • 댓글의 CRUD 및 별점 기능을 구현하였다.
  • 게시물의 평균 별점 및 좋아요 기능을 구현하였다.
  • 로딩창을 구현하였다
  • 마이페이지 구현중

댓글 구현

댓글의 경우는 기능을 어떤것까지 넣을것인지, 구현을 어떻게 할것인지 고민을 많이 했던것 같다. CSS는 댓글을 구현할때쯤에 이제 flex 레이아웃을 어떻게 구현해야 하는지 감을 많이 잡게 되었다. 이제서야 서먹서먹했던 css와 친구가 된 느낌이였다.

기능의 경우에는 처음에는 이미지까지 처리할까 하다, 여유가 없을것 같아서 폐기 하였고, 구현의 경우에는 입력하는 텍스트의값을 리덕스로 뺄까, 컴포넌트의 state로 뺄까 고민하다 굳이 전역으로 둘 필요는 없을것 같다 컴포넌트위 state로 빼는걸로 하였다. 지나고보면 왜 그런 고민을 했는지 잘 기억이 나지 않는것 같다.
댓글의 페이징 기능은 우선순위가 높지 않고, 여유롭게 진행중인 상황이 아니여서 구현하지 않았다.

  const onPublish = useCallback(
    async ({ reviewId, content, reviewRating, image }) => {
      try {
        if (reviewId) {
          await updateReview({
            reviewId,
            content,
            placeId,
            reviewRating,
            image,
          });
          dispatch(
            updateReviewState({ reviewId, content, reviewRating, image }),
          );
        } else {
          await writeReview({ content, placeId, reviewRating, image });
          dispatch(readReview({ placeId }));
        }
      } catch (e) {
        console.log(e);
      }
    },

    [dispatch, placeId],
  );
// 리듀서내 액션 update와 remove 
    [UPDATE_REVIEWS]: (
      state,
      { payload: { reviewId, content, reviewRating, image } },
    ) => ({
      ...state,
      reviews: state.reviews.map((item) =>
        item.id === reviewId
          ? { ...item, content: content, reviewRating, image }
          : item,
      ),
    }),
    [REMOVE_REVIEWS]: (state, { payload: { reviewId } }) => ({
      ...state,
      reviews: state.reviews.filter((item) => item.id !== reviewId),
    }),

댓글의 등록과 수정,삭제는 약간 다른 로직을 갖고있는데, 일반적인 등록은 reviewId값이 없는경우 댓글 등록API를 실행하였고, 이후에 리뷰값을 전체 다 받아오는 dispatch(readReview()) 를 실행하여서 댓글을 새로 다 받아 재렌더링 하였다. 이렇게 한 이유는 사용자가 댓글을 쓰는 순간에도 다른 사용자가 댓글을 작성할수있어 받아올때의 댓글List와 작성후의 댓글 List가 다를수있을거라 생각하였기 때문이다.

update와 remove의 경우는 댓글List가 변할수는 있어도, 그 순서에는 변화가 없을것이라 생각하여 API 전송 성공이후, 리덕스에서 보관하는 리뷰의 값만 수정 또는 제거하였다.

평균 별점 및 좋아요 기능 구현

//좋아요를 체크해줫는지 판별하는 state값
const [isSubscribe, setIsSubscribe] = useState(false);
//클릭 이벤트
const onLikeClick = async () => {
  const response = await subscribePlace({ placeId });
  dispatch(updateLikeCount({ likeCount: response.data }));
  setIsSubscribe(!isSubscribe);
};
//초기 상세페이지 클릭시 받아오는값
  useEffect(() => {
    async function getData() {
      dispatch(getPlace({ placeId }));
      if (user) {
        const response = await checkSubscribe({ placeId });
        const checked = response.data.isLove;
        setIsSubscribe(checked);
      }
    }
    getData();
  }, [dispatch, placeId, user]);

//리뷰 평점
  const avgRating = () => {
    if (reviews.length === 0) return 0;
    return (
      reviews.reduce((acc, cur) => {
        return (acc += parseFloat(cur.reviewRating));
      }, 0) / reviews.length
    );
  };

좋아요의 경우는 처음에 상세페이지의 정보를 받아올때 유저가 로그인 상태라면 서버에 해당 유저가 이 페이지의 좋아요를 누른적이 있는지 bool값으로 받아서 isSubscribe에 저장한다. 유저가 로그아웃상태라면 기본값인 false가 들어간다. 좋아요 버튼을 누를시에는 isSubscribe의 값을 반대로 하도록 하였다. 또한 API에서 받아온 업데이트 이후 좋아요COUNT값을 리덕스에서 변경하였다.

isSubscribe값은 하트아이콘을 표현할때 사용하는데, true면 fill, false면 속이 비어있도록 하였다.

평균 평점은 백엔드쪽에서 따로 받아오는건 없고, 전체 review를 받아오는데, 해당 리뷰의 length와 각 리뷰의 reviewRating을 리듀스를 사용하여 구현하였다.

로딩창 구현

로딩창은 노력대비 자주 보이고, 커서 만족도가 높았다.
첫 구상은 전체 화면의 배경색을 주어 가운데에 loading아이콘이 돌아가는걸 구상하였는데, 현재 라우터에 헤더가 기본 라우터로 설정되어있어서, 헤더부분만 배경색이 적용되지 않았다. 아마 포탈을 사용하여 리액트의 root바깥쪽에서 렌더링하면 될거 같긴한데, 타협하여 무색의 배경화면에 공통 헤더부분을 제외한 화면 가운데에서 스핀만 돌아가게하는걸로 하였다.

const LoadingPageBlock = styled.div`
  width: 50%;
  height: 50%;
  left: 25%;
  top: 25%;
  position: fixed;
  opacity: 0.3;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 10;
`;

const LoadingPage = () => {
  return (
    <LoadingPageBlock>
      <LoadingOutlined style={{ fontSize: '150px' }} />;
    </LoadingPageBlock>
  );
};
....
// 사용
  if (loading) return <LoadingPage />;

마이페이지

구상한 마이페이지의 옵션은 내가 좋아요 버튼을 누른 장소들의 카테고리와 지역버튼이 모여있는 영역이 따로있어서 선택이 가능하고, 왼쪽에는 아이템 리스트가 나오는 형식이였다.

생각한걸 구현 하기 위해선
페이지에 진입시 GET으로 DB에 data요청 - redux의 places에 정보저장
places에서 카테고리와 지역정보의 중복되지 않은 정보값 필요.
컴포넌트에서 places를 조건 별로 필터링할때 쓰일 state
카테고리와 지역버튼을 담을 state
이 필요하였다.

일단 처음으로 구현한건 카테고리,지역버튼(이하 태그)를 담을 state를 구현한거였는데,

// 받아온 places에서 중복되지 않는 카테고리와 지역정보값, 이 값들을 사용해서 태그를 렌더링한다.
  const categorys = places
    ? [...new Set(places.map((item) => item.category))]
    : [];
  const regions = places
    ? [...new Set(places.map((item) => item.region_1))]
    : [];

  // 선택된 태그를 담을 state
  const [selectedTag, setSelectedTag] = useState({
    categorys: [],
    regions: [],
  });

//태그 클릭 이벤트
  const onClick = (item) => {
    const key = Object.keys(item).toString();
    let updateItem = [];
    const originalItem = selectedTag[key];
    // 전체 버튼 클릭시
    if (!item[key]) {
      setSelectedTag({
        ...selectedTag,
        [key]: updateItem,
      });
    } else {
      // 태그 클릭시 기존 아이템이 존재하면 제거, 없으면 추가
      updateItem = originalItem.includes(item[key])
        ? originalItem.filter((originItem) => originItem !== item[key])
        : [...originalItem, item[key]];
      setSelectedTag({
        ...selectedTag,
        [key]: updateItem,
      });
    }
  };

//선택된 태그들의 렌더링, 선택시에는 활성화, 선택되지 않았을시에는 비활성화
  const categorysRender = () => {
    if (!categorys) return;
    const categorysItem = [];
    categorysItem.push(
      //전체 태그버튼은 처음 한번 생성
      <CustomButton
        shape={'round'}
        key="cat_all"
        //categorys 태그가 선택된게 없으면 체크true
        checked={!selectedTag.categorys.length}
        onClick={() => onClick({ categorys: '' })}
      >
        전체
      </CustomButton>,
    );
    categorys.forEach((item, idx) => {
      categorysItem.push(
        <CustomButton
          shape={'round'}
          key={idx}
          //해당 태그가 selectedTag.categorys에 존재하면 체크 true
          checked={selectedTag.categorys.includes(item)}
          onClick={() => onClick({ categorys: item })}
        >
          {item}
        </CustomButton>,
      );
    });

이런식으로 구현하였다. 이전에 구현한 모달태그와 크게 다르진 않았다.

이후 선택된 정보태그값을 가지고 useEffect에서 태그값에 의존성을 두었고, 선택된 태그값이 places의 각각의 item들에 포함되어있는 값들만 sortedPlaces라는 places의 정보를 담을 state에 보관하였다.

  //places 정보를 담고 조건 selectedTag에 따라 필터링할 state
  const [sortedPlaces, setSortedPlaces] = useState([]);

  useEffect(() => {
    if (
      //선택된 태그가 없으면 전체 정보 저장
      selectedTag.categorys.length === 0 &&
      selectedTag.regions.length === 0
    ) {
      setSortedPlaces(places);
      // 선택된 categorys 태그가 없으면 places의 아이템중에서
      //regions에 저장된 값과 일치하 정보를 필터링
    } else if (selectedTag.categorys.length === 0) {
      setSortedPlaces(
        places.filter((item) => selectedTag.regions.includes(item.region_1)),
      );
    } else if (selectedTag.regions.length === 0) {
      setSortedPlaces(
        places.filter((item) => selectedTag.categorys.includes(item.category)),
      );
    } else {
      //태그가 모두 선택 상황일시 두값 다 필터링
      setSortedPlaces(
        places.filter(
          (item) =>
            selectedTag.categorys.includes(item.category) &&
            selectedTag.regions.includes(item.region_1),
        ),
      );
    }
  }, [selectedTag.categorys, selectedTag.regions, places]);

추가적으로 렌더링시에는 필요한 부분만 렌더링 한게아닌, 전체를 다 렌더링하고, 필요하지 않은 정보는 display:none으로 처리 하지 않는 방식을 하였는데, 이 방식을 사용한다면 첫 화면에서 페이지 뒤편까지 모두 렌더링 된 상태이기때문에, 다음 페이지로 넘어갈때 부드럽게 넘어갈수있게된다.

  const render = () => {
    const result = [];
    sortedPlaces.map((place, idx) => {
      //인덱스가 해당 페이지에 맞을때만 보이는 컴포넌트를 생성
      if ((pageInex - 1) * VIEW_ITEM <= idx && idx < pageInex * VIEW_ITEM)
        return result.push(
          <PlaceItem place={place} key={idx} onLikeClick={onLikeClick} />,
        );
      //인덱스가 해당 페이지에 해당 되지 않을때 숨김
      else
        return result.push(
          <PlaceItem
            place={place}
            key={idx}
            hidden
            onLikeClick={onLikeClick}
          />,
        );
    });
   return result;

(마이페이지의 아이템들은 페이지네이션을 하였다. 페이지 인덱스는 1부터 시작이고, 한 페이지에 보여줄 VIEW_ITEM은 4로 설정하였다.)

마지막으로 태그를 클릭하여 sortedPlaces의 값이 바뀌면 pageIndex는 1로 초기화 하도록 하였다. 이부분을 설정하지 않는다면 버그가 발생하게된다.(5페이지를 보고있다 다른 태그를 선택하였는데, 페이지가 그대로 5페이지인채로 있게된다)

  useEffect(() => {
    setPageInex(1);
  }, [sortedPlaces]);


(완성 화면)

이번 프로젝트에서 무한 슬라이딩이나 무한스크롤, 그리고 모달 등등을 하였지만
마이페이지의 구현이 가장 React의 장점을 많이 사용할 수 있었던 부분이였던것 같다.
개인적으로 이번 프로젝트에서 가장 마음에 드는 기능구현이였다.

마무리

시작할때는 포부넓게 시작하였지만, 하다보니 부족함을 많이 느꼈다.

목표로 했던 타입스크립트 프로젝트는 3월달 이후 실행도 못해봤고,
해보고 싶었던 테스팅코드도 못해봤고,
최적화도 아직 손도 대보지 못했고,
코드 리팩토링도 못하였고,
GIT의 사용도 미숙해서 commit도 일관되게 사용하지 못했다.
심지어 아직 기능구현도 마무리 짓지 못했다.(마감전에는 가능할것 같다..)

그래도 얻은것도 많은 프로젝트라고 생각한다.
가장 중요한, "스스로 프로젝트를 설계에서부터 마무리까지 하였다." 라는 경험이 가장 큰 결실물이라 생각한다. 돌이켜보면 스스로 공부할때는 숙제 형식이든, 책의 예제를 따라하든 css는 기본적으로 제공되고, 기능구현 위주의 공부였는데, 이번 프로젝트처럼 백지에서 별 다른 도움 없이 시작해서 마무리까지 짓는건 처음이였다. 또한 프로젝트를 진행하면서 필요한 기능은 직접 찾아가면서 공부 및 구현하였고, css도 이전까지는 이해는 하되, 스스로 짤 생각은 못하였는데, 이번 프로젝트에서 실제로 작성하면서 많이 익숙해진것 같다.

백엔드분과의 협업 프로젝트도 이번이 처음이였다.
이전에는 빈약한 지식이긴 했지만, DB도 직접 구현하면서 필요하거나 수정될게 있으면 그때그때 수정하였는데, 이번 프로젝트에서는 백엔드와 프론트의 인원이 따로 나뉘어 있어서 뭔가 이전에 기획했던것 이외에 다른 걸 요구한다던가, 출력 값의 수정을 요구한다던가 하는 갑작스런 요구변경에 미안함을 느꼈다.
그리고 그런 요구들은 바로 반영되는것도 아니여서, 진행이 정체되기도 하였다.
이번 프로젝트 기능구현 설계의 빈약함과 설계의 중요함을 느끼게 되었다.

post-custom-banner

0개의 댓글