본문 바로가기

FrontEnd

[번역] Headless UI Component란 무엇인가?

반응형

원문 : https://www.merrickchristensen.com/articles/headless-user-interface-components/

 

Merrick Christensen

Using Webflow with Netlify Configure Netlify to send particular routes to Webflow so that you can selectively serve pages that are designed and hosted on Webflow. 2 Minute Read » JSON Lisp Learn about how programming languages work as we design & implemen

www.merrickchristensen.com

headless ui

요약

headeless UI 컴포넌트는 자체 UI를 제공하지 않으며(렌더링 X)

최대의 시각적 유연성을 제공하는 컴포넌트다.


동전 앞 뒷면 맞추기 컴포넌트(<CoinFlip/> Component)

동전 던지기를 흉내내는 컴포넌트가 있다고 생각해보자.

50%는 head(앞면), 50%는 tail(뒷면)이 나올 것이다.

const CoinFlip = () =>
  Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;

text 대신 이미지를 보여줄 수 있다.

const CoinFlip = () =>
  Math.random() < 0.5 ? (
    <div>
      <img src="/heads.svg" alt="Heads" />
    </div>
  ) : (
    <div>
      <img src="/tails.svg" alt="Tails" />
    </div>
  );

SEO를 위해 레이블이 필요해졌다.

원래 해당 컴포넌트를 사용하는 곳을 망치지 않기 위해 플래그를 추가한다.

const CoinFlip = (
  // We'll default to false to avoid breaking the applications
  // current usage.
  { showLabels = false }
) =>
  Math.random() < 0.5 ? (
    <div>
      <img src="/heads.svg" alt="Heads" />

      {/* Add these labels for the marketing site. */}
      {showLabels && <span>Heads</span>}
    </div>
  ) : (
    <div>
      <img src="/tails.svg" alt="Tails" />

      {/* Add these labels for the marketing site. */}
      {showLabels && <span>Tails</span>}
    </div>
  );

동전을 다시 던지기 위한 버튼을 추가해보자

이번에도 이전 소스를 망치지 않기 위해 showButton 플래그를 추가한다.

const flip = () => ({
  flipResults: Math.random(),
});

class CoinFlip extends React.Component {
  static defaultProps = {
    showLabels: false,
    // We don't repurpose `showLabels`, we aren't animals, after all.
    showButton: false,
  };

  state = flip();

  handleClick = () => {
    this.setState(flip);
  };

  render() {
    return (
      // Use fragments so people take me seriously.
      <>
        {this.state.showButton && (
          <button onClick={this.handleClick}>Reflip</button>
        )}
        {this.state.flipResults < 0.5 ? (
          <div>
            <img src="/heads.svg" alt="Heads" />
            {showLabels && <span>Heads</span>}
          </div>
        ) : (
          <div>
            <img src="/tails.svg" alt="Tails" />
            {showLabels && <span>Tails</span>}
          </div>
        )}
      </>
    );
  }
}

어떤 동료가 다가와서 물어본다.

"로직이 유사한것 같은데 주사위 굴리기 결과 맞추기 컴포넌트를 해당 코드를 재활용해서 만들수 있을까?"

  1. 버튼을 클릭해 주사위를 다시 굴린다
  2. 마케팅 사이트에서는 컴포넌트만 보여준다, 앱에서는 다시 굴릴수 있게 한다. 블로그에서는 SEO 레이블을 생략하낟
  3. 다른 인터페이스를 갖고 있다. (코인 > 주사위)
  4. 다른 시행확률을 갖고 있다. (1/2 > 1/6)

이제 두 가지 선택지가 있다.

  • 안 된다고 한다.
  • 컴포넌트에 기능을 추가해주고 무거운 책임에 의해 컴포넌트가 붕괴하는 모습을 본다.

Headless Component 만나기

Headless UI 컴포넌트는 로직과 동작을 컴포넌트의 사각적 표현과 분리한다.
React를 이용하면 두 가지 방법으로 구현할 수 있다.
  • Render Props pattern
  • Hooks
  • View & Controller
  • ViewModel & View

아래는 children을 Render Prop으로 구현한 방법이다.

const flip = () => ({
  flipResults: Math.random(),
});

class CoinFlip extends React.Component {
  state = flip();

  handleClick = () => {
    this.setState(flip);
  };

  render() {
    return this.props.children({
      rerun: this.handleClick,
      isHeads: this.state.flipResults < 0.5,
    });
  }
}

이 컴포넌트는 자체적으로 아무것도 렌더링 하지 않기 때문에 headless component다.
다른 리액트 엘리컨트, 컴포넌트를 렌더링 하는 것은 render prop이며,
해당 컴포넌트는 렌더링을 위한 의존성을 공급할 뿐이다.

<CoinFlip>
  {({ rerun, isHeads }) => (
    <>
      <button onClick={rerun}>Reflip</button>
      {isHeads ? (
        <div>
          <img src="/heads.svg" alt="Heads" />
        </div>
      ) : (
        <div>
          <img src="/tails.svg" alt="Tails" />
        </div>
      )}
    </>
  )}
</CoinFlip>
The marketing website code:

<CoinFlip>
  {({ isHeads }) => (
    <>
      {isHeads ? (
        <div>
          <img src="/heads.svg" alt="Heads" />
          <span>Heads</span>
        </div>
      ) : (
        <div>
          <img src="/tails.svg" alt="Tails" />
          <span>Tails</span>
        </div>
      )}
    </>
  )}
</CoinFlip>

프리젠테이션에서 로직을 완전히 독립시킨 모습이다.

로직을 더 많이 재사용하고 더 많은 시각적 유연성을 누릴 수 있다.

핵심은 메커니즘과 인터페이스를 분리하는 것이다.


주사위 굴리기 컴포넌트 구현하기 (<DiceRoll />)

동전 앞뒷면 맞추기 컴포넌트와 주사위 굴리기 결과 맞추기의 공통점은 다음과 같다.

  • 성공 / 실패 두 가지 결과가 있다.
  • 특정 확률에 의해 성공 / 실패가 정해진다.

이 공통점인 변화하는 특정 확률을 파라미터로,

공통적인 정해진 결과인 성공 / 실패를 리턴값으로 추상화하자.

const run = () => ({
  random: Math.random(),
});

class Probability extends React.Component {
  state = run();

  handleClick = () => {
    this.setState(run);
  };

  render() {
    return this.props.children({
      rerun: this.handleClick,

      // By taking in a threshold property we can support
      // different odds!
      result: this.state.random < this.props.threshold,
    });
  }
}

동전 앞뒷면 맞추기는 다음과 같이 구현한다.

const CoinFlip = ({ children }) => (
  <Probability threshold={0.5}>
    {({ rerun, result }) =>
      children({
        isHeads: result,
        rerun,
      })
    }
  </Probability>
);

주사위 굴리기 결과 맞추기는 다음과 같이 구현한다.

const RollDice = ({ children }) => (
  // Six Sided Dice
  <Probability threshold={1 / 6}>
    {({ rerun, result }) => (
      <div>
        {/* She was able to use a different event! */}
        <span onMouseOver={rerun}>Roll the dice!</span>
        {/* Totally different interface! */}
        {result ? (
          <div>Big winner!</div>
        ) : (
          <div>You win some, you lose most.</div>
        )}
      </div>
    )}
  </Probability>
);

분리 규칙 - 유닉스 철학

"Unix 철학의 기초"의 규칙 4는 다음과 같다.
분리 규칙: 메커니즘에서 정책을 분리합니다. 엔진과 인터페이스의 분리를 의미합니다.
- 에릭 S. 레이몬드

policy는 interface로 바꿔 생각할 수 있다.

mechanism은 logic & behavior로 생각할 수 있다.

인터페이스와 메커니즘은 서로 다른 시간 척도에서 변경되는 경향이 있으며
인터페이스는 메커니즘보다 훨씬 빠르게 변경됩니다.
GUI 툴킷의 모양과 느낌의 유행은 왔다 갔다 할 수 있지만 래스터 작업과 합성은 영원합니다.
따라서 인터페이스와 메커니즘의 걀합은 두 가지 나쁜 영향을 미칩니다.
- 사용자 요구 사항에 따라 인터페이스를 변경하기 어렵게 만듭니다.
- 인터페이스를 변경하려고 하면 메커니즘이 불안정해질 수 있습니다.
한편, 두 가지를 분리함으로써 메커니즘을 손상시키지 않고 새로운 인터페이스를 실험할 수 있습니다.
또한 메커니즘에 대한 좋은 테스트를 훨씬 쉽게 작성할 수 있습니다.
(인터페이스는 너무 빨리 노화되기 때문에 종종 투자를 정당화하지 못합니다).

이 자체도 훌륭한 통찰이지만,
헤드리스 컴포넌트를 사용하기에 적절한 경우에 대한 통찰도 제공한다.

 

1. 이 구성 요소의 수명은 얼마나 될까?

  • 인터페이스를 제외하고 메커니즘을 의도적으로 보존할 가치가 있을까?
  • 다른 모양과 느낌을 가진 다른 프로젝트에서 이 메커니즘을 사용할 수 있을까?

2. 인터페이스가 얼마나 자주 바뀌는가?

3. 동일한 메커니즘을 사용하는 인터페이스가 존재하는가?

"메커니즘"과 "정책"을 분리할 때 지불하는 간접 비용이 존재한다.
분리의 이점이 간접 비용의 가치가 있는지 확인해야 한다.
과거의 많은 MV* 패턴이 잘못되었던 곳이라고 생각한다.
그들은 모든 것이 이런 식으로 분리되어야 한다는 공리로 시작했습니다.
실제로는 메커니즘과 정책이 종종 밀접하게 연결되어 있거나 분리 비용이 분리의 이점을 능가하지 않을 때가 존재한다.
 

Headless Component 예시

bootstrap, mui와 같은 완성형 프레임워크들의 문제점은 로직과 UI가 강결합되어 스타일, 디자인을 커스터마이징하거나,

반대로 기능을 커스터마이징 하는 것이 컴포넌트, 스타일에 속박되는 경우가 있다.

로직과 UI를 잘 분리해내면 로직과 UI를 독립적으로 구성하여 합치는 방식으로, 원하는 컴포넌트를 더 쉽고 유지보수 가능하게 만들 수 있다.

염두에 두어야 할 것은 UI보다 로직이 더 오래 살아남는다는 것이다.

반응형