HeadlessComponent 리팩토링

전해림·2023년 11월 26일
0
post-thumbnail

필자는 프론트엔드 개발을 한지 1년이 조금 넘었다. 나는 개발을 하면서 항상 공통컴포넌트.. 공통컴포넌트.. 하면서 어떻게 하면 공통된 UI 코드를 효율적이게 짤 수 있을까? 항상 고민했던것 같다.

하지만 공통컴포넌트를 만들었지만 recoil을 사용해서 전역으로 state를 관리했었고 컴포넌트
style을 바꾸고싶다면 props로 넘기며 분기처리를 해야만 했다.

//CheckBox.tsx
const CheckBox = () => {
  const [isChecked, setIsChecked] = useRecoilState<boolean>(CheckBoxAtom);

  const toggleCheckbox = () => {
    setIsChecked(!isChecked);
  };
  return (
    <>
      <S.Label>
        <S.Input
          type="checkbox"
          checked={isChecked}
          onChange={toggleCheckbox}
        />

        {isChecked ? (
          <Lottie
            animationData={CheckBoxLottie}
            loop={false}
            play
            style={{ width: 20, height: 20 }}
            direction={1}
          />
        ) : (
          <Lottie
            animationData={CheckBoxLottie}
            loop={false}
            play
            style={{ width: 20, height: 20 }}
            direction={-1}
          />
        )}
      </S.Label>
    </>
  );
};

export default CheckBox;

이러한 방법은 분기처리를 여러번 해야된다는 문제점과 state을 전역으로 관리해야된다는 문제점이 있었다..
그렇다 공통의 저주에 걸린것이다.

공통의 저주에서 벗어나기위해 여러 블로그를 찾아보던중 Headless Components을 보게 되었다.

Headless란?

Headless를 그대로 번역하면 '머리가 없는'이라는 뜻이다.
좀 더 자세히 이야기 하면 스타일이 없고 로직만 있는 컴포넌트를 의미한다.

공통 컴포넌트에 스타일을 입히지 않고 로직만을 짠 뒤 컴포넌트를 사용하는 곳에서 스타일을 사용한다고 생각하면 좀 더 쉬울것 같다.

나는 예전에 짰던 공통 체크박스를 Headless Components의 Function as Child Component로 리팩토링을 했다.

//CheckBoxHeadless.ts
import { useState } from "react";

type CheckboxHeadlessProps = {
  isChecked: boolean;
  onChange: () => void;
};

const CheckBoxHeadless = (props: {
  children: (args: CheckboxHeadlessProps) => JSX.Element;
}) => {
  const [isChecked, setIsChecked] = useState(false);

  if (!props.children || typeof props.children != "function") return null;

  return props.children({
    isChecked,
    onChange: () => setIsChecked(!isChecked),
  });
};

export default CheckBoxHeadless;

function as child Component는 자식에 어떤 것이 들어올지 예상할 수 없기 때문에 children prop으로 받아 그대로 전달하는 것이다.

//App.tsx
const App = () => {
  return(
    <CheckBoxHeadless>
            {({ isChecked, onChange }) => {
              return (
                <S.Label>
                  <input
                    type="checkbox"
                    checked={isChecked}
                    onChange={onChange}
                    style={{ display: "none" }}
                  />

                  {isChecked ? (
                    <Lottie
                      animationData={TE}
                      loop={false}
                      play
                      style={{ width: 20, height: 20 }}
                      direction={1}
                    />
                  ) : (
                    <Lottie
                      animationData={TE}
                      loop={false}
                      play
                      style={{ width: 20, height: 20 }}
                      direction={-1}
                    />
                  )}

                  <p>로그인 상태 유지</p>
                </S.Label>
              );
            }}
          </CheckBoxHeadless>
      )

이러한 방식은 사용하려는 state 값을 위에서 따로 선언할 필요가 없어, 다른 컴포넌트에 해당 state를 실수로 넣을 일이 적어진다. 그리고 관련된 코드가 한 곳에 모여 있어 읽기 편하다. 하지만 다른 곳에서 해당 state를 공유할 경우, CheckboxHeadless가 감싸야 할 코드량이 많아지는 단점이 있다.

결론

이 체크박스는 style이 변경될 일이 별로 없기 때문에 HeadlessComponent를 사용하지 않아도 되지만 자주쓰이는 버튼같은 경우는 HeadlessComponent를 사용하면 공통컴포넌트를 쉽게 만들 수 있을것 같다.
필자는 HeadlessComponent를 처음 접했을때 공통컴포넌트에 씨게 걸려있어서 와...이거 사용했으면 공통컴포넌트 저주 탈출하겠다..x되네.. 라고 생각을 했다 ^^

참고자료
https://www.howdy-mj.me/design/headless-components
https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC

profile
프론트엔드 개발자 전해림입니다

0개의 댓글