create-react-app으로 오늘의 집 웹사이트를 클론하면서 막혔던 부분을 기록해본다.
이번 파트는 dropdown이다.
dropdown으로 리액트 이벤트를 구현하려했을때 처음 막히는 부분은
state를 어떻게 사용해서 dropdown이 사라지고 나타나는 걸 구현할 수 있을까였다.
js에서는 classlist.add()
classlist.remove()
를 이용해서 특정 클래스가 있을 때 max-height
값을 조절해주면 됐었는데, 리액트에서 state를 이용해서 하려니 머리가 멍해졌다.
컴포넌트에서 dropdown이 1개만 존재한다면, state가 1가지 부분만 고려하면 됐기 때문에 간단하게 처리할 수 있었다.
isOpen
이라는 state를 만들어 false
값을 가지게 했다.click
을 하면 해당 state의 값을 반대로 설정하게 한다.false
면 hidden
, true
면 빈 클래스를 반환했다.const [isOpen, setIsOpen] = usestate(false);
const dropdownClicked = () => {
setIsOpen(!isOpen)
}
<button onClick={dropdownClicked}>드롭다운</button>
<ul className={`${dropdownList} ${isOpen ? '' : 'hidden'}`}>
...
hidden
클래스에 max-height : 0
이라는 css가 적용되도록 하면
클릭 이벤트가 발생할 때마다 hidden
클래스가 toggle되면서
dropdown 메뉴가 나타났다 사라졌다 하는 모습을 볼 수 있다.
.hidden {
max-height : 0;
overflow : hidden;
}
.dropdownList {
max-height : 100px; // 해당 리스트 전체의 높이만큼 설정
transition : max-height 200ms ease-in-out;
}
만약 같은 컴포넌트 안에 dropdown이 2개 이상 존재하면 어떻게 될까
일일이 해당 state를 하나씩 만들어서 관리하고 싶지만,
open은 동일한데 dropdown만 달라지기에 따로따로 만들 필요가 있나 싶기도하고
각각의 드롭다운의 열림 여부를 객체로 만들어 state에 할당해보기로 했다.
const [opened, setOpened] = usestate({ community: false, store : false, interior : false})
이때 usestate
로 객체의 값을 변경할 때 주의를 할 필요가 있다.
const toggleMenu = (event) => {
const openNow = opened; // 이러지마요...
// const openNow = { ...opened };
...
// openNow 내부 값들 변경
...
setOpened(openNow)
}
처음에 새로운 객체에 기존 state의 reference 값을 할당해 버리는 바람에
직접적으로 opened 객체 내부의 값을 변경해 버린 꼴이 됐다.
그리고는 setstate()로 이전과 같은 reference 값을 전달했기 때문에
리렌더링이 되지 않아서 몇 번이고 코드를 다시보고 헤맸었던....
단순히 call by value
이 아니라 call by reference
이기 때문에
직접적으로 객체 내부에 접근해서 값을 수정하는 것이 아니라
새로운 객체를 생성해서 해당 값으로 업데이트해야
react에서 state 변경을 감지하고 렌더를 하는 것 같다.
const [opened, setOpened] = usestate({ community: false, store : false, interior : false})
const toggleMenu = (event) => {
const openNow = { ...opened }
for (let key in openNow) {
if (key === event.target.id) {
openNow[key] = !openNow[key];
} else {
openNow[key] = false;
}
}
setOpened(openNow);
};
<button onClick={toggleMenu}>커뮤니티</button>
<ul className={`${dropdownList} ${opened ? '' : 'hidden'}`}>
<button onClick={toggleMenu}>스토어</button>
<ul className={`${dropdownList} ${opened ? '' : 'hidden'}`}>
<button onClick={toggleMenu}>인테리어</button>
<ul className={`${dropdownList} ${opened ? '' : 'hidden'}`}>
클릭으로 dropdown이 열렸다 닫혔다가 가능해진 이후 시점에는
다른 곳을 클릭했을 때 (focus가 이동했을 때) dropdown이 닫히게 해야한다.
onBlur
이벤트리스너로 dropdown을 보이지 않게 처리하는 것은 간단하다
바로 onBlur
에 dropdown을 닫는 콜백함수를 주면 된다
<button onClick={toggleMenu} onBlur={toggleMenu}>커뮤니티</button>
<ul className={`${dropdownList} ${opened ? '' : 'hidden'}`}>
여기서 중요한 부분은 dropdown의 내부 요소로 focus가 이동할 때에는
dropdown이 닫히면 안된다는 점이다.
이걸 구현한다고 예전에 blur 이벤트를 사용하면서 고생한 기억이 있는데
우선 기억해야 할 것은 마우스 이벤트의 처리 순서이다
mousedown -> blur -> mouseup -> click
사용자가 마우스를 꾹 누르게 되면 focus이동이 발생하고,
이로 인해 blur 이벤트가 발생하게 된다.
그렇게 때문에 mousedown 이벤트에서 focus가 이동하지 않은 것으로 처리해주면
blur이벤트가 발생하지 않아 dropdown이 닫히는 것을 방지할 수 있다.
여기서 사용할 수 있는 것이 event.preventDefault()
이다.
mousedown이벤트에서 사용하면, default로 수행되는 focus 이동을 막을 수 있다.
const preventFocusMove = (event) => {
event.preventDefault(); // blur 이벤트를 방지할 수 있다
}
<button onClick={toggleMenu} onBlur={toggleMenu}>커뮤니티</button>
<ul className={`${dropdownList} ${opened ? '' : 'hidden'}`}
onMousedown={preventFocusMove}>