티스토리 뷰

사이드바 메뉴 리팩토링

  • 파이널 프로젝트가 끝나고 틈틈이 리팩토링을 진행하고 있는데 단순히 하나의 tsx 파일에 들어있는 코드를 컴포넌트로 분리해서 가독성을 향상시키는 것 외에도 로직 자체를 변화시켜서 개선할 수 있는 부분도 많이 보였습니다.
  • 이번에는 사이드바 메뉴쪽을 개선시켜본 후에 정리를 해보았습니다.

 

개선 전 전체 코드

import defaultImage from "/images/cat.jpg";
import * as S from "./style";
import * as H from "../Header/styled";
import Logout from "../../assets/icons/Logout";
import product from "../../assets/product.svg";
import notice from "../../assets/notice.svg";
import clothes from "../../assets/clothes.svg";
import askQuestion from "../../assets/askQuestion.svg";
import magnifier from "../../assets/magnifier.svg";
import cart from "@/assets/cart.svg";
import managerIcon from "@/assets/managerIcon.svg";
import { useLocation, useNavigate } from "react-router-dom";
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import Backdrop from "./Backdrop";
import LoginModal from "../Modal_login/LoginModal";
import SignupModal from "../Modal_signup/SignupModal";
import { delCookie } from "@/utils/token";
import { IMG_URL } from "@/constants/constants";
import { LoginUserInfo } from "@/types/shared";
import { useRecoilValue } from "recoil";
import { userInfoState } from "@/recoil/atom";

interface Props {
  isOpen: boolean;
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
  toggleMenu: () => void;
  headerRef: React.MutableRefObject<HTMLDivElement | HTMLButtonElement | null>;
}

const HamburgerDropdown = ({
  isOpen,
  setIsOpen,
  toggleMenu,
  headerRef,
}: Props) => {
  const navigate = useNavigate();
  const path = useLocation().pathname;
  const barRef = useRef<HTMLDivElement | null>(null);
  const portalElement = document.getElementById("modal") as HTMLElement;
  const userInfo = useRecoilValue<LoginUserInfo | null>(userInfoState);
  const [loginModalOpen, setLoginModalOpen] = useState(false);
  const [signupModallOpen, setSignupModalOpen] = useState(false);

  const logoutHandler = async () => {
    if (confirm("로그아웃 하시겠습니까?")) {
      toggleMenu();
      await delCookie();
      if (path === "/") {
        window.location.reload();
      } else {
        window.location.href = "/";
      }
    }
  };

  const handleClickOutside = (e: React.BaseSyntheticEvent | MouseEvent) => {
    if (
      !headerRef.current?.contains(e.target) &&
      !barRef.current?.contains(e.target)
    )
      toggleMenu();
  };

  useEffect(() => {
    if (isOpen) {
      window.addEventListener("mousedown", handleClickOutside);
      return () => window.removeEventListener("mousedown", handleClickOutside);
    }
  }, [isOpen]);

  return (
    <>
      {createPortal(
        <>
          {isOpen && <Backdrop />}
          {loginModalOpen && (
            <LoginModal closeModal={() => setLoginModalOpen(false)} />
          )}
          {signupModallOpen && (
            <SignupModal closeModal={() => setSignupModalOpen(false)} />
          )}
          <S.SideBar isOpen={isOpen} ref={barRef}>
            <S.Profile>
              {!userInfo ? (
                <>
                  <div className="auth_btn">
                    <H.LoginBtn
                      onClick={() => {
                        setLoginModalOpen(true);
                        setIsOpen(false);
                      }}
                    >
                      LOGIN
                    </H.LoginBtn>
                    <H.SignupBtn
                      onClick={() => {
                        setSignupModalOpen(true);
                        setIsOpen(false);
                      }}
                    >
                      SIGN UP
                    </H.SignupBtn>
                  </div>
                </>
              ) : (
                <>
                  {userInfo.profile ? (
                    <img src={IMG_URL + "/" + userInfo.profile} />
                  ) : (
                    <img src={defaultImage} />
                  )}

                  <div className="profile_name">{userInfo?.name} 님</div>
                  <div></div>
                </>
              )}
            </S.Profile>
            {userInfo && (
              <>
                <S.MenuBox>
                  <S.Menu>
                    <li
                      onClick={() => {
                        navigate("/productlist");
                        toggleMenu();
                      }}
                    >
                      <img src={product} title="상품 보기" />
                      <h3 className="nav_text">상품 보기</h3>
                      <div className="nav_description">상품 보기</div>
                    </li>
                    {userInfo.role === "admin" ? (
                      <></>
                    ) : (
                      <>
                        <li
                          onClick={() => {
                            navigate("/collection");
                            toggleMenu();
                          }}
                        >
                          <img src={clothes} title="수거 요청" />
                          <h3 className="nav_text">수거 요청</h3>
                          <div className="nav_description">수거 요청</div>
                        </li>
                      </>
                    )}
                    <li
                      onClick={() => {
                        navigate("/notice");
                        toggleMenu();
                      }}
                    >
                      <img src={notice} title="공지사항" />
                      <h3 className="nav_text">공지사항</h3>
                      <div className="nav_description">공지사항</div>
                    </li>
                    <li
                      onClick={() => {
                        navigate("/questions");
                        toggleMenu();
                      }}
                    >
                      <img src={askQuestion} title="Q&A" />
                      <h3 className="nav_text">Q & A</h3>
                      <div className="nav_description">Q&A</div>
                    </li>
                    <li
                      onClick={() => {
                        navigate("/cart");
                        toggleMenu();
                      }}
                    >
                      <img src={cart} title="장바구니" />
                      <h3 className="nav_text">장바구니</h3>
                      <div className="nav_description">장바구니</div>
                    </li>
                    {userInfo.role !== "admin" ? (
                      <>
                        <li
                          onClick={() => {
                            navigate("/mypage");
                            toggleMenu();
                          }}
                        >
                          <img src={magnifier} title="마이페이지" />
                          <h3 className="nav_text">마이페이지</h3>
                          <div className="nav_description">마이페이지</div>
                        </li>
                      </>
                    ) : (
                      <li
                        onClick={() => {
                          navigate("/admin/products");
                          toggleMenu();
                        }}
                      >
                        <img src={managerIcon} title="관리자페이지" />
                        <h3 className="nav_text">관리자페이지</h3>
                        <div className="nav_description">관리자페이지</div>
                      </li>
                    )}
                  </S.Menu>
                </S.MenuBox>
                <div className="logout-icon">
                  <Logout logoutHandler={logoutHandler} />
                </div>
              </>
            )}
          </S.SideBar>
        </>,
        portalElement
      )}
    </>
  );
};

export default HamburgerDropdown;
  • 코드가 너무 길게 늘어져있어서 사이드바가 어떻게 생겨먹었는지 그려지지 않을뿐더러 반복적인 코드가 쓸데없이 많은 것 같네요!

 

컴포넌트 분리

  • 우선, 사이드바를 부분으로 나누어서 보면 이미지와 닉네임이 있는 프로필 부분과 메뉴 부분으로 나눌 수 있을 것 같습니다. 따라서 컴포넌트도 두 부분으로 분리하면 좋을 것 같다는 생각이 들었습니다.

Profile.tsx

import * as S from "./style";
import * as H from "../Header/styled";
import { LoginUserInfo } from "@/types/shared";
import { IMG_URL } from "@/constants/constants";
import defaultImage from "@/assets/icons/default_image.png";

interface ProfileProps {
  userInfo: LoginUserInfo | null;
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
  setLoginModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
  setSignupModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

const Profile = (props: ProfileProps) => {
  const { userInfo, setIsOpen, setLoginModalOpen, setSignupModalOpen } = props;
  return (
    <S.Profile>
      {!userInfo ? (
        <>
          <div className="auth_btn">
            <H.LoginBtn
              onClick={() => {
                setLoginModalOpen(true);
                setIsOpen(false);
              }}
            >
              LOGIN
            </H.LoginBtn>
            <H.SignupBtn
              onClick={() => {
                setSignupModalOpen(true);
                setIsOpen(false);
              }}
            >
              SIGN UP
            </H.SignupBtn>
          </div>
        </>
      ) : (
        <>
          {userInfo.profile ? (
            <img src={IMG_URL + "/" + userInfo.profile} />
          ) : (
            <img src={defaultImage} />
          )}

          <div className="profile_name">{userInfo?.name} 님</div>
          <div></div>
        </>
      )}
    </S.Profile>
  );
};

export default Profile;

HamburgerDropdown.tsx

<Profile
  userInfo={userInfo}
  setIsOpen={setIsOpen}
  setLoginModalOpen={setLoginModalOpen}
  setSignupModalOpen={setSignupModalOpen}
/>
  • 프로필 부분을 따로 컴포넌트화 한 뒤에 필요한 props를 넘겨주어서 비교적 간단하게 리팩토링을 할 수 있었습니다.

 

반복의 제거

  • 문제는 메뉴 부분인데
<li
  onClick={() => {
    navigate("/productlist");
    toggleMenu();
  }}
>
  <img src={product} title="상품 보기" />
  <h3 className="nav_text">상품 보기</h3>
  <div className="nav_description">상품 보기</div>
</li>
  • 메뉴마다 위 코드가 반복적으로 작성되는 것을 볼 수 있는데 중복 때문에 코드가 길어져서 가독성과 유지보수성을 헤치고 있는 것 같습니다.
  • 우선 반복되는 부분을 따로 분리해보았습니다.

SidebarMenu.tsx

import { useNavigate } from "react-router-dom";

interface SideBarMenuProps {
  url: string;
  imgSrc: string;
  title: string;
  toggleMenu: () => void;
}

const SidebarMenu = (props: SideBarMenuProps) => {
  const { url, imgSrc, title, toggleMenu } = props;
  const navigate = useNavigate();
  return (
    <li
      onClick={() => {
        navigate(url);
        toggleMenu();
      }}
    >
      <img src={imgSrc} title={title} />
      <h3 className="nav_text">{title}</h3>
      <div className="nav_description">{title}</div>
    </li>
  );
};

export default SidebarMenu;
  • 모든 메뉴는 위 컴포넌트로 찍어낼 수 있습니다.
  • 그런데 여기서 두 가지 문제가 발생합니다.

<유저가 보는 사이드바 메뉴>

 

<관리자가 보는 사이드바 메뉴>

 

  • 하나는 관리자와 유저가 보는 메뉴가 다르다는 점과 또 다른 하나는 메뉴의 순서가 정해져있다는 점입니다.
  const Menus = [
    {
      url: "/productlist",
      imgSrc: product,
      title: "상품 보기",
    },
    { url: "/collecton", imgSrc: clothes, title: "수거 요청",},
    {
      url: "/notice",
      imgSrc: notice,
      title: "공지사항",
    },
    {
      url: "/questions",
      imgSrc: askQuestion,
      title: "Q & A",
    },
    { url: "/cart", imgSrc: cart, title: "장바구니",},
    { url: "/mypage", imgSrc: magnifier, title: "마이페이지"},
    {
      url: "/admin/products",
      imgSrc: managerIcon,
      title: "관리자페이지",
    },
  ];
  • 따라서 위와 같이 Menus 배열을 만든 후 단순히 map 메소드를 이용하는 방법으로는 해결할 수 없었습니다. 유저와 관리자가 똑같은 메뉴를 보게될 것이니까요!
{userInfo.role !== "admin" ? (
                      <>
                        <li
                          onClick={() => {
                            navigate("/mypage");
                            toggleMenu();
                          }}
                        >
                          <img src={magnifier} title="마이페이지" />
                          <h3 className="nav_text">마이페이지</h3>
                          <div className="nav_description">마이페이지</div>
                        </li>
                      </>
                    ) : (
                      <li
                        onClick={() => {
                          navigate("/admin/products");
                          toggleMenu();
                        }}
                      >
                        <img src={managerIcon} title="관리자페이지" />
                        <h3 className="nav_text">관리자페이지</h3>
                        <div className="nav_description">관리자페이지</div>
                      </li>
                    )}
  • 기존 코드에서는 userInfo.role 을 기준으로 메뉴를 각각 작성해주었는데 지금은 반복되는 코드를 따로 빼주었으니 로직도 변화되어야겠죠!
  const Menus = [
    {
      url: "/productlist",
      imgSrc: product,
      title: "상품 보기",
      role: ["admin", "user"],
    },
    { url: "/collecton", imgSrc: clothes, title: "수거 요청", role: ["user"] },
    {
      url: "/notice",
      imgSrc: notice,
      title: "공지사항",
      role: ["admin", "user"],
    },
    {
      url: "/questions",
      imgSrc: askQuestion,
      title: "Q & A",
      role: ["admin", "user"],
    },
    { url: "/cart", imgSrc: cart, title: "장바구니", role: ["admin", "user"] },
    { url: "/mypage", imgSrc: magnifier, title: "마이페이지", role: ["user"] },
    {
      url: "/admin/products",
      imgSrc: managerIcon,
      title: "관리자페이지",
      role: ["admin"],
    },
  ];
  • Menus 배열에 role이라는 프로퍼티를 추가해보았습니다.
  • 만약 관리자만 보는 메뉴라면 ["admin"] 이고 유저만 보는 메뉴라면 ["user"], 관리자와 유저 모두에게 보이는 메뉴라면 ["admin", "user"] 를 갖게 됩니다.
<S.Menu>
    {Menus.filter(m =>
      m.role.includes(
        userInfo.role === "admin" ? "admin" : "user"
      )
    ).map(menu => (
      <SidebarMenu
        url={menu.url}
        imgSrc={menu.imgSrc}
        title={menu.title}
        toggleMenu={toggleMenu}
      />
    ))}
</S.Menu>
  • 그리고 사용할 땐 role을 기준으로 필터링을 먼저 해준 뒤 앞서 만든 SidebarMenu 컴포넌트를 사용하였습니다.
  • Menus에 정의된 순서를 지키면서 role에 따라 볼 수 없는 메뉴는 필터링 되게 됩니다.
  • 기존에 80줄이 넘었던 메뉴 부분을 분리된 컴포넌트의 코드까지 포함해도 40줄이 채 되지 않게 개선할 수 있었습니다.
  • 또한 메뉴가 추가되거나 수정되었을 때 스크롤을 내려서 해당 메뉴를 찾아서 수정할 필요없이 상단에 정의된 Menus 배열 안에서 손쉽게 추가, 수정할 수가 있게 되어 유지보수성도 매우 좋아진 것 같습니다!
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함