FrontEnd

Select 컴포넌트 만들어보기

solytory 2025. 2. 22. 21:31

♣ 리액트에서 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가 생성됐다!

제네릭을 이론적으로만 알고 실제로 사용해 본건 처음이라 처음에 되나 안 되나 어안이 벙벙했지만...? 만들었고 제대로 작동이 되는 것으로 완료!!

 

 

휴.. 다음에 또 계속 만들어야할테니 지속적으로 기록을 남겨놔야겠어영 안까먹게 ㅎㅎ