[10분 테코톡] 컴포넌트 IoC패턴

흑우·2023년 11월 28일

10분 테코톡 - 5기

목록 보기
2/16

제어의 역전이란?

  • API를 사용하는 이에게 내부적으로 어떻게 동작할지에 대한 권한을 부여하는 매커니즘

컴포넌트에서 제어의 역전

  • 컴포넌트를 사용하는 개발자에게 컴포넌트의 제어권을 넘겨줌

SelectBox 컴포넌트를 구현한다고 생각해볼게요!

const SelectBox = ({ options }) => {
  const [selectedItem, setSelectedItem] = useState<string | null>(null);
  const [open, setOpen] = useState(false);

  const toggleOpen = () => {
    setOpen((prev) => !prev);
  };

  const select = (e: MouseEvent<HTMLDivElement>) => {
    const target = e.target as HTMLDivElement;

    setSelectedItem(target.textContent);
    toggleOpen();
  };

  return (
    <section>
      <button>{selectedItem ?? "open me!"}</button>
      {open && (
        <section>
          {options.map((option, idx) => {
            return (
              <div
                key={idx}
                onClick={select}
                className={`optionItem ${
                  selectedItem === option && "selected"
                }`}
              >
                {option}
              </div>
            );
          })}
        </section>
      )}
    </section>
  );
};

export default SelectBox;
  • 필요한 상태를 정의하고, 로직을 구현하고, UI를 그린다음 로직과 연결을 하겠죠?
  • 그리고 해당 컴포넌트를 사용할 때는 필요한 options만 props로 전달하겠죠?
const App = () => {
	const options = ["red","black","blue"]
    return <SelectBox options={options}/>
}
  • 이렇게 만들어 놓은 SelectBox에 다른 요구사항이 추가된다면?
  • 버튼의 라벨을 다르게 설정할 수 있게 해주세요!
  • 해당 요구사항을 반영하기 위해 props를 통해 해결하겠죠?
const App = () => {
	const options = ["red","black","blue"]
    const triggerLabel = "열어주세요!"
    return <SelectBox options={options} triggerLabel={triggerLabel} />
}
  • 버튼이 아래쪽에도 위치하는 경우도 필요합니다!
const App = () => {
	const options = ["red","black","blue"]
    const triggerLabel = "열어주세요!"
    return <SelectBox options={options} triggerLabel={triggerLabel} buttonPosition="bottom" />
}
  • 이러한 요구사항들이 계속해서 들어오게된다면요?
  • 그럴때마다 props를 계속해서 추가할 예정이신가요? 다음과 같은 문제가 생깁니다!
    • 컴포넌트의 유지 보수가 어려워진다.
    • 구현 난이도가 올라간다.
    • 복잡한 API를 가진 컴포넌트가 된다.

이러한 문제를 해결하는 것이 제어의 역전

  • 늘어나는 요구사항에 유연하게 대처하기 위해 컴포넌트를 어떻게 사용할까의 역할을 컴포넌트가 아닌 개발자에게 넘겨줄 수 있다.

컴포넌트 IoC 패턴

Compound Component Pattern

  • 관심사가 분리된 자식 컴포넌트들이 부모 컴포넌트의 상태를 공유하면서 조합되어 하나의 컴포넌트를 생성하는 패턴
const App = () => {
	const options = ["red","black","blue"]
    const triggerLabel = "열어주세요!"
    return (
    	<Select>
      		<Select.Trigger triggerLabel={triggerLabel} />
			<section>
      		{options.map(item => {
            	return <Select.Option optionItem={item} />
            })}
      		<section>
      	</Select>
    )
}
  • Trigger와 Option은 자신이 필요한 props만 받는다 (관심사 분리)
  • 분리된 컴포넌트들은 Context API를 통해 Select 컴포넌트에 선언된 로직을 자식 컴포넌트들이 공유
  • 장점
    • API 사용 복잡도 감소 : 하나의 컴포넌트에 모든 props를 넘기고, 자식 컴포넌트에게 props drilling을 하지 않고 해당 props가 필요한 자식 컴포넌트에게만 props를 넘길 수 있음
    • 유연한 마크업 구조 : 원하는 자식 컴포넌트를 원하는 곳에 위치시킬 수 있음
  • 단점
    • 지나친 UI의 유연홤 : 의도하지 않은 코드, Select의 유효하지 않은 children, 필수적인 children의 부재 등
    • JSX 코드 증가 : 하나의 역할을 하는 컴포넌트를 여러개로 분리해서 새용해야 함

Control Props Pattern

  • 내부의 상태와 로직을 사용하지 않고, 외부에서 주입한 프로퍼티와 콜백함수를 사용함으로써 외부에서 컴포넌트의 상태를 제어할 수 있는 패턴
const App = () => {
	const options = ["red","black","blue"]
    const [selectedItem, setSelectedItem] = useState<string|null>(null)
    const [open, setOpen] = useState(false);

    const toggleBox = () => {
      setOpen((prev) => !prev);
    };

    const select = (e: MouseEvent<HTMLDivElement>) => {
      const target = e.target as HTMLDivElement;

      setSelectedItem(target.textContent);
      toggleBox();
    };
    return (
    	<ControlledSelect 
      		options={options}
			selectedItem={selectedItem}
			open={open}
			select={select}
			toggleBox={toggleBox}
      	>
    )
}
  • 장점
    • 더 많은 제어권 부여 : 메인 state가 컴포넌트 밖에 있기 때문에 사용자는 직접적으로 그 컴포넌트를 제어할 수 있음
  • 단점
    • 구현 복잡도 : 여러 위치에서의 구현이 필요함

Custom Hook Pattern

  • 메인 로직이 커스텀 훅으로 전달되고, 훅 내부의 상태와 로직들은 개발자가 제어할 수 있는 패턴
const App = () => {
	const options = ["red","black","blue"]
    const { selectedItem, open, toggleBox, select } = useSelect();
  
  	function alertSelectItem(target: string){
    	alert(`${target}을 선택했습니다.`)
    }
  	function handleClick(e: MouseEvent<HTMLDiveElement>){
    	const target = e.target as HTMLDivElement;
      	if (target.textContext === null) return;
      
      	select(e);
      	alertSelectItem(target.textContext);
    }
    return (
    	<ControlledSelect 
      		options={options}
			selectedItem={selectedItem}
			open={open}
			handleClick={handleClick}
			toggleBox={toggleBox}
      	>
    )
}
  • 커스텀훅이 구현부와 분리되서 선언이되고 필요한 SelectBox에서 필요한 props나 로직이 반환됩니다.
  • 개발자가 로직을 추가하고 싶다면 새로운 로직을 선언하고 메인 로직과 합친다음에 SelectBox로 전달해줍니다.
  • 장점
    • 더 많은 제어권 부여 : 개발자는 커스텀훅으로 제공된 메인로직과 제어해야하는 컴포넌트 사이에 원하는 로직을 삽입할 수 있음
  • 단점
    • 구현 복잡도 : 로직을 담당하는 부분이 구현부와 분리되어 있기 때문에, 이를 연결하는 것은 개발자의 몫. 구현할 때부터 커스텀훅이 적용되는 컴포넌트에 대해 잘 알고있어야 함. (개발자가 커스텀훅에서 반환된 native props를 직접 가공하고 로직을 일일히 재정의 해야함)

Props Getters Pattern

  • 커스텀훅 패턴의 단점을 보완하기 위한 패턴. 커스텀훅으로 부터 native props가 아닌 props를 반환하는 함수인 props getters를 반환
const App = () => {
	const options = ["red","black","blue"]
    const { open, getToggleProps, getSelectProps } = useGetterSelect();
  	function alertButton(){
    	alert("이거 눌러도 닫히지롱!")
    }
    return (
      	
    	<PropsGetteredSelect>
      		<button {...getToggleProps({ onClick: alertButton })}>	
      			strange button
     		<button>
      		<PropsGetteredSelect.Trigger {...getToggleProps()} />
			{open && (
            	<section className="selectContainer">
                {options.map(item => {
                    return <PropsGetteredSelect.Option {...getSelectProps()} />
                })}
                <section>
            )}
      	</PropsGetteredSelect>
    )
}
  • getToggleProps & getSelectProps
    • 커스텀훅의 native props를 반환하는 동시에 함수 인자를 받아서 새로운 함수를 native props와 함께 실행시킬 수 있게 해주는 함수
const getToggleProps = ({ onClick, ...otherProps }): any = {}) => ({
	onClick: callFnsInSquence(toggleBox, onClick),
  	open,
  	...otherProps,
})

const getSelectProps = ({ onClick, ...otherProps }): any = {}) => ({
	onClick: callFnsInSquence(select, onClick),
  	selectedItem,
  	...otherProps,
})
  • 장점
    • 사용이 용이함 : 개발자는 적절한 getter를 올바른 JSX요소에 연결하기만 하면 됨
    • 유연함 : getter의 props에 함수를 오버라이딩 할 수 있기 때문에 새로운 로직을 일일히 정의할 필요 없음
  • 단점
    • 가독성 저하 : props를 getter로 추상화 했기 때문에 native props가 숨겨짐. 로직을 올바르게 재정의하려면 getter에 의해 제공된 prop 리스트와 그 중 하나가 변경될 때 내부 로직에 미치는 영향을 알아야만 함.

컴포넌트 IoC 패턴의 효과적인 활용

  • 제어의 역전은 컴포넌트의 역할을 개발자에게 넘기는 것이기 때문에 여러가지 사이드 이펙트가 나타날 수 있습니다.
  • 제어의 역전이 효과적인 컴포넌트 경우
    • 여러가지 경우에 사용될 수 있는 재사용 가능한 컴포넌트를 만들고 싶다
    • 사용하기 쉽고 편리한 API를 제공하는 컴포넌트를 만들고 싶다.
    • UI와 기능면에서 확장성 있는 컴포넌트를 만들고 싶다.

마무리

저번 글에서 다뤘던 합성 컴포넌트 패턴과 이어지는 부분이 많은 영상이라 이해하기 쉬웠습니다. 기존에 정의되어 있는 기능이 아니라 추가적인 기능을 구현하고 싶을 때 어떻게 할 것인지를 설명하는 부분이 상당히 인상적인데요. 하지만 props 자체를 반환하는 것이 아니라 getter 함수를 반환함으로써 로직을 추가하는 걸 더 편하게 만든 것은 고민이 되는 부분 같습니다. 커스텀 훅의 메인 로직이 겉에서 들어나지 않는다는 부분이 저번 토스 컨퍼런스에서 배웠던 커스텀 훅의 안티 패턴과 비슷하게 느껴지기 때문인데요. 지나친 추상화는 오히려 독이된다는 데 주니어인 제 입장에서는 판단하기가 쉽지 않네요..

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글