[리팩토링]_React에서 커뮤니티 페이지 구현: 검색, 정렬, 무한스크롤의 효율적인 조화

hanseungjune·2023년 8월 10일

리팩토링

목록 보기
25/25
post-thumbnail

왜 이렇게 고쳤는가?

기존의 커뮤니티 페이지 로직은 사용자의 요구 사항을 충족시키기에는 다소 제한적이었습니다. 새로운 코드는 React Hook과 상태 관리를 활용하여 유동적인 정렬, 검색 및 페이지네이션 기능을 제공합니다. 이로써 사용자 경험이 향상되며, 코드의 유지보수와 확장성 또한 증가합니다.

작성 코드 및 설명

상태 초기화 및 정렬

사용자가 검색 또는 정렬을 변경할 때마다 새로운 게시물 목록을 가져오는 로직을 구현하였습니다. 기존의 상태를 초기화하고, 정렬 방식에 따라 적절한 쿼리를 수행합니다.

// 정렬 함수
  const handleSort = (sortCategory: SortCategories) => {
    setActiveSort(sortCategory);
    const isSameCategory = sort === sortCategory;
    if (isSameCategory) {
      setOrder((prev) => ({
        ...prev,
        [sortCategory]: prev[sortCategory] === "asc" ? "desc" : "asc",
      }));
    } else {
      setSort(sortCategory);
      setOrder((prev) => ({
        ...prev,
        [sortCategory]: "asc",
      }));
    }
    // 초기화
    resetPostsState();
  };
/** @jsxImportSource @emotion/react */
import { SortCategories } from "../../hooks/useLoadPosts";
import { UserCommunityBtnStyle } from "../../styles/user/UserCommunity";

export type SortButtonsType = {
  activeSort: string;
  handleSort: (sortCategory: SortCategories) => void;
};

const sortArray = [
  { idx: 0, num: 0, title: "최신순", category: "recent" },
  { idx: 1, num: 1, title: "인기순", category: "popular" },
  { idx: 2, num: 1, title: "소식공유", category: "news" },
  { idx: 3, num: 2, title: "필요해요", category: "need" },
  { idx: 4, num: 3, title: "공유해요", category: "share" },
  { idx: 5, num: 0, title: "모든", category: "all" },
];

const SortButtons = ({ activeSort, handleSort }: SortButtonsType) => {
  function isSortCategory(category: string): category is SortCategories {
    return ["recent", "popular", "news", "need", "share"].includes(category);
  }

  return (
    <div
      style={{
        height: "7vh",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        marginBottom: "1vh",
        position: "fixed",
        top: "6vh",
        left: "7vw",
        backgroundColor: "#ffffff !important",
      }}
      css={UserCommunityBtnStyle}
    >
      {/* 순서버튼 */}
      {sortArray.map((item, idx) => {
        if (item.title === "모든") {
          return null;
        }
        return (
          <button
            key={idx}
            value={item.title}
            className={
              item.category === activeSort
                ? `sort-button-news active`
                : `sort-button-news`
            }
            onClick={() => {
              if (isSortCategory(item.category)) {
                handleSort(item.category);
              }
            }}
          >
            {item.title}
          </button>
        );
      })}
    </div>
  );
};

export default SortButtons;

1. 정렬 카테고리 정의

정렬 버튼의 카테고리와 해당 카테고리에 대한 정보를 담고 있는 sortArray 배열을 정의합니다. 각 객체는 버튼의 인덱스, 순서, 제목, 카테고리 등을 담고 있습니다.

2. 타입 선언

SortButtonsType 타입은 이 컴포넌트에 전달되는 props의 타입을 정의합니다.

  • activeSort: 현재 활성화된 정렬 카테고리를 나타내는 문자열입니다.
  • handleSort: 선택한 카테고리로 정렬을 처리하는 함수입니다.

3. 타입 가드 사용

isSortCategory 함수는 카테고리 문자열이 SortCategories 타입에 맞는지 확인하는 타입 가드입니다. 이는 컴파일 타임에 안정성을 제공합니다.

4. 정렬 버튼 렌더링

sortArray 배열을 반복하여 각 정렬 카테고리에 대한 버튼을 생성합니다. 사용자가 버튼을 클릭하면, 해당 카테고리로 정렬을 수행하는 handleSort 함수가 호출됩니다. 활성화된 카테고리는 activeSort 값과 비교하여 스타일을 다르게 적용합니다.

5. 스타일링

정렬 버튼을 수평으로 정렬하고, 위치 및 배경색 등의 스타일을 적용합니다.

디바운싱 검색어 처리

useDebounce hook을 사용하여 사용자가 검색어를 입력하는 동안 불필요한 API 호출을 방지합니다. 이는 성능을 향상시키고 서버에 대한 부담을 줄입니다.

const debouncedSearchTerm = useDebounce(search, 500);
import { useState, useEffect } from "react";

export default function useDebounce(value:string, delay:number) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

무한 스크롤 구현

useScrollIntercept hook을 사용하여 사용자가 페이지 하단에 도달할 때마다 새로운 게시물을 로드하는 무한 스크롤 기능을 구현하였습니다.

const loader = useScrollIntercept(loadMore);
import { useCallback, useEffect, useRef } from "react";

const useScrollIntercept = (loadMore: () => Promise<void>) => {
  const loader = useRef<HTMLDivElement>(null);

  const handleObserver = useCallback(
    (entities: any) => {
      const target = entities[0];
      if (target.isIntersecting) {
        loadMore();
      }
    },
    [loadMore]
  );

  useEffect(() => {
    const observer = new IntersectionObserver(handleObserver, {
      threshold: 0.5,
    });
    if (loader.current) {
      observer.observe(loader.current);
    }

    return () => observer.disconnect();
  }, [handleObserver]);

  return loader;
};

export default useScrollIntercept;

컴포넌트 구성

정렬 버튼, 게시물, 커뮤니티 하단 바 등의 컴포넌트를 분리하여 코드의 가독성과 재사용성을 높였습니다.

import { PostType } from "../../hooks/useLoadPosts";
import { useNavigate } from "react-router-dom";
import PostItem from "./PostItem";

export type PostTypes = {
  posts: PostType[];
};

const Post = ({ posts }: PostTypes) => {
  const navigate = useNavigate();

  const handleDetailNavigate = (id: string) => {
    navigate(`/user/community/${id}`);
  };

  return (
    <div
      style={{
        borderTop: "1px solid #adadad",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        marginBottom: "7vh",
        marginTop: "11vh",
      }}
    >
      {posts.map((item: any, index: number) => (
        <PostItem
          key={index}
          item={item}
          handleDetailNavigate={handleDetailNavigate}
        />
      ))}
    </div>
  );
};

export default Post;

기능

게시물 목록을 반복하여 각 게시물 항목을 렌더링합니다. 각 게시물은 PostItem 컴포넌트를 사용하여 표시됩니다.

주요 부분 설명

  • Props: posts 배열을 받아와 각 게시물의 정보를 표시합니다.
  • Navigation: useNavigate 훅을 사용하여 게시물 상세 페이지로 이동하는 기능을 합니다.
  • handleDetailNavigate: 게시물의 ID를 사용하여 해당 게시물의 상세 페이지로 이동하는 함수입니다.
  • 게시물 렌더링: posts 배열을 맵핑하여 각 게시물을 PostItem 컴포넌트로 렌더링합니다.
import { FaEye, FaRegComment } from "react-icons/fa";
import { getTimeAgo } from "../../utils/getTimeAgo";

export type PostTypes = {
  item: any;
  handleDetailNavigate: (id: string) => void;
};

const PostItem = ({ item, handleDetailNavigate }: PostTypes) => {
  return (
    <div
      style={{
        padding: "1rem",
        height: "140px",
        width: "90vw",
        borderBottom: "1px solid #adadad",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      }}
      onClick={() => handleDetailNavigate(item.userId)}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
        }}
      >
        <span
          style={{
            padding: "0.5rem 1rem",
            width: "4rem",
            height: "1.4rem",
            backgroundColor: "#EFEFEF",
            color: "#3b3b3b",
          }}
        >
          {item.category}
        </span>
        <span
          style={{
            fontSize: "1.4rem",
            fontWeight: "900",
            lineHeight: "2.5rem",
          }}
        >
          {item.title.slice(0, 15) + "..."}
        </span>
        <span
          style={{
            color: "#adadad",
            lineHeight: "1rem",
            fontWeight: "900",
          }}
        >
          {item.content.slice(0, 15) + "..."}
        </span>
        <span
          style={{
            color: "#adadad",
            lineHeight: "2rem",
            fontWeight: "900",
          }}
        >
          {item.region + " " + getTimeAgo(item.date)}
        </span>
      </div>
      <div
        style={{
          width: "24vw",
          height: "100%",
          display: "flex",
          alignItems: "flex-end",
          justifyContent: "end",
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            color: "#adadad",
            fontSize: "1rem",
          }}
        >
          <FaRegComment
            style={{
              fontSize: "1rem",
              color: "#adadad",
            }}
          />
          <div
            style={{
              marginLeft: "0.5rem",
              marginRight: "0.5rem",
            }}
          >
            {item.commentCount}
          </div>
          <FaEye
            style={{
              fontSize: "1.2rem",
              color: "#adadad",
            }}
          />
          <div
            style={{
              marginLeft: "0.5rem",
            }}
          >
            {item.hit}
          </div>
        </div>
      </div>
    </div>
  );
};

export default PostItem;

기능

개별 게시물 항목을 렌더링합니다. 제목, 내용 요약, 지역, 날짜, 댓글 수, 조회 수 등의 정보를 표시합니다.

주요 부분 설명

  • Props: item은 게시물의 상세 정보를 담고 있으며, handleDetailNavigate는 게시물을 클릭했을 때 상세 페이지로 이동하는 함수입니다.
  • 게시물 내용: 게시물의 카테고리, 제목, 내용, 지역, 날짜 등을 표시합니다.
  • 댓글과 조회 수: FontAwesome 라이브러리를 사용하여 댓글과 조회 수 아이콘을 표시하고, 해당 숫자도 표시합니다.
  • 클릭 이벤트: 게시물을 클릭하면, handleDetailNavigate 함수를 호출하여 해당 게시물의 상세 페이지로 이동합니다.

총평 및 향상된 부분

  • 사용자 경험 향상: 새로운 코드는 정렬, 검색, 페이지네이션 등의 동적 기능을 제공하여 사용자 경험을 향상시켰습니다.
  • 코드의 유지보수와 확장성 증가: 기능별로 컴포넌트와 훅을 분리하여 코드의 가독성과 재사용성이 향상되었습니다. 디바운싱과 무한 스크롤 등의 성능 최적화도 도입되었습니다.
  • 타입스크립트 적용: 타입스크립트를 통해 코드의 안정성을 높였으며, 개발 과정에서 타입 체크를 통해 오류를 미리 잡을 수 있게 되었습니다.

이러한 개선 사항들은 프로젝트의 품질을 높이고, 미래의 유지보수와 확장을 용이하게 하며, 사용자에게 더 나은 서비스를 제공할 수 있게 만듭니다.

profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글