'use client';
import React, { useState } from 'react';
import { Icon } from '../Icon';
type Props = {
contents: string[];
size?: 'md' | 'lg';
placeholder?: string;
};
export function Select({ contents, size, placeholder }: Props) {
const [selectedContent, setSelectedContent] = useState(placeholder ? placeholder : contents[0]);
const [openSelect, setOpenSelect] = useState(false);
return (
<div
className={`${openSelect ? 'rounded-lg' : 'rounded-lg border border-gray-100'} text-b'${
size === 'md' ? 'md:min-w-26 w-fit min-w-24' : 'md:min-w-66 w-60'
} cursor-pointer border border-gray-200 bg-white text-start text-base font-semibold text-gray-400 md:text-lg`}
>
<div
className={`${
openSelect
? 'border-b-2 border-gray-100 hover:rounded-lg hover:rounded-b-none'
: 'border-0'
} flex justify-between ${
size === 'md' ? 'items-center py-1 pl-3 pr-2 text-sm' : 'px-5 py-2 md:py-3'
} hover:rounded-md hover:bg-gray-50`}
onClick={() => setOpenSelect(!openSelect)}
>
{selectedContent}
<Icon
name="arrowDown"
className={`transform transition-transform duration-300 ${
openSelect ? 'rotate-180' : ''
} ${size === 'md' ? 'w-5' : ''} }`}
/>
</div>
{openSelect && (
<div className="flex flex-col">
{contents.map((item, key) => (
<div
onClick={() => {
setSelectedContent(contents[key]);
setOpenSelect(!openSelect);
}}
className={`cursor-pointer border-gray-100 ${
size === 'md' ? 'px-3 py-1 text-sm' : 'px-5 py-2 md:py-3'
} last:border-none hover:bg-gray-50 hover:last:rounded-b-md`}
key={key}
>
{item}
</div>
))}
</div>
)}
</div>
);
}
Select
뿐만 아니라 GroupingComponent
도 있는데 groupName
만 추가된 형태임에도 새로운 컴포넌트를 만들어야됐습니다
Effective Component 강의 「지속 가능한 성장과 컴포넌트」를 보고 다음과 같은 인사이트를 얻었습니다.
변하는 것과 변하지 않는 것을 나누자.
데이터는 같지만 UI는 달라질 수 있다면 UI와 데이터를 분리하자.
useCalendar
)💡 변화하는 부분은
훅
, 보여지는 부분은컴포넌트
로 분리
하나의 역할을 가진 컴포넌트를 여러 개 조합해서 구성하자.
Trigger
Menu
Item
→ 이벤트 흐름: onClick
→ onChange
이렇게 하면 각 컴포넌트는 서로를 몰라도 되고, 독립적으로 재사용 가능
데이터 주입받는 방식도 컴포넌트처럼 분리해야 한다.
이 둘을 분리하면 외부 의존성 없이도 관리하기 쉬움
🔍 예: 인터페이스 정의부터 고민하기
만약 Select에 MultiSelect나 검색 기능이 추가된다면,
어디서부터 수정해야 할지 감이 잡히지 않았습니다.
구조가 확장에 취약하다는 점을 강하게 느꼈습니다.
변경에 유연한 구조가 진짜 ‘좋은 컴포넌트’다!
영상에서 dropdown컴포넌트를 다음과같이 구조화한 예시를 보여주었습니다
DropdownTrigger
: 열고 닫는 역할 Menu
: 리스트의 wrapper Item
: 실제 선택할 항목 onClick
→ onChange
변경된 컴포넌트는 서로의 존재를 몰라도 동작합니다.
그리고 이러한 구조는 확장성과 재사용성 유지보수성을 높입니다
“이 컴포넌트의 의도는 무엇인가?”
“기능보다 표현 방식이 더 중요할 수도 있다.”
💬 “리팩토링은 기능을 더하는 것이 아니라, 이해를 더하는 과정이다.”
useSelectMain
: 선택 상태 및 열림 여부 등 4가지 상태를 지역 상태로 관리합니다
selected
: 현재 선택된 항목
isOpen
: 드롭다운 열림 여부
setIsOpen
: 드롭다운 열고 닫기 제어
handleSelect
: 옵션 선택 핸들러
SelectMain
: UI 뼈대를 구성하고, context로 하위 컴포넌트에 상태 전달
SelectContext.Provider
: context를 통해 하위 컴포넌트로 상태 전파
useSelectContext
: 하위 컴포넌트들이 context 값을 받아 사용
구조로 나타내면 이렇게 표현할 수 있습니다
[useSelectMain] ← 지역 상태 생성
│
▼
[SelectMain] ← 상태를 context로 감싸서 하위에 전달
│
▼
[SelectContext.Provider] ← context 전파
│
▼
[useSelectContext()] ← 하위 컴포넌트가 context 값 사용
├── Option → onSelect, size
└── OptionList → size
└── TriggerButton → size,onSelect, selected
그리고 마지막으로는 만들어진 컴포넌트를 조립해서
각각의 컴포넌트처럼 사용할 수 있도록 만들었습니다
이번 리팩토링을 통해,
그동안 컴포넌트를 나눈다 = 파일/폴더 단위로 나눈다 라고만 생각했던 저 자신을 돌아보게 되었습니다.
컴포넌트는 단순히 폴더에 따라 분리하는 것이 아니라,
그 안에서도 역할(기능 vs UI) 기반으로 더 깊이 있게 나눌 수 있고,
필요한 기능을 조립해서 사용하는 ‘블록형 사고방식’이 가능하다는 것을 새롭게 체감했습니다.
하나의 Select 컴포넌트 안에서도
단순히 쓰기 좋게 만드는 게 아니라,
나중에 내가 다시 볼 때도 유용한 구조로 만드는 것이 진짜 설계라는 걸 느꼈습니다.
잘 만든 컴포넌트는 나에게 다시 되돌아온다 라는 생각이 가장 들었고
이 구조가 앞으로 제 프로젝트에서도 재사용될 수 있다는 생각에 설계의 중요성을 더 실감하게 됐습니다.
이번 경험을 바탕으로
에 더 깊이 고민하며 성장해 나가고 싶습니다.
https://www.youtube.com/watch?v=fR8tsJ2r7Eg&t=46s
https://velog.io/@aeong98/%EC%BB%B4%ED%8C%8C%EC%9A%B4%EB%93%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-Select-%EB%A7%8C%EB%93%A4%EA%B8%B0
https://ui.shadcn.com/docs/components/dropdown-menu