프로젝트 리팩토링 - Dropdown 합성 컴포넌트로 구현하기

D uuu·2024년 4월 12일
1

project

목록 보기
6/10

기존 코드 문제점

기존에 작성한 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;

개선 방법

1. 특정 도메인과의 결합을 끊고 독립적인 dropdown 구현하기

이전에는 dropdown 컴포넌트가 특정 도메인과 강하게 결합되어 있어 재사용성이 낮았다면, 개선된 방식에서는 dropdown은 단순히 버튼을 누르면 리스트를 보여주고 선택하는 역할만 담당한다.

2. 한가지 역할만 하도록 컴포넌트 모듈화하기

기존에는 dropdown 과 관련된 여러 기능들이 한 곳에 모여 있었지만, 개선된 방식에서는 각 컴포넌트가 명확한 역할을 수행한다.

Dropdown.Toggle은 드롭다운을 열고 닫는 역할,
Dropdown.Menu는 드롭다운의 옵션 목록을 보여주는 역할,
Dropdown.Option 컴포넌트는 각 옵션을 표시하고 선택하는 역할을 수행한다.

이렇게 역할을 명확히 분리함으로써 코드의 응집성을 높이고 재사용성을 높였다.

3. 필요한 곳에서 CSS 을 적용하도록 하기

스타일링은 각 컴포넌트를 사용하는 곳에서 직접 설정할 수 있도록 했다. 이렇게 함으로써 컴포넌트를 보다 유연하게 사용할 수 있으며, 컴포넌트의 스타일링이 외부 의존성을 최소화하여 필요에 따라 스타일을 쉽게 변경할 수 있도록 했다.

코드로 구현하면 아래와 같다.
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;
		`}
`;
profile
배우고 느낀 걸 기록하는 공간

0개의 댓글

관련 채용 정보