React에서 UI와 로직 분리하기

contability·2024년 4월 4일
0

이를 헤드리스 컴포넌트라 하는데 난해해 보일 수 있지만, 그 진정한 힘은 유연성, 재사용 가능성, 코드베이스의 구성과 깔끔함을 개선하는 능력에 있다.
이 글에서는 이 패턴이 정확히 무엇인지, 왜 유용한지, 인터페이스 디자인에 대한 접근 방식을 어떻게 혁신할 수 있는지를 조명하면서 이 패턴에 대해 설명한다.

예를 들어보면

Toggle Component

const ToggleButton = () => {
  const [isToggled, setIsToggled] = useState(false);

  const toggle = useCallback(() => {
    setIsToggled((prevState) => !prevState);
  }, []);

  return (
    <div className="toggleContainer">
      <p>Do not disturb</p>
      <button onClick={toggle} className={isToggled ? "on" : "off"}>
        {isToggled ? "ON" : "OFF"}
      </button>
    </div>
  );
};

간단하게 버튼을 클릭할 때 마다 isToggled state의 값이 true와 false 사이에서 전환되는 컴포넌트가 있다.

이제 섹션의 세부 정보를 표시하거나 숨길 수 있는 완전히 다른 컴포넌트인 collapsableSection를 작성해보자.

const collapsableSection = ({ title, children }: ExpandableSectionType) => {
  const [isOpen, setIsOpen] = useState(false);

  const toggleOpen = useCallback(() => {
    setIsOpen((prevState) => !prevState);
  }, []);

  return (
    <div>
      <h2 onClick={toggleOpen}>{title}</h2>
      {isOpen && <div>{children}</div>}
    </div>
  );
};

여기서 ToggleButton의 켜고 끄는 동작과 collapsableSection의 펼치고 접는 동작은 서로 유사하다.
이러한 공통점을 인식하면 이 공유 기능을 별도의 함수로 추상화 할 수 있게 된다.

여기서 유사점이 있는 기능만 React Custom hook으로 빼보면 이런 모양이 될 것이다.

const useToggle = (init = false) => {
  const [state, setState] = useState(init);
  
  const toggle = useCallback(() => {
    setState((prevState) => !prevState);
  }, []);

  return [state, toggle];
};

동작과 프레젠테이션을 분리하는 중요한 개념을 강조하며 이 Custom hook은 JSX와 독립적인 상태로 역할을 한다.

일정 규모 이상의 프로젝트에 시간을 투자해 본 사람이라면 대부분 업데이트나 버그가 UI 비주얼 부분이 아니라 로직과 관련이 있다는 것을 쉽게 알 수 있다.
Hook은 이러한 논리적 측면을 중앙 집중화하고 유지 관리하기 쉽게 만들 수 있다.

Headless Component

실제로 이 패턴을 사용하여 동작과 UI를 분리하는 라이브러리들이 이미 존재하며 가장 유명한 라이브러리는 Downshift이다.
Downshift는 UI를 랜더링하지 않고 동작과 상태를 관리하는 헤드리스 컴포넌트라는 개념을 적용한다.

const StateSelect = () => {
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
  } = useSelect({items: states});

  return (
    <div>
      <label {...getLabelProps()}>Issued State:</label>
      <div {...getToggleButtonProps()} className="trigger" >
        {selectedItem ?? 'Select a state'}
      </div>
      <ul {...getMenuProps()} className="menu">
        {isOpen &&
          states.map((item, index) => (
            <li
              style={
                highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
              }
              key={`${item}${index}`}
              {...getItemProps({item, index})}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  )
}

Downshift의 useSelect hook을 활용한 dropdown list 컴포넌트이다.
이처럼 활용했을 때 여러 컴포넌트나 프로젝트에서 동작 로직을 공유할 수 있어 유용하다.

다른 헤드리스 컴포넌트 라이브러리들도 있다.

조금 더 살펴보자면

이렇게 로직과 UI를 계속 분리하면 점차 계층화된 구조가 만들어진다.
이 구조는 애플리케이션 전체의 계층화된 아키텍처가 아니라 애플리케이션의 UI 부분에 한정된 구조를 말한다.

이 구조에서 JSX는 전달된 프로퍼티를 표시하는 역할을 담당한는 최상위 레이어에 정의된다.
그 바로 아래에 컴포넌트의 모든 동작을 유지하고, 상태를 관리하며, JSX가 상호작용할 수 있는 인터페이스를 제공하는 헤드리스 컴포넌트가 위치한다.
이 구조의 기반에 도메인별 로직을 캡슐화하는 데이터 모델이 있고 이 모델은 UI나 상태와는 관련이 없다.
대신 데이터 관리와 비즈니스 로직에 집중한다.

이러한 계층적 접근 방식은 관심사를 깔끔하게 분리하여 코드의 명확성과 유지보수성을 향상시킨다.

장단점

정리하자면

장점

  1. 재사용성 - 코드 중복을 줄일 수 있고 애플리케이션 전반에서 일관성을 유지할 수 있다.
  2. 관심사 분리 - 업무가 분산된 대규모 팀의 경우 코드베이스를 더 이해하고 관리하기 쉽게 만들 수 있다.
  3. 유연성 - 기본 로직에 영향을 주지 않고 원하는 만큼 디자인에 더 큰 유연성을 부여할 수 있다.
  4. 테스트 가능성 - 비즈니스 로직에 대한 단위 테스트를 작성하기 더 쉬워진다.

단점

  1. 초기 오버헤드 - 단순한 애플리케이션이나 컴포넌트의 경우 오버엔지니어링이 될 수도 있고 불필요하게 복잡도를 높일 수 있다.
  2. 러닝 커브 - 개념에 익숙하지 않은 개발자는 학습 곡선이 더 가파르게 진행될 수 있다.
  3. 남용 가능성 - 불필요한 경우에도 모든 컴포넌트를 헤드리스로 만들려고 시도한다면 지나치게 복잡해질 수 있다.
  4. 잠재적인 성능 문제 - 일반적으로는 큰 문제가 아니지만 신중하게 처리하지 않는다면 공유 로직을 사용한 여러 컴포넌트를 랜더링하는 경우 성능 문제가 발생할 수 있다.

헤드리스 UI는 다른 아키텍처 패턴처럼 만능 솔루션이 아니다.
구체적인 요구 사항과 복잡성에 따라 개발자가 잘 판단해서 사용하는 것이 중요할 것 같다.

출처: https://itnext.io/decoupling-ui-and-logic-in-react-a-clean-code-approach-with-headless-components-82e46b5820c

0개의 댓글