왜 그동안 한번도 만들어보지 않았던거지?? 생각해보면..
mui 가 있기때문에 만들생각안하고 가져다 쓰기만 했다.
이번 기회에 이왕이면 커스텀으로 만들어보자.>!!!
동작예상:
- 사용자가 셀렉트 박스를 클릭하면 옵션이 드롭다운 형식으로 나타나고
- 옵션을 클릭하면 선택된 값이 업데이트되고 드롭다운이 닫힌다.
React 컴포넌트 설계
- 상태(State): 어떤 데이터가 동적으로 변경되는가?
- isOpen: 드롭다운 열림/닫힘 상태.
- selectedValue: 현재 선택된 값.
- Props: 외부에서 전달되는 데이터는 무엇인가?
- label, options, placeholder, value, onChange 등.
상태 관리
- isOpen:
- 사용자가 셀렉트 박스를 클릭하면 열림(true) 또는 닫힘(false) 상태로 변경.
- useState(false)를 통해 초기 닫힘 상태를 설정.
const [isOpen, setIsOpen] = useState(false); - selectedValue:
- 사용자가 옵션을 클릭하면 선택된 값을 저장.
- useState(value || "")로 초기값 설정.
const [selectedValue, setSelectedValue] = useState(value || "");
이벤트 처리
셀렉트 박스 클릭 이벤트
- SelectBox를 클릭하면 isOpen 상태를 반전시킴.
<SelectBox isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
옵션 클릭 이벤트
- 옵션을 클릭하면 selectedValue를 업데이트하고 드롭다운을 닫음.
if (onChange) onChange(value);
onChange라는 함수가 있을 때만 실행한다
onChange는 부모 컴포넌트에서 자식 컴포넌트로 전달된 콜백 함수입니다.
이를 통해 자식 컴포넌트(CustomSelect)가 부모 컴포넌트로 데이터를 전달할 수 있습니다.
onChange의 역할
- 값 전달
- CustomSelect 내부에서 사용자가 선택한 값을 부모 컴포넌트로 전달
- 부모 컴포넌트는 이 값을 받아 다른 작업(예: 상태 업데이트)을 수행
- 통신 역할
- 자식 컴포넌트(CustomSelect)는 setSelectedValue로 내부 상태를 업데이트
- 동시에 부모에게도 값이 전달되도록 onChange를 호출
동작 흐름
- 사용자가 "Banana"를 클릭:
- handleOptionClick("banana") 실행.
- 내부 상태 selectedValue를 "banana"로 설정.
- onChange("banana") 호출 → 부모의 handleSelectChange 실행.
- 부모에서 "Selected value in parent: banana" 출력.
왜 onChange가 필요한가?
React는 부모-자식 간 단방향 데이터 흐름을 사용합니다:
- 부모 → 자식: 부모가 데이터를 자식에게 전달 (예: options와 onChange).
- 자식 → 부모: 자식이 콜백(onChange)을 호출해 부모에게 데이터를 전달.
onChange 속성이 없는 경우, 부모 컴포넌트는 자식 컴포넌트(CustomSelect)에서 발생한 값 변경을 알 수 없습니다.
- 자식 컴포넌트(CustomSelect) 내부에서는 정상 동작.
- 선택된 값은 CustomSelect 내부에만 저장되고, 부모 컴포넌트는 이를 알지 못합니다.
JSX 구조
\
++++ 추가로 셀렉트 박스가 열렸을때 외부클릭으로 닫히게 해보자
셀렉트 박스 외부를 클릭했을 때 드롭다운이 닫히는 동작은, DOM 이벤트 리스너와 ref를 활용하여 외부 클릭을 감지하는 방식으로 구현된다.
1. useRef로 DOM 요소 참조
const selectRef = useRef<HTMLDivElement>(null);
- useRef는 DOM 요소를 직접 참조하기 위해 사용됩니다.
- selectRef는 <SelectContainer>를 참조하며, 컴포넌트 내의 특정 DOM 요소를 기준으로 외부 클릭을 감지합니다.
+ useRef는 React에서 제공하는 훅으로, 컴포넌트 안에서 DOM 요소나 상태를 직접적으로 참조하고 유지하기 위해 사용된다.
useRef가 하는 역할
1. 특정 DOM 요소를 직접 조작 ( DOM요소 클릭했는지, 포커스 설정했는지 etc)
2. useRef는 컴포넌트가 다시 렌더링되어도 값이 초기화되지 않고 유지
const selectRef = useRef<HTMLDivElement>(null);는 드롭다운 컴포넌트의 루트 요소를 참조하기 위해 사용되었습니다.
동작 방식
- 초기값
- useRef<HTMLDivElement>(null)의 초기값은 null입니다.
- React가 DOM 요소를 생성한 후, 해당 참조(selectRef.current)를 해당 DOM 요소로 업데이트합니다.
- DOM 요소 연결
- ref 속성을 <SelectContainer>에 전달합니다:+
-
tsx코드 복사<SelectContainer ref={selectRef}>
- React는 selectRef.current에 해당 DOM 요소를 할당합니다.
- 외부 클릭 감지
- selectRef.current.contains(event.target as Node)로, 클릭된 요소가 SelectContainer 내부인지 확인합니다.
2. mousedown 이벤트 리스너 추가
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
setIsOpen(false); // Select 외부를 클릭하면 드롭다운을 닫는다
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
- document.addEventListener:
- mousedown 이벤트는 사용자가 마우스를 누를 때 트리거됩니다.
- 이벤트는 최상위 요소인 document에 바인딩되어, 모든 클릭 이벤트를 감지합니다.
- event.target:
- 이벤트 객체의 target은 클릭된 요소를 나타냅니다.
- selectRef.current는 <SelectContainer> DOM 요소를 참조하므로, contains 메서드를 사용해 클릭된 요소가 컨테이너 내부인지 외부인지 확인합니다.
+handleClickOutside 함수
- 이 함수는 클릭 이벤트(mousedown)를 처리합니다.
- event.target: 사용자가 클릭한 DOM 요소를 나타냅니다.
- selectRef.current:
- selectRef는 <SelectContainer> 요소를 참조합니다.
- selectRef.current는 <SelectContainer>의 실제 DOM 노드를 가리킵니다.
- contains 메서드:
- selectRef.current.contains(event.target)는 클릭된 요소가 SelectContainer 내부에 포함되어 있는지 확인합니다.
- 내부에 포함되어 있다면 true, 그렇지 않으면 false를 반환합니다.
- setIsOpen(false):
- 드롭다운(isOpen)을 닫는 상태 변경 함수입니다.
- 클릭된 요소가 SelectContainer 외부에 있다면 드롭다운을 닫습니다.
3. 이벤트 리스너 등록
document.addEventListener("mousedown", handleClickOutside);
- document.addEventListener:
- mousedown 이벤트는 마우스 버튼을 누를 때 트리거됩니다.
- 이 코드는 전역적으로 모든 클릭 이벤트를 감지합니다.
.
4. useEffect의 Cleanup
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
- 컴포넌트가 언마운트될 때, 이벤트 리스너를 제거합니다.
- 리스너를 제거하지 않으면 메모리 누수와 불필요한 이벤트 호출이 발생할 수 있습니다.
전체 동작 순서
- 사용자가 화면의 어느 위치를 클릭합니다.
- mousedown 이벤트 리스너가 호출됩니다.
- 이벤트의 target이 <SelectContainer> 내부인지 외부인지 확인합니다:
- 내부라면 드롭다운 상태(isOpen) 변경 없이 유지.
- 외부라면 setIsOpen(false)로 드롭다운을 닫습니다.
- 드롭다운 닫힘 상태에 따라 화면이 업데이트됩니다.
왜 이렇게 구현해야 할까?
다른 방법의 한계
- CSS만으로 외부 클릭 감지 불가능:
- CSS는 DOM 요소 간의 관계(내부, 외부)를 직접적으로 확인할 수 없습니다.
- 외부 클릭 여부는 JavaScript로만 확인 가능합니다.
- onClick 이벤트만으로 한계:
- 단순히 onClick만 사용하면 내부 요소의 클릭과 외부 클릭을 구분하기 어려워집니다.
- 내부 클릭에서도 드롭다운이 닫힐 수 있는 부작용이 발생할 수 있습니다.
이 방식의 장점
- DOM 관계를 정확히 감지하여 내부와 외부를 구분합니다.
- 모든 DOM 요소와의 관계를 감지하므로, 복잡한 UI에서도 안정적으로 동작합니다.
- ref와 useEffect를 활용하여 React 컴포넌트의 상태 관리와 DOM 이벤트를 효율적으로 결합합니다.