티스토리 뷰
개발냥이/타입스크립트(Typescript)
[리액트+타입스크립트] 프로젝트 리팩토링(refactoring)_사이드바(sidebar)
데브캣_DevCat 2023. 7. 31. 23:55사이드바 메뉴 리팩토링
- 파이널 프로젝트가 끝나고 틈틈이 리팩토링을 진행하고 있는데 단순히 하나의 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 배열 안에서 손쉽게 추가, 수정할 수가 있게 되어 유지보수성도 매우 좋아진 것 같습니다!
반응형
'개발냥이 > 타입스크립트(Typescript)' 카테고리의 다른 글
[타입스크립트] 인덱스 시그니쳐(Index signature) 적용하기 (0) | 2023.09.10 |
---|---|
디바운싱(Debouncing)과 쓰로틀링(Throttling) 정리 (0) | 2023.08.19 |
[Next.js + 타입스크립트] 무한 스크롤(Infinite scroll) 구현해보기(feat. Intersection observer) (0) | 2023.07.15 |
[리액트+타입스크립트] 이미지 업로드 구현 & 이미지와 콘텐츠 하나의 객체로 관리하기 (0) | 2023.07.12 |
[Next.js + typescript] 페이지네이션(Pagination) 구현해보기 (0) | 2023.07.10 |
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- java
- 리액트
- 스프링부트
- 자바
- JavaScript
- JPA
- 스프링
- 정렬
- SQL
- 자바스크립트
- DP
- 프로그래머스
- 해시맵
- 백준
- 형변환
- 이분탐색
- Nest
- Algorithm
- Spring
- Comparator
- 자바bfs
- SQLD
- dfs
- CS
- 알고리즘
- BFS
- 타입스크립트
- 자바dp
- 자바트리
- Queue
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
글 보관함