기존에 작성한 Dropdown 코드의 문제점을 나열하자면 아래와 같다.
1. 도메인과 강하게 결합되어 있다
기존 코드는 특정 도메인과 강하게 연결되어 있었고, 따라서 여러 props 을 받고 있었기에 재사용이 불가능하다.
다른 곳에서 dropdown 이 필요할 경우 매번 만들어서 사용해야 한다는 번거로움이 있었다.
2. 역할이 명시적이지 않다
Dropdown 을 구현하기 위한 부수적인 로직 (handleClick, useState) 이 필요했고, 역할이 명시적으로 드러나지 않았다. 이로 인해 코드를 이해하고 유지보수하기 어려웠다.
실제로 작성한 기존 코드👇
const DropDown = ({
list,
handleTimePicker,
currentValue,
placeHolder,
selectable = false,
filterTime,
}: TDropDownProps) => {
const [isFolded, setIsFolded] = useState(true);
const handleClick = (e: React.MouseEvent) => {
setIsFolded((prev) => !prev);
};
const handleClickTime = (value: string) => {
handleTimePicker(value);
setIsFolded(true);
};
const renderLisItem = (time: Time) => {
const result = filterTime ? filterTime(time) : true;
return (
<Option
key={time.value}
onClick={() => handleClickTime(time.value)}
disabled={!time.selectable || !result}
>
{time.label}
</Option>
);
};
return (
<Self>
<TopListItem
$isFolded={isFolded}
onClick={handleClick}
disabled={!selectable}
>
{currentValue ? currentValue : placeHolder}
</TopListItem>
{selectable && !isFolded && (
<List>
<RenderList data={list} renderItem={renderLisItem} />
</List>
)}
</Self>
);
};
export default DropDown;
이전에는 dropdown 컴포넌트가 특정 도메인과 강하게 결합되어 있어 재사용성이 낮았다면, 개선된 방식에서는 dropdown은 단순히 버튼을 누르면 리스트를 보여주고 선택하는 역할만 담당한다.
기존에는 dropdown 과 관련된 여러 기능들이 한 곳에 모여 있었지만, 개선된 방식에서는 각 컴포넌트가 명확한 역할을 수행한다.
Dropdown.Toggle은 드롭다운을 열고 닫는 역할,
Dropdown.Menu는 드롭다운의 옵션 목록을 보여주는 역할,
Dropdown.Option 컴포넌트는 각 옵션을 표시하고 선택하는 역할을 수행한다.
이렇게 역할을 명확히 분리함으로써 코드의 응집성을 높이고 재사용성을 높였다.
스타일링은 각 컴포넌트를 사용하는 곳에서 직접 설정할 수 있도록 했다. 이렇게 함으로써 컴포넌트를 보다 유연하게 사용할 수 있으며, 컴포넌트의 스타일링이 외부 의존성을 최소화하여 필요에 따라 스타일을 쉽게 변경할 수 있도록 했다.
코드로 구현하면 아래와 같다.
Context 을 사용해서 해당 Context를 구독하는 컴포넌트들은 상태의 변경을 감지하여 자동으로 업데이트된다.
Dropdown 컴포넌트 내부에서 열고 닫기, 그리고 선택한 값 설정과 같은 기능을 관리하고 있기 때문에 Dropdown을 사용하는 곳에서는 이와 관련된 코드를 따로 작성할 필요가 없어졌다. 이로써 사용하는 쪽의 코드가 더 간결해졌다.
// Dropdown 컴포넌트에서 사용할 Context 타입 정의
type DropdownContextType = {
isOpen: boolean;
toggleDropdown: () => void;
selectedOption: string | null;
selectOption: (value: string) => void;
};
// Dropdown 컴포넌트에서 사용할 Context 생성
export const DropdownContext = createContext<DropdownContextType>({
isOpen: false,
toggleDropdown: () => {},
selectedOption: null,
selectOption: () => {},
});
// Dropdown 컴포넌트 정의
const Dropdown: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | null>(null); // 선택된 옵션 상태
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
const selectOption = (value: string) => {
setSelectedOption(value);
setIsOpen(false);
};
return (
<DropdownContext.Provider value={{ isOpen, toggleDropdown, selectedOption, selectOption }}>
{children}
</DropdownContext.Provider>
);
};
// DropdownToggle, DropdownMenu, DropdownOption 컴포넌트를 불러와 export 해준다
export { Dropdown, DropdownToggle, DropdownMenu, DropdownOption };
수정 전&후 view 와 관련된 코드만 봤을때 수정 후 코드가 훨씬 간결하다.
Dropdown 으로 감싸진 부분에 Toggle, Menu, Option(RenderList 에서 수행한다) 을 조합해서 사용이 가능하다.
Dropdown 컴포넌트 내부에서 열고 닫기, 그리고 선택한 값 설정과 같은 기능을 관리하고 있기 때문에 Dropdown을 사용하는 곳에서는 이와 관련된 코드를 따로 작성할 필요가 없다.
사용하는 곳에서는 필요에 맞게 CSS 처리를 해주면 된다.
나는 Styled-Component 을 사용하고 있기 때문에 아래와 같이 DropdownToggle 컴포넌트를 감싸서 필요에 맞게 스타일링을 해줬다.
도메인과의 결합을 끊어냄으로써 Dropdown 역할만 수행하는 컴포넌트로 탈바꿈했다.
또한 각 역할에 맞게 컴포넌트를 모듈화 함으로써 역할이 명시적으로 드러난다.
그리고 필요한 컴포넌트에서 스타일링을 주입함으로써 변경에 유연하게 대처가 가능해졌다.
수정 전 코드👀
return (
<Self>
<TopListItem
$isFolded={isFolded}
onClick={handleClick}
disabled={!selectable}
>
{currentValue ? currentValue : placeHolder}
</TopListItem>
{selectable && !isFolded && (
<List>
<RenderList data={list} renderItem={renderLisItem} />
</List>
)}
</Self>
);
수정 후 코드😎
return (
<Self>
<Dropdown>
<Toggle disabled={!selectable}>{selectedTime ? selectedTime : placeHolder}</Toggle>
<Menu>
<RenderList data={timeList} renderItem={renderOption} />
</Menu>
</Dropdown>
</Self>
);
일부 CSS 코드😉
const Toggle = styled(DropdownToggle)<{ disabled: boolean }>`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 4.5rem;
max-height: 56px;
padding: 16px 12px 16px 16px;
background-color: #3581ff;
color: white;
border: 1px solid rgba(38, 45, 57, 0.08);
font-size: 1.2rem;
box-sizing: border-box;
line-height: 22px;
user-select: none;
border-radius: 4px;
cursor: pointer;
${({ disabled }) =>
disabled &&
css`
opacity: 0.5;
`}
`;