[TSL] HeadlessUI 활용기 : Disclosure 요구사항 맞게 구현하기 (onClick 기능)

intersoom·1일 전
0

TSL

목록 보기
11/11
post-thumbnail

Headless UI란?

headless ui란, 컴포넌트 라이브러리로, UI 로직만 제공하고 스타일이나 디자인은 포함하지 않는 라이브러리다.

Ant design과 같은 컴포넌트 라이브러리와의 차이점은 특정 디자인을 강제하지 않고, 디자인을 사용자 맘대로 지정할 수 있다는 점이 장점이 있다.

headless-ui와 비슷한 라이브러리로는 radix-ui가 있다.

더 나아가, 이를 활용해서 또 새로운 형태의 라이브러리를 제공하는 shadcn 같은 라이브러리도 있는데,,
shadcn은 radix-ui에 디자인 입힌 버전..? 일종의 Tailwind CSS 기반의 Radix UI 래퍼..(?)라고 생각하면 된다.

어떻게 활용했는가.

TMI

사실 headless-ui를 사용하면서 다양한 이슈들을 경험하고 있다.

얼마 전에는 라이브러리에서 활용하는 라이브러리의 이슈(@tanstack/virtual) 때문에 발생하는 이슈가 있는데, 해결했다고 관련 issue가 PR 언급하며 closed 됐는데,, 아직 같은 이슈가 발생중이다... 🤔
(궁금하신 분들을 위해서.. 이 이슈다.. 아직도 빈배열 들어가면 오류난다! 그래서 일단 이러한 형태['']를 넣는 방식으로 해결했다. )

조만간 조금 더 제대로 파보고 내가 issue 올려볼까 생각중이다.

Disclosure 관련 사항

요구사항 :

disclosure가 open인 경우에만, validation을 진행하고 close인 경우에는 validation을 하지 않는다.

근데 disclosure에 대한 onChange가 존재하지 않아서 어떻게 구현해야할지 고민을 열심히 해보았다!

그래서 순차적으로 다음과 같은 해결법들을 생각해냈다.

1번 : onChange 속성을 임의로 구현해서 사용

작동은 하지만, 렌더링 문제 때문에 에러가 발생했다.

이거는 딱 봐도 좋은 방법은 아니다... ㅋㅋㅋ...

2번 : 토글 추가

disclosure를 활용하지 않고 그 외적인 방법으로 해결하는 방법을 생각해봤다.

disclosure 내부에 토글을 하나 더 추가해서.. 그걸 키고 닫을 때, validation의 여부를 키고 끄고를 하고자 했다. 이거 관련해서 30분 이상 시간을 쓰고 있자, 비효율적임을 깨닫고 생각해본 방법이다.

근데 해당 방식 사용시,

  1. 일차적으로 디자인이 구림.. FE 개발자로서 용납할 수 없다. 심지어 한 단계가 추가되는 것이기 때문에 UX도 구려진다..
  1. 이차적으로 컴포넌트 구조가 복잡해진다. 기존에는 disclosure의 children으로 Disclosure가 열고 닫힘에 따라서 보여주는 컴포넌트의 렌더링 여부를 결정하면 되었는데,,
    그 내부에 있는 일부 컴포넌트를 밖으로 끄집어내고,, 이상한 조건부 코드도 추가해야하고,, 컴포넌트가 굉장히 엉망진창으로 바뀔 것을 보고서..

오히려 이게 더 시간이 많이 걸리고 코드가 더러워질 것 같다는 판단 하에 disclosure를 활용해서 해결하자는 결론에 이르렀다..!

결론 : 찐짜 해결책

그래서 해결책에 도달한 과정은 다음과 같다.

  1. Disclosure 관련 공식 문서 정독
  2. as를 통해서 ref 주입을 통해서 바깥에서 button을 조작할 수 있는 것 발견 !
  3. 다음과 같이 구현
    • 바깥에서 구현한 button에 내가 원하는 onClick 속성 추가
    • 기존의 onClick을 props를 통해서 받아와서 넣어주기
            const Button = forwardRef<HTMLButtonElement, any>(function (props, ref) {
                const { onClick, ...rest } = props;
                const { setValue, resetField } = useFormContext<CampaignView>();
                const [open, setOpen] = useState(false);
            
                return (
                    <button
                        ref={ref}
                        {...rest}
                        onClick={(e) => {
                            handleDisclosureButtonClick();
                            onClick?.(e);
                        }}
                    />
                );
            
                function handleDisclosureButtonClick() {
                    if (open) {
                        setOpen(false);
                        resetField('name');
                    } else {
                        setOpen(true);
                        setValue('name', initial_value);
                    }
                }
            });

이렇게 해서

function Example() {
  return (
    <Disclosure>
      <DisclosureButton>Open mobile menu</DisclosureButton>
      <DisclosurePanel>
        <CloseButton as={Button} href="/home">
          Home
        </CloseButton>
      </DisclosurePanel>
    </Disclosure>
  )
}

as={Button}과 같이 넣어주면 된다~!

https://github.com/tailwindlabs/headlessui/discussions/640
해당 논의를 보면, children으로 button 컴포넌트를 따로 추가해줘도 되는 것 같다!

나는 공통 컴포넌트로 만들어서 사용하기 때문에 불필요한 곳들도 존재하기 때문에 내가 생각해낸 방식이 더 적합한 것 같다. 다만, 상황에 따라서 위의 논의에서 언급한 방식이 코드가 간결해질 것 같기도 하니 참고하시라!

post-custom-banner

0개의 댓글