하루에 하나씩 작은 컴포넌트를 만들고 기존코드를 분해해서 단순화하는 업무를 맡았다. 취업 전에는 미니프로젝트를 만들면서 다양한 라이브러리나 기술을 경험하면서 대략적인 개발과정을 파악했다면, 취업 후에는 실질적으로 커스텀이 필요한 입력폼 관련 ui등을 만드는 업무를 하게 되었다.
현재 맡게된 프로젝트는 하나의 컴포넌트가 상당히 비대하고 서비스로직과 단순 ui 코드들이 혼재되어있어 파악이 어려웠다. 프로젝트의 ui가 버전업 되면서 다양한 기능이 추가되고 삭제되기를 반복하면서 기존에 있던 데드코드들을 삭제하고 좀더 나은 프론트코드를 만들기 위해 ui단을 분리하고 있다.
- 드롭다운 메뉴만들기
- react-transition-group으로 하위메뉴에서 트랜지션하는 기능만들기
open의 상태에 따라서 하위의 렌더링이 결정된다.
// nav
fnction NavItem(props) {
const [open, setOpen] = useState(false);
return (
<li>
<a href="#" className="icon-button" onClick={() => setOpen(!open)}>
{props.icon}
</a>
{open && props.children}
</li>
);
}
<NavItem>
{/** 하위에 있는 메뉴가 열립니다. **/}
<ul>
<li>메뉴1</li>
<li>메뉴2</li>
<li>메뉴3</li>
<li>메뉴4</li>
</ul>
</NavItem>
css transition 으로 렌더링을 판별하는 상태값 isOpen을 전달하고 classNames 값에 따라 트랜지션이 작동한다
function DropdownContainer(props) {
const divRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState(props.default);
return (
<div className="select" default={props.default}>
<span onClick={() => setIsOpen(!isOpen)}>{selected}</span>
<CSSTransition
nodeRef={divRef}
in={isOpen}
unmountOnExit
timeout={300}
classNames="drop"
>
<DropdownList ref={divRef}>{props.children}</DropdownList>
</CSSTransition>
</div>
);
}
// ref는 하위 컴포넌트에 전달할 수 없어서 forwardRef를 사용함
const DropdownList = React.forwardRef((props, ref) => {
return (
<ul ref={ref} className="dropdown-list">
{props.children}
</ul>
);
});
drop-enter {
opacity: 0;
transform: translate(-50%, -30px);
}
.drop-enter-active {
opacity: 1;
transition: opacity 300ms, transform 300ms;
transform: translate(-50%);
}
.drop-exit {
opacity: 1;
}
.drop-exit-active {
opacity: 0;
transition: opacity 300ms, transform 300ms;
}
부모 컴포넌트에서 state값으로 드롭다운의 현재 페이지를 저장한다. 그 후 자식 컴포넌트중 하나에서 페이지를 바꾸는 props (goToMenu) 값을 받아서 클릭시 goToMenu의 값으로 렌더링한다.
CSS Transition으로 서로가 mount/unmount를 교차하게 css 코드를 작성한다.
function DropdownMenu() {
const [activeMenu, setActiveMenu] = useState("main");
const [menuHeight, setMenuHeight] = useState(null);
const calcHeight = (el) => {
const height = el.offsetHeight;
setMenuHeight(height);
};
function DropdownItem(props) {
return (
<a
href="#"
className="menu-item"
onClick={() => props.goToMenu && setActiveMenu(props.goToMenu)}
>
<span className="icon-button">{props.leftIcon}</span>
{props.children}
</a>
);
}
return (
<div className="dropdown" style={{ height: menuHeight }}>
{/* 첫번째 메뉴 */}
<CSSTransition
in={activeMenu === "main"}
unmountOnExit
timeout={500}
onEnter={calcHeight}
classNames="menu-primary"
>
<div className="menu">
<DropdownItem goToMenu="setting">go to secondary</DropdownItem>
<DropdownItem>My setting</DropdownItem>
</div>
</CSSTransition>
{/* 두번째 메뉴 */}
<CSSTransition
in={activeMenu === "setting"}
unmountOnExit
timeout={500}
onEnter={calcHeight}
classNames="menu-secondary"
>
<div className="menu">
<DropdownItem goToMenu="main">first</DropdownItem>
<DropdownItem>My setting</DropdownItem>
<DropdownItem>My setting</DropdownItem>
<DropdownItem>My setting</DropdownItem>
<DropdownItem>My setting</DropdownItem>
<DropdownItem>My setting</DropdownItem>
<DropdownItem>My setting</DropdownItem>
</div>
</CSSTransition>
</div>
);
}
.menu-primary-enter {
transform: translateX(-110%);
}
.menu-primary-enter-active {
transform: translateX(0%);
transition: all var(--speed) ease;
}
.menu-primary-exit {
position: absolute;
}
.menu-primary-exit-active {
transform: translateX(-110%);
}
/****/
.menu-secondary-enter {
transform: translateX(110%);
}
.menu-secondary-enter-active {
transform: translateX(0%);
transition: all var(--speed) ease;
}
.menu-secondary-exit {
position: absolute;
}
.menu-secondary-exit-active {
transform: translateX(110%);
}