프론트엔드

[부캠위키] React 에서 Custom Select 구현하기

CodeBoyEd 2021. 12. 10. 22:00

React에서 Custom Select 구현하기

👋 문제 제기


💡 React에서 Custom Select Component 구현 시 on/off state 관리, Position 설정, 클릭 및 스크롤 이벤트 관리를 어떻게 할 것인가?


Custom Select 를 만들기로 결심한 계기

<사진 1> 토글 예시

Select tag는 CSS 를 적용하는 것이 어렵습니다. 기본적은 Select tag는 브라우저 마다 다른 디자인을 보이며 특히 option 부분에는 CSS 를 적용하기 위해서 특별한 Plugin을 사용해야 합니다.

물론, 모바일의 경우에는 내장되어 있는 Select의 기능을 지원 받을 수 있지만, 저는 기능적인 이점 보다 디자인적인 통일감을 주고 싶었습니다.

때문에, 위와 같이 직접 디자인하고 같은 기능을 갖도록 구현하고자 했습니다.


👀 바쁘신 분들을 위한 세줄 요약

1. select 바로 아래에 투명한 레이어를 구현하여 외부 클릭 이벤트를 처리하고자 했습니다.
2. select position 설정에 어려움이 있어 click event bubbling 을 이용하여 구현했습니다.
3. useRef 를 활용하여 불필요한 렌더링이 일어나는 상황을 방지했습니다.


✏ 탐구 과정

처음 생각했던 Select 의 기능

  • Select는 첫 화면에서 렌더링 되지 않으며, 관련 토글을 클릭할 경우 on 상태가 됩니다.
  • Select는 on 상태일 때 렌더링이 되며, 가장 높은 z-index 값을 갖습니다.
  • Select 내부를 클릭할 경우, 토글 영역이 클릭 된 값으로 업데이트 되고 off 상태가 됩니다.
  • Select 가 on 상태일 때, 셀렉트를 제외한 모든 외부 영역을 클릭하면 off 상태로 변합니다.

첫번째 시도 - select 배경 만들기

가장 먼저 생각났던 방법은 토글을 클릭할 경우, state를 on으로 해주고 외부 영역을 클릭할 경우 off 를 해주는 방법이었습니다. 이를 위해서는 document 객체에 click 이벤트 걸고, select 내부 영역의 클릭일 경우에는 stopPropagation() 함수를 통해 이벤트 전파를 막아야 했습니다.

하지만 모던자바스크립트에 따르면, 특정 지역에서 이벤트 전파를 막는 것은 이후에 큰 오류를 초래할 수 있기 때문에, 지양하라는 글을 읽게 되었습니다. 그 이유는 이벤트 전파를 막게 되면 해당 영역은 일명 deadzone (데드존)이 되는데, 이후에 사용자의 행동 패턴을 감지하는 것과 같은 기능을 추가하거나 할 때, 찾아내기 힘든 오류를 만들 수 있다는 것 입니다.

때문에, 다른 방법을 고민하다가 select 가 on 될 때, select 바로 아래에 보이지 않는 투명한 영역을 같이 띄워주고 외부 영역에 클릭 이벤트가 감지될 경우 select를 off 시키는 로직을 생각하게 됐습니다.

하지만 위와 같은 로직을 구현하게 되면서 치명적인 2가지 문제점을 깨닫게 되었습니다.

가장 큰 문제는 select의 Position을 잡기가 어렵다는 점입니다. select는 가장 높은 z-index 값을 가져야하기 때문에, position을 absolute로 설정하고 z-index 값을 부여해야 합니다. 이때, select 는 자신을 on/off 시키는 토글 바로 아래쪽에 생겨야 하기 때문에, 토글 컴포넌트의 하위 컴포넌트로 배치를 하고, 토글 컴포넌트에 position: relative ( 또는 absolute ) 값을 부여해야합니다.

그래야 select 의 위치가 토글의 위치에 상대 값으로 설정이 됩니다. select가 토글 위치의 상대 값으로 설정되어야 하는 이유는, 브라우저의 크기가 변동 되어 토글이 위치가 변하더라도 select 의 위치가 함께 따라갈 수 있도록 하기 위함입니다. 또한, 하나의 select가 아닌 여러 군데에서 사용 될 select component를 구현하는 과정이기 때문에, 호출하는 토글의 위치 값을 아는 것은 중요합니다.

하지만 위와 같이 토글과 select 사이에 투명한 배경을 깔게 되면 배경에도 position 값을 설정해야 합니다. 이때 select가 토글의 위치를 상대 값으로 계산하는 것이 아니라 배경의 위치에 대한 상대 값을 계산하기 때문에 토글의 상대 값을 알아내는 것이 불가능하게 됩니다.

두번째 문제는 투명한 select 배경이 on 되어 있는 상태에서는 다른 컴포넌트의 hover 와 같은 속성이 꺼지는 것이었습니다.

두가지 문제를 겪으면서 외부에 배경 레이어를 만드는 것은 적합하지 않다고 판단했습니다.


두번째 시도 - event bubbling 활용

두번째 방법은 document 에 클릭 이벤트를 등록하는 이벤트 버블링을 이용한 방법이었습니다.

그러기 위해서는 클릭 된 영역을 식별할 수 있도록 id 값을 부여해야 했습니다. 식별 되어야 하는 아이디의 종류는 2가지였습니다.

  1. 클릭 된 부분이 select 내부의 영역인가, select on/off 하는 토글 영역인가, 그 외 영역인가
    • select 내부의 영역일 경우 아래의 식별 과정으로 이동한다.
    • 토글 내부의 영역일 경우에는 아래의 식별 과정으로 이동한다.
    • 그 외 영역일 경우에는 전체 토글의 state 를 off로 한다.
  2. 클릭 된 부분이 select 혹은 토글 영역일 경우 여러 개의 select (혹은 토글) 중에서 어떤 select (혹은 토글)인가 ex) 위 <사진 1> 에서 로그인 토글인가, 분류 토글인가
    • select 내부 영역일 경우 해당 select 종류에 맞는 로직을 수행한다.
    • toggle 내부 영역일 경우 다른 select 모두 off 하고 해당 select 만 on/off 한다. (on일 경우 off, off일 경우 on)

또한, select들의 on/off state와 토글 내부의 value state를 관리하기 위해 최상위 컴포넌트에서 useReducer hook과 context API를 사용했습니다. 하지만 여전히 문제가 존재했는데요 document에 click 이벤트를 걸다 보니 외부 영역을 click 할 때마다 불필요한 렌더링이 일어나는 것이었습니다. 또한, 마우스 scroll 이벤트에도 select 가 off 되도록 이벤트를 추가 등록했는데, 이 경우에도 scroll 마다 불필요한 렌더링이 발생했습니다.


불필요한 렌더링 최소화

불필요한 렌더링이 일어나는 과정을 나열해보겠습니다.

  1. 클릭 이벤트가 일어난다.
  2. Select 영역, Toggle 영역 둘 다 아닌 그 외 영역이 클릭 된 것으로 식별 된다.
  3. Select 이미 모두 off 상태인데, 다시 off 상태로 재정의 한다.
  4. state 가 재 정의 되었으므로 렌더링이 일어난다.

이와 같은 문제를 해결하기 위해, 처음에는 리액트에서 렌더링 최적화에 사용되는 useMemo, useCallback hooks API 를 사용하고자 했습니다. 하지만 useMemo 와 useCallback 은 메모리 영역을 사용하기 때문에, 비싼 계산이 아니라면 지양하라는 사실을 알게되었고, 우선적으로 고려될 사항이 아니라고 생각했습니다. 저는 3번에서 Select 가 모두 off 상태일 경우에는 state 를 재정의 하지 않도록 return 하는 조건문을 추가하고자 했습니다.

하지만, event 함수를 선언하는 시점에서 select on/off state 는 모두 off 였기 때문에, 이후에 state값이 바뀌더라도 반영이 되지 않는 상황이 발생합니다.

이와 같은 문제는 useRef hooks API 를 활용하여 해결했습니다. useRef.current 값을 기준으로 모든 select state가 off 인 경우에는 state 변경 이벤트가 일어나지 않도록 설정했습니다.

위와 같은 과정을 통해 custom select component를 구현할 수 있었습니다.

🙂 '나' 만의 결론

👍 깨달은 점


이벤트 버블링을 이용하게 될 경우, 모든 select, toggle 마다 state 관리 이벤트 등록을 하지 않아도 되므로 효율적인 방식이라고 생각합니다.

다만, document 에 등록되는 이벤트가 많아지게 된다면, 이벤트를 관리하는 체계적인 규칙이 필요할 것으로 예상됩니다.

👎 아쉬운 점


토글과의 상대 위치를 계산하는 select component 를 만들더라도, 부분 부분 css 값의 차이가 나기 때문에 이에 대한 처리를 해줘야 합니다. 저의 경우 css transform 값을 props로 정의하였습니다. 결국 상황에 맞는 세세한 css 값을 추가적으로 고려해야 했습니다.

이러한 부분도 설계 규칙을 만들어 토글의 크기를 계산하고 자동화할 수 있다면 훨씬 생산성 있는 component가 될 수 있을 것으로 기대합니다.

개선될 수 있는 부분은 지속적으로 리팩토링 및 업데이트하겠습니다.