
Intro
동양농산 e-commerce를 개발하는 과정 중 Form에 필요한 컴포넌트를 하나하나 직접 React 컴포넌트로 만들고 있습니다.
개발 시 필요 기능을 정의하고, 구현 과정과 한계를 기록하고 습관화함으로써, 역할과 책임이 분명한 컴포넌트를 설계하고 빠르게 구현하는 실력을 키워나가고 있습니다.
이번
<Select>
컴포넌트 역시 가장 1차원적으로 빠르게 구현해본 후,
‘역할과 책임이 분명한 컴포넌트’, ‘재사용과 변경에 유연한 컴포넌트’ 등의 관점에서 해당 컴포넌트를 분석,고민해보고 개선된 <Select>
를 구현해보고자 합니다.Step1. 1차원 적인 <Select> 구현하기
a. 최초 빠른 구현
- Native Element(
<select>, <option>
)을 사용하여 기본적으로 내장된 기능을 활용하고, 추가 요구사항은 props로 받아 처리할 수 있도록 구현한다. - Interface
<Select label="대분류" placeholder="옵션을 선택해주세요" options=[{label:opt1 , value:1 }, ... ]} > interface Props { options: Array<{label: string, value: string}>; label?: string; placeholder?: string; icon?: Icon; size?: 'xs' | 'sm' | 'md' | 'rg' disabled?: boolean } function Select({ options, label, placeholder, icon, size, disabled }){ return ( <> <Text>{label}</Text> <StyledSelect defaultValue={defaultValue || ""}> <option value="" disabled style={{display:'none'}}> {placeholder} </option> {options.map((opt) => ( <option key={opt.value} value={opt.value}> {opt.label} </option> ))} {icon && <Icon ... />} </StyledSelect> </> ) } ...


🟡 default 값 지정 관련 이슈 (본문의 흐름과는 무관)
[문제 상황 / 원인 분석]
<select>
에서 기본값(defaultValue)를 props로 받아, 최초 렌더링 시 기본 값으로 옵션을 선택하기를 원하는 상황
// props.defaultValue = 2로 바나나를 기본 값으로 지정하기를 원함 <select value={props.defalultValue}> <option value="1">사과</option>) <option value="2">바나나</option> </select>
- vanillaJS에서는
selected
속성을 통해 기본 값을 설정할 수 있지만, React에서는value
를 통해 기본값을 설정할 수 있다. - 그러나,
value
를 통해 기본값을 설정 시, 해당 값이 읽기전용으로 취급되어 select컴포넌트에서 옵션을 변경 시에 반영되지 않는다. (즉, 값이 더 이상 변경되지 않아 되비제어 컴포넌트로 활용할 수 없다.)
React 렌더링 생명주기에서 폼 엘리먼트의 value 어트리뷰트는 DOM의 value를 대체합니다. 비제어 컴포넌트를 사용하면 React 초깃값을 지정하지만, 그 이후의 업데이트는 제어하지 않는 것이 좋습니다. 이러한 경우에 value 어트리뷰트 대신 defaultValue를 지정할 수 있습니다. (by React 공식문서)
[문제 해결]
- 기본값 설정을 위해서는 다음과 같은 2가지 방법의 해결책이 있었다.
1) 제어 컴포넌트 방식을 사용하는 것(
value
속성) 2) 완전한 비제어 컴포넌트로 사용하기 (defaultValue
속성)
비제어컴포넌트 방식으로
<Select>
를 구현 중이기에 두 번째 방법(defaultValue
)을 사용하였다.// props.defaultValue = 2로 바나나를 기본 값으로 지정하기를 원함 <select defaultValue={props.defalultValue}> <option value="1">사과</option>) <option value="2">바나나</option> </select>
- key 관련 이슈
- React에서 자식노드들을 처리하기 위해선 key를 지정하여 성능을 개선하도록 하고 있다.
- 이에 따라 uuid 패키지를 통해, 고유한 값을 생성하도록 하여, key문제를 해결하였다.
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성합니다.예를 들어, 자식의 끝에 엘리먼트를 추가하면, 두 트리 사이의 변경은 잘 작동할 것입니다.자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인합니다. 인덱스를 key로 사용 중 배열이 재배열되면 컴포넌트의 state와 관련된 문제가 발생할 수 있습니다. 컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용됩니다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀔 것입니다. (by React 공식문서)
import uuid from "react-uuid" ... <select key={uuid()} defaultValue={props.defalultValue}> <option value="1">사과</option>) <option value="2">바나나</option> </select>
한계점
- 확장이 불가능에 가까워, 변경에 대한 취약해짐
- 검색을 통한 옵션 선택, 객체형 value 처리 등과 같이 Native Element를 사용할 경우 구현이 어려워진다.
- style 처리에 대한 제약이 크다.
- Native Element를 사용하기 때문에,
<select>
에서 제공하는 스타일만을 사용할 수 있다는 제약
Step2. 문제 파악 후 <Select> 개선하기
Goal
합성 컴포넌트 패턴(Compound Component)를 통하여, 변경에 유연하도록 Select 컴포넌트의 구조를 나눈다.
이때 NativeElement가 아닌 유사한 인터페이스를 가지도록
<ul>
<li>
<button>
를 활용한다.Headless 하도록 구현하여, Select 컴포넌트의 style과 기능에 대한 책임을 분리한다.
What (기능 정의)
<Select>
요소의 기본 기능 및 추가 기능 요구사항은 다음과 같다.- 키보드/마우스 클릭을 통해 옵션 리스트 영역을 열고 닫을 수 있다. (Basic)
- 옵션 리스트가 열려있는 상태일 때, 선택 된 옵션은 focus되어야하고, 키보드로 옵션 탐색이 가능하다. (Basic)
- 옵션은 검색이 가능하다. (Advance)
- 옵션 리스트 클릭 시 api 호출을 통해 dynamic하게 받아올 수 있도록 한다. (Advance)
How (구현 관점)
- 키보드/마우스 클릭을 통해 옵션 리스트 영역을 열고 닫을 수 있다.
- 옵션 렌더링 여부를 결정할
open
상태가 필요
- 옵션 리스트가 열려있는 상태일 때, 선택 된 옵션은 focus되어야하고, 키보드로 옵션 탐색이 가능하다.
Select
컴포넌트에서 현재 선택된 옵션의 value가 무엇인지 알아야함- 각 옵션의 ref와 value를 상위컴포넌트에서 알 수 있어야한다.
(이슈)
- 왜 알아야하고, 어떻게 알 수 있을까?
- why? (비제어컴포넌트로 사용할 때 필요할 듯?)
- How?
- 인터페이스 구체화
<select>-<option>
컴포넌트들은 항상 부모자식 관계로 같이 사용되는 컴포넌트이다.- 따라서, 두 개 이상의 컴포넌트가 협력하는 구조를 가진 합성 컴포넌트(Compound Component)패턴으로
<Select>
를 구현하고자 한다
합성 컴포넌트(Compound Component) 패턴
<Select>를 Native Component가 아닌 확장가능한 Element로 구성할 때, 아래와 같은 구조로 인터페이스를 생각해 볼 수 있다.
<Select open={open} defaultValue="value"> <Select.Button> Option을 선택해주세요</Select.Button> <Select.OptionList> <Select.Option value="value1">label1</Select.Option> <Select.Option value="value2">label2</Select.Option> <Select.Option value="value3">label3</Select.Option> </Select.OptionList> </Select>
- 합성 컴포넌트 패턴은 두 개 이상의 컴포넌트간의 관계를 보다 유용하게 표현하기 위해 사용한다. 이를 위해 컴포넌트들간의 암묵적 공유상태(implicit sharing state)를 주로 사용합니다.
- <Select>는 가장 상위 컴포넌트로서, 컴포넌트에서 사용될 상태를 저장하고, 제어를 책임지는 컴포넌트이다.
- 현재 선택된 option의 값인
value
와 OptionList의open
상태를 가진다. - 하위 자식컴포넌트들과의 상태 공유를 위해 “Context API”를 사용한다.
Headless 하도록 구현하여, Select 컴포넌트의 style과 기능에 대한 책임을 분리한다.
- Emotion의 className을 각 합성컴포넌트의 prop으로 넘겨 연결해준다.
- 한계
- 합성컴포넌트임을 한 눈에 확인할 수 없어 가독성 측면에서 떨어지는 현상
이슈
- 비제어컴포넌트/제어컴포넌트에서의 사용상황 고려하기
useControllValue
Hooks 구현예정