어디서부터 시작할까 막막하다가
일단 할 수 있는 정도에서 구상한 화면을 먼저 만들기로 했습니다.
가장 처음 만든 것은 사이드바입니다.
저는 고정된 사이드바로 크게 다섯가지 메뉴와 그 아래 세부메뉴로 구성하였습니다.
모든 메뉴가 다 오픈되어 있지 않은 대표적인 메뉴 다섯가지만 보이고 세부메뉴는 클릭하면 드롭다운으로 보일 수 있도록 하고싶었습니다.
결과물
간단히 과정을 복기해보겠습니다.
문제1. 클릭 이벤트 DOM 조작
위에 gif에서 확인 할 수 있듯이 메뉴 클릭 시 세부 메뉴 목록을 띄우도록 개발하고 싶었습니다.
처음에는 이벤트가 발생하는 엘리먼트를 찾아 classList에 직접 추가/제거 하려고 했습니다.
그러나 jsx 코드 내에서 getElementById를 사용하려니 뭔가 잘못되고 있는 느낌이 들고, 또 제가 원하는 대로 동작이 바로 실행되지 않았습니다. 그래서 구글에 'React 클래스 추가' 를 검색해보니 바로 아래글이 나왔습니다.
https://sylagape1231.tistory.com/59
[ReactJS] React로 요소에 클래스 추가 및 제거하기 (부제 : React에서 직접 DOM을 조작하지 않도록 하자
🚨 문제 상황 인스타그램을 클론 코딩하는 과정에서 바닐라 JS로 DOM 요소에 접근한 후 클래스를 추가하고 제거하는 데 classList의 add, remove 메소드를 활용하였다. 그러나 이 코드를 React로 변환하
sylagape1231.tistory.com
문제점
해당 블로그의 내용이 저와 동일한 문제가 생겼으며 React에서는 직접 DOM을 조작하면 안된다는 말과 함께 풀어나가는 것을 볼 수 있었습니다.
이유
리액트의 가상 DOM은 상태가 변경되거나 생명주기 메서드가 실행되는 등 DOM의 변경이 필요한 시점에 업데이트됩니다. 리액트는 가상 DOM을 업데이트한 후, 실제 DOM과 비교하여 차이점을 찾아 그 부분만 실제 DOM에 적용하는 Reconciliation(재조정) 과정을 거쳐 렌더링합니다. 이를 통해 전체 DOM을 다시 렌더링하는 대신, 필요한 부분만 효율적으로 업데이트하여 성능을 최적화할 수 있습니다.
그러나 만약 개발자가 직접 DOM을 조작하면, 브라우저의 실제 DOM과 리액트 가상 DOM 간에 불일치가 발생하여 예기치 않은 동작이나 충돌이 발생할 수 있습니다. 또한, React Hook을 이용하여 상태를 관리하는 방식과 직접 DOM을 조작하는 코드가 혼합된다면, 코드의 일관성이 떨어지고 관리가 복잡해질 수 있으며, 이는 유지보수성과 가독성을 저하시킬 수 있습니다.
이러한 이유로 인해 getElementById를 사용하여 클래스리스트를 수정할 생각을 한 것이 잘못된 방식이라는 것을 알았습니다. 대신해서 React Hook 중 useState로 상태를 관리하여 엘리먼트의 클래스를 지정해주는 방식이 생각났습니다.
수정 결과
1. 일부분의 코드만 가져오자면 가장 처음 클릭된 메뉴명을 저장하는 상태값을 생성하였습니다. => clickItem
2. MenuItem 컴포넌트를 생성할 때, isClicked에 위에서 저장된 clickItem과 현재 아이템의 텍스트 동일 여부에 대해 넘겨주었습니다.
const [clickItem, setClickItem] = useState('');
{
menus.map((item) => {
return (
<MenuItem
key={item.id}
text={item.text}
subMenus={item.subMenus}
onClick={onClickList}
isClicked={clickItem===item.text}
/>
);
})
}
3. MenuItem 컴포넌트 내에서 삼항연산자를 사용하여 props로 받은 클릭여부에 따라 클래스명을 지정하도록 하였습니다.
<ul className={`sub-menu ${isClicked ? 'open-sub-menu' : ''}`} >
4. open-sub-menu에 맞는 css를 적용하여 클래스명이 지정되었을 때, 서브 메뉴가 보일 수 있도록 해결하였습니다.
.menu .open-sub-menu {
height: auto;
max-height: 250px;
transition: 0.5s;
}
문제 2. transition과 height:auto
문제1과 동일하게 서브메뉴를 보여주는 과정에서 높이를 어떻게 지정해야 할 지 고민했습니다. 처음 height:auto를 사용했을 때, 각 서브메뉴의 사이즈대로 오픈이 됐으나 너무 뚝뚝 끊기게 나타나고 사라져서 transition을 적용하여 부드럽게 보여주고 싶었습니다.
문제점
아래와 같이 transition을 적용했으나 여전히 보여주고 사라지기만 했습니다.
.menu .open-sub-menu {
height: auto;
transition: 0.5s;
}
이유
height가 auto로 되어있으면 브라우저가 transition을 적용할 목표 값을 찾지 못하기 때문에 max-height를 사용해야 한다고 합니다.
결과
max-height로 최대 높이를 지정만 해주니 처음 있던 gif처럼 부드럽게 작동할 수 있었습니다.
.menu .open-sub-menu {
height: auto;
max-height: 250px;
transition: 0.5s;
}
최종 코드
sidebar.jsx를 먼저 작성하여 틀을 잡았습니다.
일단 메뉴 목록은 배열로 작성하여 고정시켜놓고 사용했습니다.
import './sidebar.css';
import MenuItem from "./menu-item.jsx";
import {useState} from "react";
const menus = [
{
id: "branch",
text: "지점 관리",
href: "/branch",
subMenus: [
{
id: "branch",
text: "지점 정보",
href: "/branch"
},
{
id: "product",
text: "상품 등록",
href: "/product"
},
{
id: "products",
text: "상품 조회",
href: "/products"
},
]
},
{
id: "user",
text: "직원 관리",
href: "/users",
subMenus: [
{
id: "user",
text: "직원 등록",
href: "/user"
},
{
id: "users",
text: "직원 조회",
href: "/users"
},
]
},
{
id: "member",
text: "회원 관리",
href: "/members",
subMenus: [
{
id: "member",
text: "회원 등록",
href: "/member"
},
{
id: "members",
text: "회원 조회",
href: "/members"
},
]
},
{
id: "session",
text: "수업 관리",
href: "sessions",
subMenus: [
{
id: "session",
text: "수업 등록",
href: "/session"
},
{
id: "sessions",
text: "수업 시간표",
href: "/sessions"
},
]
},
{
id: "mypage",
text: "마이페이지",
href: "me",
subMenus: [
]
}
]
const Sidebar = () => {
const [clickItem, setClickItem] = useState('');
const onClickList = (text) => {
console.log(text);
setClickItem(text);
}
return (
<div className='sidebar'>
<div className='title'>
<div className='title-logo'>F</div>
<div className='title-name'>FitnessManagement</div>
</div>
<ul className='menu'>
{
menus.map((item) => {
return (
<MenuItem
key={item.id}
text={item.text}
subMenus={item.subMenus}
onClick={onClickList}
isClicked={clickItem===item.text}
/>
);
})
}
` </ul>
</div>
);
}
export default Sidebar;
sidebar.css
.sidebar {
max-width: 250px;
background-color: white;
box-shadow: rgba(0,0,0,0.25) 0 0 5px;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
}
.sidebar .title {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.sidebar .title .title-logo {
border-radius: 10px;
background-color: cornflowerblue;
color: white;
font-size: 30px;
font-weight: bold;
padding: 0 8px;
}
.sidebar .title .title-name {
font-weight: bold;
font-size: 20px;
margin:auto;
}
.sidebar .menu {
margin-top: 0;
padding: 0;
}
.sidebar .menu .menu-item {
padding: 0;
margin: 0;
}
menu-item.jsx
import './menu-item.css';
import { MdArrowForwardIos } from "react-icons/md";
const MenuItem = ({text, subMenus, onClick, isClicked}) => {
const onClickMenu = (event) => {
onClick(event.target.innerText)
}
return (
<li className='menu'>
<div className='menu-item' onClick={onClickMenu}>
<span>{text}</span>
{
subMenus.length > 0 ?
<MdArrowForwardIos className={isClicked ? 'arrow-click' : 'arrow'}/> : ''
}
</div>
<ul className={`sub-menu ${isClicked ? 'open-sub-menu' : ''}`} >
{
subMenus.map((item) => {
return (
<li key={item.id}>
<a href={item.href}>{item.text}</a>
</li>
);
})
}
</ul>
</li>
);
}
export default MenuItem;
menu-item.css
li {
list-style: none;
}
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgb(226, 226, 226);
padding: 0 10px;
line-height: 70px;
color: black;
font-weight: 600;
text-decoration: none;
}
.menu-item .arrow {
transition: 0.5s;
}
.menu-item .arrow-click {
transform: rotate(90deg);
transition: 0.5s ease-in-out;
}
.menu .sub-menu {
font-size:14px;
max-height: 0;
transition: max-height 1s ease-in-out;
overflow: hidden;
padding: 0 10px;
background-color: rgb(226, 226, 226);
line-height: 50px;
}
.menu .sub-menu > li a {
color: black;
font-weight: 500;
text-decoration: none;
}
.menu .open-sub-menu {
height: auto;
max-height: 250px;
transition: 0.5s
}
'플젝..해보자..' 카테고리의 다른 글
[서브프로젝트] 첫째 주 기록 (0) | 2024.08.19 |
---|