♣ 리액트에서 SelectComponent 만들어 보기!
제가 포트폴리오를 작업하면서 <select>, <option>을 사용해야 되는 순간이 있었습니다..
하지만 기본으로 제공하는 select 태그는 커스텀이 조금 어렵다? 내 마음대로는 할 수 없다?라는 불편함이 있었어요! 그래서 저는 제가 커스텀 select를 만들기로 했습니다.
- 기본 태그 사용 시 제공되는 UI
<select>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
- 커스텀 UI
<div>
<label></label>
<ul>
{menuList.map((item) => (
<li></li>
))}
</ul>
</div>
단순하게 구조만 얘기한다면 위의 구조와 같고
나는 React + TypeScript + scss를 사용해서 작업을 진행하였다.
만들어진 진행 순서대로 나열하기
1. 구조 만들기 (위와 동일)
2. 스타일링 하고 적용
- 스타일의 경우 scss를 사용할 건데 같은 클래스명이나 태그가 있다고 해도 딱 해당 컴포넌트에서만 사용할 수 있도록 module화를 진행했다
// 파일명 : select.module.scss
// 최상위 div
.select-con {
position: relative;
z-index: 1000;
width: 100%;
min-width: 200px;
padding: 0.5rem;
border-radius: 12px;
border: 1px solid #fff;
align-self: center;
cursor: pointer;
background: #fff;
color: #333;
// 최상위 div의 before
&::before {
content: '⌵';
position: absolute;
top: 1px;
right: 8px;
color: #333;
font-size: 1.25rem;
}
// div > label
.label {
font-size: 0.85rem;
margin-left: 4px;
text-align: center;
}
// div > ul
.list {
position: absolute;
list-style: none;
top: 45px;
left: 0;
width: 100%;
overflow: hidden;
overflow-y: scroll;
max-width: 0;
padding: 0;
// height: 100px;
border-radius: 7px;
background-color: #fff;
color: #333;
// div > ul.on
&.on {
max-width: none;
}
// div > ul > li
.select {
font-size: 0.85rem;
padding: 0.85rem 0.5rem;
transition: background-color 0.2s ease-in;
// div > ul > li:hover
&:hover {
background: #efefef;
color: #000;
}
// div > ul > li.active
&.active {
color: #00b2ff;
}
}
}
위와 같이 scss를 사용해서 스타일링을 해주고 모듈화를 했기 때문에 아래 코드와 같이. tsx에 적용!
<div className={tm(`${styles['select-con']}`)}>
<label className={tm(`${styles.label}`)}>선택하세요</label>
<ul className={tm(`${styles.list}}>
{menuList.map((item) => (
<li
key={""}
className={tm(`${styles.select}`,
)}
>
옵션1
</li>
))}
</ul>
</div>
3. 기능 적용
- useState를 사용해서 열고 닫히는것과 같은 로직 생성 (사실 그냥 클래스명 붙였다 떼었다 하는 거예요)
4. 타입 지정하기
-저는 이걸 만들 때 어디다가 가져다 써도 괜찮은 컴포넌트를 만드는 게 목적이었기 때문에 그러면? 일단 외부에서 props를 받아와야 재사용성이 높아질 테니 MenuList, setState는 무조건 받아와야겠다라고 생각하고 분할하였는데 조금 더 생각해 보니 그럼 타입값이 Select가 사용되는 곳에 따라서 다를 텐데라고 생각이 들다 보니 여기서 고민을 하다가 예전에 배워뒀던 제네릭이라는 거 생각나서 적용을 했습니다.
📍제네릭이란?
- 타입을 마치 함수의 파라미터처럼 사용할 수 있는 것을 의미
어차피 menuList랑 setState는 같은 타입을 가질 거 같은 거예요..!!! 앗 그래 그럼 1개만 외부에서 타입값을 받아오면 되겠다 싶어서 타입을 지정하고 근데? 라벨이나 벨류가 물론 보편적으로 사용되는 것들이 있지만 이쪽에서는 menu.label 다른 곳은 menu.title 이런 식으로 선택값이 다를 수 있기 때문에 menu만 전달되면 외부에서 필요값을 선택할 수 있게 콜백함수를 통해 라벨과 벨류값을 핸들링하기로 생각했고 만들었습니다...
최최종!!
import { useEffect, useState } from 'react';
import { tm } from '../../utils/twMerge';
import styles from './style/select.module.scss';
// 외부에서 받아오는 타입<T>을 통해 셀렉트에서 사용 될 객체의 타입지정
interface SelectProps<T> {
menuList: T[];
setState: React.Dispatch<React.SetStateAction<T | null>>;
defaultValue: T;
getLabel: (item: T) => string; // 각 아이템에서 표시할 텍스트를 추출하는 함수
getValue: (item: T) => string | number; // 각 아이템의 고유 값을 추출하는 함수
}
const Select = <T,>({ menuList, setState, defaultValue, getLabel, getValue }: SelectProps<T>) => {
// 열고 닫기
const [isOpen, setIsOpen] = useState<boolean>(false);
// 현재 선택 된 메뉴
const [selectedItem, setSelectedItem] = useState<T | null>(null);
// 초기값 설정
useEffect(() => {
if (defaultValue) {
setSelectedItem(defaultValue);
} else if (menuList.length > 0) {
setSelectedItem(menuList[0]);
}
}, [defaultValue, menuList]);
// 선택 메뉴 변경 핸들러
const handleChangeSelectValue = (e: React.MouseEvent<HTMLLIElement, MouseEvent>, item: T) => {
e.stopPropagation(); // 이벤트전파 방지
setSelectedItem(item); // 변경 된 아이템 (내부)
setState(item); // 변경 된 아이템 (외부)
setIsOpen(false); // 옵션창 닫기
};
return (
<div className={tm(`${styles['select-con']}`)} onClick={() => setIsOpen((prev) => !prev)}>
<label className={tm(`${styles.label}`)}>{selectedItem ? getLabel(selectedItem) : '선택하세요'}</label>
<ul className={tm(`${styles.list} ${isOpen ? styles.on : ''}`)}>
{menuList.map((item) => (
<li
key={getValue(item)}
className={tm(
`${styles.select}`,
selectedItem && getValue(selectedItem) === getValue(item) && styles.active
)}
onClick={(e) => handleChangeSelectValue(e, item)}
>
{getLabel(item)}
</li>
))}
</ul>
</div>
);
};
export default Select;
// 셀렉트 사용페이지
<Select<ProjectType>
menuList={projects} // 메뉴리스트
setState={setSelectProject} // 변경될 메뉴리스트 담을 state
defaultValue={projects[0]} // 기본값
getLabel={(project) => project.title} // ProjectType의 title을 label로 사용
getValue={(project) => project.title} // ProjectType의 title을 value로 사용
/>
최종적으로 위와 같은 코드가 만들어졌고 위와 같은 UI가 생성됐다!
제네릭을 이론적으로만 알고 실제로 사용해 본건 처음이라 처음에 되나 안 되나 어안이 벙벙했지만...? 만들었고 제대로 작동이 되는 것으로 완료!!
휴.. 다음에 또 계속 만들어야할테니 지속적으로 기록을 남겨놔야겠어영 안까먹게 ㅎㅎ