드롭박스를 만들어보자.. +외부클릭으로 드롭다운 닫히는 동작

2024. 11. 28. 16:46·⚛️ React

왜 그동안 한번도 만들어보지 않았던거지?? 생각해보면..

mui 가 있기때문에 만들생각안하고 가져다 쓰기만 했다.

 

이번 기회에 이왕이면 커스텀으로 만들어보자.>!!!

 

동작예상: 

 

  • 사용자가 셀렉트 박스를 클릭하면 옵션이 드롭다운 형식으로 나타나고
  • 옵션을 클릭하면 선택된 값이 업데이트되고 드롭다운이 닫힌다.

React 컴포넌트 설계 

  • 상태(State): 어떤 데이터가 동적으로 변경되는가?
    • isOpen: 드롭다운 열림/닫힘 상태.
    • selectedValue: 현재 선택된 값.
  • Props: 외부에서 전달되는 데이터는 무엇인가?
    • label, options, placeholder, value, onChange 등.

상태 관리

  1. isOpen:
    • 사용자가 셀렉트 박스를 클릭하면 열림(true) 또는 닫힘(false) 상태로 변경.
    • useState(false)를 통해 초기 닫힘 상태를 설정.
     
    const [isOpen, setIsOpen] = useState(false);
     
  2. selectedValue:
    • 사용자가 옵션을 클릭하면 선택된 값을 저장.
    • useState(value || "")로 초기값 설정.
     
    const [selectedValue, setSelectedValue] = useState(value || "");
     

 이벤트 처리

셀렉트 박스 클릭 이벤트

  • SelectBox를 클릭하면 isOpen 상태를 반전시킴.
         
    <SelectBox isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
     

 

옵션 클릭 이벤트

  • 옵션을 클릭하면 selectedValue를 업데이트하고 드롭다운을 닫음.
 
const handleOptionClick = (value: string) => {
    setSelectedValue(value);  // 선택된 값을 상태에 저장
    setIsOpen(false);  // 드롭다운을 닫음
    if (onChange) onChange(value);  // 외부에서 전달된 콜백 함수 호출
  };
 

 

 if (onChange) onChange(value);  

onChange라는 함수가 있을 때만 실행한다

onChange는 부모 컴포넌트에서 자식 컴포넌트로 전달된 콜백 함수입니다.

이를 통해 자식 컴포넌트(CustomSelect)가 부모 컴포넌트로 데이터를 전달할 수 있습니다.

 

onChange의 역할

  1. 값 전달
    • CustomSelect 내부에서 사용자가 선택한 값을 부모 컴포넌트로 전달
    • 부모 컴포넌트는 이 값을 받아 다른 작업(예: 상태 업데이트)을 수행
  2. 통신 역할
    • 자식 컴포넌트(CustomSelect)는 setSelectedValue로 내부 상태를 업데이트
    • 동시에 부모에게도 값이 전달되도록 onChange를 호출

동작 흐름

  • 사용자가 "Banana"를 클릭:
    1. handleOptionClick("banana") 실행.
    2. 내부 상태 selectedValue를 "banana"로 설정.
    3. onChange("banana") 호출 → 부모의 handleSelectChange 실행.
    4. 부모에서 "Selected value in parent: banana" 출력.

왜 onChange가 필요한가?

React는 부모-자식 간 단방향 데이터 흐름을 사용합니다:

  1. 부모 → 자식: 부모가 데이터를 자식에게 전달 (예: options와 onChange).
  2. 자식 → 부모: 자식이 콜백(onChange)을 호출해 부모에게 데이터를 전달.
<CustomSelect
                label="selectbox"
                options={[
                    { value: "2024", label: "2024" },
                    { value: "2023", label: "2023" },
                    { value: "2022", label: "2022" },
                ]}
            />

onChange 속성이 없는 경우, 부모 컴포넌트는 자식 컴포넌트(CustomSelect)에서 발생한 값 변경을 알 수 없습니다.

 

  • 자식 컴포넌트(CustomSelect) 내부에서는 정상 동작.
  • 선택된 값은 CustomSelect 내부에만 저장되고, 부모 컴포넌트는 이를 알지 못합니다.

 

 

JSX 구조

 
return (
    <SelectContainer>
      {label && <Label>{label}</Label>}
      <SelectBox isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
        <SelectedValue>{selectedValue || placeholder}</SelectedValue>
        <DropdownIcon isOpen={isOpen}>∨</DropdownIcon>
        {isOpen && (
          <Options>
            {options.map((option) => (
              <Option
                key={option.value}
                onClick={() => handleOptionClick(option.value)}
              >
                {option.label}
              </Option>
            ))}
          </Options>
        )}
      </SelectBox>
    </SelectContainer>
  );
};
 

\

 

 

 

++++ 추가로 셀렉트 박스가 열렸을때 외부클릭으로 닫히게 해보자 

셀렉트 박스 외부를 클릭했을 때 드롭다운이 닫히는 동작은, 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);는 드롭다운 컴포넌트의 루트 요소를 참조하기 위해 사용되었습니다.

동작 방식

  1. 초기값
    • useRef<HTMLDivElement>(null)의 초기값은 null입니다.
    • React가 DOM 요소를 생성한 후, 해당 참조(selectRef.current)를 해당 DOM 요소로 업데이트합니다.
  2. DOM 요소 연결
    • ref 속성을 <SelectContainer>에 전달합니다:+
    • tsx
      코드 복사
      <SelectContainer ref={selectRef}>
    • React는 selectRef.current에 해당 DOM 요소를 할당합니다.
  3. 외부 클릭 감지
    • 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 함수

 
const handleClickOutside = (event: MouseEvent) => { if (selectRef.current && !selectRef.current.contains(event.target as Node)) { setIsOpen(false); } };
  • 이 함수는 클릭 이벤트(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);
};

 

 

  • 컴포넌트가 언마운트될 때, 이벤트 리스너를 제거합니다.
  • 리스너를 제거하지 않으면 메모리 누수와 불필요한 이벤트 호출이 발생할 수 있습니다.

 

전체 동작 순서

  1. 사용자가 화면의 어느 위치를 클릭합니다.
  2. mousedown 이벤트 리스너가 호출됩니다.
  3. 이벤트의 target이 <SelectContainer> 내부인지 외부인지 확인합니다:
    • 내부라면 드롭다운 상태(isOpen) 변경 없이 유지.
    • 외부라면 setIsOpen(false)로 드롭다운을 닫습니다.
  4. 드롭다운 닫힘 상태에 따라 화면이 업데이트됩니다.

왜 이렇게 구현해야 할까?

다른 방법의 한계

  1. CSS만으로 외부 클릭 감지 불가능:
    • CSS는 DOM 요소 간의 관계(내부, 외부)를 직접적으로 확인할 수 없습니다.
    • 외부 클릭 여부는 JavaScript로만 확인 가능합니다.
  2. onClick 이벤트만으로 한계:
    • 단순히 onClick만 사용하면 내부 요소의 클릭과 외부 클릭을 구분하기 어려워집니다.
    • 내부 클릭에서도 드롭다운이 닫힐 수 있는 부작용이 발생할 수 있습니다.

이 방식의 장점

  • DOM 관계를 정확히 감지하여 내부와 외부를 구분합니다.
  • 모든 DOM 요소와의 관계를 감지하므로, 복잡한 UI에서도 안정적으로 동작합니다.
  • ref와 useEffect를 활용하여 React 컴포넌트의 상태 관리와 DOM 이벤트를 효율적으로 결합합니다.

 

 

 

'⚛️ React' 카테고리의 다른 글

Firebase uid 로 리덕스 저장해서 해당user의 데이터 불러오기  (0) 2024.12.11
firebase + redux 로 상태관리 하기  (0) 2024.12.10
React-Portal로 Modal 구현하기  (0) 2024.11.27
[React] React-Hook-Form에 대해 알아보기  (1) 2024.11.12
Virtual DOM  (1) 2024.11.11
'⚛️ React' 카테고리의 다른 글
  • Firebase uid 로 리덕스 저장해서 해당user의 데이터 불러오기
  • firebase + redux 로 상태관리 하기
  • React-Portal로 Modal 구현하기
  • [React] React-Hook-Form에 대해 알아보기
gprorogpfus
gprorogpfus
:- >
  • gprorogpfus
    gpfusdldks
    gprorogpfus
  • 전체
    오늘
    어제
    • 분류 전체보기 (55)
      • 🎭JavaScript (2)
      • 👚 CSS (1)
      • ⚛️ React (13)
      • 🌃 Next.js (5)
        • 🔜 next.js-study (3)
      • 🥏TypeScript (10)
      • 🏴알고리즘 (2)
      • 🌴트러블슈팅 (3)
      • ⛲ 프로젝트 (6)
        • 👖gproro-shop-app (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    TypeScript
    GIT
    Redux
    react
    JavaScript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
gprorogpfus
드롭박스를 만들어보자.. +외부클릭으로 드롭다운 닫히는 동작
상단으로

티스토리툴바