[React] Tree형 Sidebar 만들기 - 2
이전글 - [React] Tree형 Sidebar 만들기 - 1 (tistory.com)
#1. 최하위 노드(leaf)와 상위 노드(root) 분리
실제 화면 url을 가지고 있는 최하위 노드(leaf)와 상위 노드(root)를 분리해서 그려보자.
최하위 노드는 link를 연결해서 해당 메뉴로 이동할 수 있어야 하고, 상위 노드는 가지고 있는 하위 노드를 접었다 폈다 할 수 있어야 한다.
일단 메뉴를 열고 닫을 때 상태를 표현해줄 아이콘을 설치해보자
npm install react-icons
최하위 노드와 상위노드를 구분하는 방법은 childrens 가 있는가로 구분할 수 있으므로 아래와 같이 분리한다.
import {React} from 'react'
import {SbTitle, SbSub, SbLink} from './Sb.style'
import { HiChevronUp } from 'react-icons/hi'
const SbItem = ({ item, depth = 0 }) => {
const icon = <HiChevronUp />;
if(item.childrens.length > 0) {
return (
<div>
<SbTitle depth={depth}>[{depth}]{item.menuNm}{icon}</SbTitle>
<SbSub>
{item.childrens.map((child) => (
<SbItem item={child} depth={depth + 1} />
))}
</SbSub>
</div>
)
} else {
return (
<SbTitle depth={depth}>[{depth}]{item.menuNm}</SbTitle>
)
}
}
export default SbItem
items.childrens.length 가 0보다 크면 하위노드가 있는 것이고, 그런 경우에만 메뉴명 옆에 icon을 달아주고 하위 메뉴를 로딩해주면 된다.
반대의 경우에는 하위 메뉴를 달아줄 필요가 없고 단지 메뉴명만 보여주면 된다.
상위 메뉴를 클릭하면 토글형식으로 메뉴를 열었다 닫았다 할 예정이므로, 현재 상태를 관리할 state를 생성하고
onClick이벤트로 클릭할 때마다 상태값을 반대로 변경해주기만 하면 된다.
또한 펼쳐진 상태에서는 아래를 향하는 아이콘, 접혀진 상태에서는 위를 향하는 아이콘으로 상태를 표시해준다.
물론 최하위메뉴에서는 아이콘을 표시하지 않는다.
import {React, useState} from 'react'
import {SbTitle, SbSub, SbLink} from './Sb.style'
import { HiChevronDown, HiChevronUp } from 'react-icons/hi'
const SbItem = ({ item, depth = 0 }) => {
const [collapsed, setCollapsed] = useState(false);
const icon = collapsed ? <HiChevronUp /> : <HiChevronDown />;
function toggleCollapse() {
setCollapsed(prevValue => !prevValue);
}
if(item.childrens.length > 0) {
return (
<div>
<SbTitle depth={depth} onClick={toggleCollapse}>[{depth}]{item.menuNm}{icon}</SbTitle>
<SbSub>
{item.childrens.map((child) => (
<SbItem item={child} depth={depth + 1} />
))}
</SbSub>
</div>
)
} else {
return (
<SbTitle depth={depth}>[{depth}]{item.menuNm}</SbTitle>
)
}
}
export default SbItem
자 이제 현재까지 상태를 화면으로 보자.
이제 진짜 접었다 폈다하기만 하면 될 것 같다.
#2. 메뉴 접었다 펴기
메뉴를 어떻게 접었다 펼까?
아까 useState로 관리하던 상태값에 따라서 하위 div의 높이를 0으로 조절했다가 원래상태로 돌리면 접었다 폈다 하는 것처럼 보이지 않을까?
그래서 하위 div에도 collapsed props를 전달해주고 sb.styles.js에서 해당 값을 받아서 처리하면 될 것 같다.
또한, leaf노드에는 link를 달아주어 메뉴이동을 할 수 있도록 수정한다.
import {React, useState} from 'react'
import {SbTitle, SbSub, SbLink} from './Sb.style'
import { HiChevronDown, HiChevronUp } from 'react-icons/hi'
const SbItem = ({ item, depth = 0 }) => {
const [collapsed, setCollapsed] = useState(false);
const icon = collapsed ? <HiChevronUp /> : <HiChevronDown />;
function toggleCollapse() {
setCollapsed(prevValue => !prevValue);
}
if(item.childrens.length > 0) {
return (
<div>
<SbTitle depth={depth} onClick={toggleCollapse}>[{depth}]{item.menuNm}{icon}</SbTitle>
<SbSub isOpen={collapsed}>
{item.childrens.map((child) => (
<SbItem item={child} depth={depth + 1} />
))}
</SbSub>
</div>
)
} else {
return (
<SbTitle depth={depth}>
<SbLink to={item.windowUrl}>[{depth}]{item.menuNm}</SbLink>
</SbTitle>
)
}
}
export default SbItem
import styled from "styled-components";
import { Link } from 'react-router-dom';
// 사이드바 전체를 감싸는 div
export const SbContainer = styled.div`
min-width: 16rem;
width: auto;
height: auto;
min-height: 70vh;
font-size: 14px;
`
// SbItem에서 하위메뉴들을 묶어줄 div
export const SbSub = styled.div`
overflow: hidden;
max-height: ${props => props.isOpen ? "100%" : "0"};
`;
// 메뉴명을 보여줄 div
export const SbTitle = styled.div`
display: flex;
align-items: center;
padding-left: ${props => (props.depth * 20)}px;
height: 32px;
&:hover {
background-color: #f6f6f2;
cursor: pointer;
border-right: solid 5px;
}
`;
// 제일 하위메뉴에서 클릭할 Link
export const SbLink = styled(Link)`
color: inherit;
text-decoration: inherit;
`;
최종적으로 적용된 화면을 보자.
원하는 대로 사이드바가 완성되었다.