[21/07/19] 업체 탐색 설명 모달 리팩토링
드랍다운 리스트
드랍다운 리스트 포지셔닝 변경
return (
<>
{type === 'quantity' ? (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{l}
</DropDownItem>
))}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
) : (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{type === 'hashtags' && list && '*중복 선택 가능'}
{list ? (
list.map((l) => (
<DropDownItem
key={l.name}
onClick={(e) => onClick({ e, value: l })}
>
{l.name}
</DropDownItem>
))
) : (
<None select={select}></None>
)}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
)}
</>
);
return (
<>
{type === 'quantity' ? (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{l}
</DropDownItem>
))}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
) : (
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
{focus[type] && (
<DropDownList>
{type === 'hashtags' && list && '*중복 선택 가능'}
{list ? (
list.map((l) => (
<DropDownItem
key={l.name}
onClick={(e) => onClick({ e, value: l })}
>
{l.name}
</DropDownItem>
))
) : (
<None select={select}></None>
)}
</DropDownList>
)}
<Icon icon="Arrow" size={10} />
</Wrapper>
)}
</>
);
매우 가독성이 떨어지는 코드이다. DropDownList
가 FilterItem
의 자식컴포넌트로 들어가있는 모습을 확인할 수 있는데, 이는 드랍다운의 위치를 잡아주기위하여 그렇게 한것이다.
하지만 이렇게 자식으로 들어가게되면 FilterItem
에 overflow-x:scroll
을 추가해줘야하는 경우 드랍다운이 밑으로 가려지게됨.
그래서 DropDownList
를 FilterItem
의 자식이 아닌 형제로 두기로 결정함. 이렇게 되면
위 빨간 사각형을 컨테이너 블록으로 인식하여 위치가 제대로 잡히지 않게되는데, 이를 제대로 구현하기위해 우선 getBoundingClientRect
메소드를 이용하기로 하였다.
const onClickFilterItem = (e) => {
const { x, y } = e.currentTarget.getBoundingClientRect();
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
getClientBoundingRect
는 뷰포트로부터 x,y
좌표값을 구해준다. position
이 뷰 포트기준이라면 top:y
,left:x
값을 줌으로서 위치를 기존과 같이 FilterItem
바로 밑에 드랍다운이 켜지도록 할 수 있다.
하지만 그렇게 하기위해서 우선 컨테이너 블록을 뷰포트로 인식하게 해야한다. 지금의 코드는
///Filter
position:relative;
///DropDownItem(Filter의 직계자식)
position:absolute;
이기 때문에 컨테이너 블록을 뷰포트가 아닌 Filter
로 인식하게된다. 따라서 뷰포트로 부터의 좌표값을 구해도 Filter
의 뷰포트로부터 좌표를 빼준다음 top,left
프로퍼티에 값을 넣어줘야하는 복잡함이 생김.
그렇기에 이 쓸모없는 구조를 바꾸고자 뷰포트를 부모로 인식하는 position:fixed
를 사용하고자 하였다. DropDownList
에 position:fixed
를 주었지만 제대로 동작하지않았다. 그 이유는 다음과 같다
단, 요소의 조상 중 하나가 transform, perspective, filter 속성 중 어느 하나라도 none이 아니라면 (CSS Transforms 명세 참조) 뷰포트 대신 그 조상을 컨테이닝 블록으로 삼습니다.
const Wrapper = styled.div`
// ExplorePick Component
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
//...
Filter
의 바로위 조상 ExplorePick
컴포넌트가 transform
속성을 사용고 있었기 때문. 뷰포트 대신 ExplorePick
을 컨테이너 블록으로 삼아버림. 위 코드는 모달을 정 중앙에 위치하게 하기위한 코드인데 이 코드를 다음과 같이 바꾸었다.
// ExplorePick Component
const Wrapper = styled.div`
position: fixed;
// auto auto를 하게되면 위아래좌우가 같은 비율로 마진값을 갖게됨.
margin: auto auto;
// 위치를 0 0 0 0 으로 잡고
left: 0;
right: 0;
top: 0;
bottom: 0;
// 명시된 너비와 높이값을 갖고있다면
// 정중앙에 오게된다.
width: 80%;
height: 307px;
min-width: 266px;
min-height: 307px;
이렇게 하면 transform
속성없이 모달 내부의 content
를 정중앙에 배치할 수 있고, 앞선 DropDownList
도 컨테이너 블록을 뷰포트로 삼고 배치할수있게된다. 다음은 완성된 FilterItem
이다.
// ...
// FilterItem
function FilterItem({ select, current, list, onClick, type, focus, setFocus }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const onClickFilterItem = (e) => {
// 뷰포트로 부터 좌표를 구한후
const { x, y } = e.currentTarget.getBoundingClientRect();
// 이를 상태에 집어넣는다.
setPosition({ x, y });
setFocus((prev) => {
Object.keys(prev).forEach((key) => {
if (key !== type) prev[key] = false;
});
return {
...prev,
[type]: !prev[type],
};
});
};
return (
<>
<Wrapper focus={focus[type]} onClick={onClickFilterItem}>
{current}
<Icon icon="Arrow" size={10} />
</Wrapper>
{focus[type] && (
// 그 상태값을 켜지는 드랍다운리스트에 전달해준다.
<DropDownList
position={position}
list={list}
onClick={onClick}
select={select}
type={type}
></DropDownList>
)}
</>
);
}
드랍다운 리스트 분리하기(리팩토링)
// 종류나 해시태그 필터를 선택하였을때 품목이나 종류가 선택되지 않았으면
// 보여주는 컴포넌트
function None({ select }) {
// useRef는 재 렌더링 시 새롭게 계산한다.
const needType = useRef(select.item ? '종류' : '품목');
return (
<NoneWrapper>
<NoneText>
{needType.current}을 먼저 선택해야 해시태그를 선택할 수 있어요!
</NoneText>
<Button
variant="primary"
fontWeight="400"
borderRadius="10px"
width="70px"
height="38px"
fontSize={'14px'}
>
{needType.current} 선택
</Button>
</NoneWrapper>
);
}
None.propTypes = {
select: PropTypes.object,
};
function DropDownList({ position, list, onClick, type, select }) {
// list가 없다면 None컴포넌트를 보여준다. DropDown 컴포넌트는 위치를 잡게 해주는 컴포넌트
if (!list) {
return (
<DropDown top={position.y} left={position.x}>
<None select={select}></None>
</DropDown>
);
}
return (
// list도 있는 경우 다음의 DropDownList를 렌더링한다.
<DropDown top={position.y} left={position.x}>
{type === 'hashtags' && list && <span>*중복 선택 가능</span>}
{list.map((l) => (
<DropDownItem key={l} onClick={(e) => onClick({ e, value: l })}>
{type === 'quantity' ? l : l.name}
</DropDownItem>
))}
</DropDown>
);
}
DropDownList.propTypes = {
position: PropTypes.object,
list: PropTypes.array,
onClick: PropTypes.func,
type: PropTypes.string,
select: PropTypes.object,
};
export default DropDownList;
드랍다운 리스트 외부를 클릭하면 꺼지게 하기
우선 드랍다운리스트는 필터아래 필터아이템컴포넌트에 속해있다. 즉, 필터아래 직계 자식이다. 외부를 클릭하면 꺼지게 하는 기능은 이벤트 버블링을 이용하여 구현하였다.
// Filter 컴포넌트
useEffect(() => {
// outside click => off dropdownlist
const pageClickEvent = (e) => {
if (filterRef.current !== null && !filterRef.current.contains(e.target)) {
setFocus({
item: false,
kind: false,
hashtags: false,
quantity: false,
});
}
};
if (Object.values(focus).includes(true)) {
window.addEventListener('click', pageClickEvent);
return () => {
window.removeEventListener('click', pageClickEvent);
};
}
}, [focus, filterRef]);
이벤트는 타겟에서 시작하여 window
까지 버블링되어 진행된다. 클릭하여 focus[type]
이 true
, 즉 드랍다운리스트가 켜지게되면 window
에 클릭 이벤트 핸들러를 걸어준다. 이 핸들러는 내가 누른 요소가 Filter
컴포넌트의 자식인지 아닌지를 체크한다.
Filter
컴포넌트의 외부를 누르면 Filter
컴포넌트의 자식이 아니게 되므로 focus
의 모든 프로퍼티 값을 false
로 바꾸어 드랍다운 리스트가 꺼지게한다.
처음에는 window
에 이벤트 핸들러를 걸어줄 생각을 못하고 ExplorePick
에 이벤트 핸들러를 걸어주어 위 로직을 수행하려고 하였다. 하지만 이렇게 되면 focus
드랍다운을 켜주는 상태값을 ExplorePick
에서 관리해주어야하는데, 이는 컴포넌트의 의미 상 맞지 않고 아토믹 디자인 패턴에도 어긋나게된다.(확장성에 좋지 않다고 판단하였음)
그래서 ExplorePick
에 핸들러를 주기보단 window
에 핸들러를 걸어 처리하도록하였다.
팁
위 디자인처럼 style
을 추가해주기 위해 span
으로 감싼 뒤 다음의 선택자로 스타일링
& > span:first-child + ${DropDownItem} {
/* span이 첫번째 자식인 경우 그 DropDownItem은 다음 스타일을 입힌다.*/
border-top: 1px solid ${(props) => props.theme.whiteColor_3};
}
굳이 span
으로 감싸지 않아도 스타일링을 할 수는 있으나, 태그로 감싸는게 명확해서
Author And Source
이 문제에 관하여([21/07/19] 업체 탐색 설명 모달 리팩토링), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@rat8397/210719-업체-탐색-설명-모달-리팩토링저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)