rsuite theme 부분 적용

support·2024년 4월 5일

rsuite라는 ui 라이브러리를 사용하고 있습니다.
전체적으로 다크모드를 사용하고 있고
셀렉트 박스 하나만 라이트모드를 사용해야 하는 상황이 발생했습니다.

테마에서 주입한 선택자보다 순위가 높아야 하는 상황입니다.
먼저 간단하게 바꿀 수 있는 지 확인해보겠습니다.

꼼수 1. 선택자를 적용해서 명시도를 높여보기

import { css } from "@emotion/react";


const selectWrapper = css`
 .rs-picker-menu {
 color: white;
 }
`;

꼼수 2. 다른 방법으로 명시도 높여보기

emotion 에서는 & 가 현재 컴포넌트를 의미해서 붙여쓰면 명시도가 높아지는데요

import { css } from "@emotion/react";


const selectWrapper = css`
 &.rs-picker-menu {
 color: white;
 }
`;

다른 선택자로 인해 오버라이딩 되어서 더 복잡한 상황이 발생했습니다.

역시 꼼수는 한계가 있습니다.
조금 더 좋은 패턴으로 풀어나갈 수 있도록 코드를 분석해보도록 하겠습니다.


시도 1

다크테마는 CustomProvider로 주입이 되니
해당 컴포넌트만 CustomProvider로 한번 더 감싸
라이트 테마를 주입할 수는 없을까?

<CustomProvider theme="dark">
</CustomProvider>

적용이 되지 않아서 CustomProvider 코드를 까봤더니 테마클래스가 body에 바로 꽂히게 되어 있고 useEffect 로 사용되고 있어 CustomProvider를 중첩해서 사용할 수는 없었습니다.

시도 2

select box 에 선택된 css 를 봤을 때 rs-theme-light , rs-theme-dark가 존재했고 className을 직접 주입해봤습니다.

<SelectPicker
  data={data}
  style={{ width: 224 }}
  className="rs-theme-light"
/>

select box 만 바뀌고 하위 메뉴는 테마가 바뀌지 않았는데요
elements로 확인해보니 하위 컴포넌트가 portal 로 열려서 적용되지 않았습니다.

시도 3

다시 공식문서로 가보겠습니다.
rsuite에서는 container 라는 속성을 제공하는데요.
컴포넌트가 렌더링될 컨테이너를 지정해주는 옵션입니다.

SSR 에서도 사용하기 위해서 ref 를 이용,
SelectPicker의 드롭다운 메뉴를 div 요소 내부에 표시하도록 해보겠습니다.

const ref = useRef(null);


<div ref={ref} className="rs-theme-light">
  <SelectPicker
    data={data}
    style={{ width: 224 }}
    container={() => {
    return ref.current;
    }}
  />
</div>

문제를 해결했습니다...!
하지만 라이브러리에서 공식적으로 제공하는 방법이 아니기 때문에
다른 개발자가 쉽게 사용하지 않도록 설명도 작성하고 추상화를 해주도록 하겠습니다.

추상화

import { useRef } from "react";
import { SerializedStyles } from "@emotion/react";

interface Props {
  children: React.ReactNode;
  css?: SerializedStyles;
}

const DangerousLightThemeWrapper = ({ children, css }: Props) => {
  const ref = useRef(null);

  return (
    <div ref={ref} className="rs-theme-light" css={css}>
      {children}
    </div>
  );
};

export default DangerousLightThemeWrapper;

위와 같이 기본 틀을 잡아줬고 이제 children 에 props 를 넘길 수만 있으면 됩니다.

<SelectPicker data={data} container={() => ref.current} />

cloneElement 로 children 에 props를 주입할 수 있지만
공식 문서에 레거시 api로 등록이 되어 있고 다른 방법을 추천하고 있습니다.

완성 코드

context api, hoc도 추천해주고 있지만 방법 자체는 render props가
rsuit를 사용하는 것과 비슷해서 제일 자연스러워 보였습니다.

그래서 render props로 코드를 작성해 완성했습니다.

import { useRef } from "react";
import { SerializedStyles } from "@emotion/react";

interface Props {
  children: (containerRef: () => HTMLElement) => React.ReactNode;
  style?: SerializedStyles;
}

/**
 * rsuite의 라이트 테마를 부분적으로 사용하기 위한 컴포넌트 입니다.
 * portal로 외부에서 열리는 컴포넌트까지 테마를 적용하기 위해 container 참조를 제공합니다.
 * 라이브러리에서 공식적으로 제공하지 않는 기능이므로, 사용에 주의해야 합니다.
 *
 * @typedef {Object} Props
 * @property {(containerRef: () => HTMLElement) => React.ReactNode} children
 *           컴포넌트에 포함될 자식 요소들을 렌더링하기 위한 함수.
 *           이 함수는 컨테이너 요소의 참조를 반환하는 함수를 인자로 받습니다.
 * @property {SerializedStyles} [style]
 *           @emotion/react 라이브러리를 사용하여 생성된 스타일 객체.
 *           이 스타일은 컴포넌트에 적용됩니다.
 */

const DangerousLightThemeWrapper = ({ children, style }: Props) => {
  const ref = useRef<HTMLDivElement>(null);

  return (
    <div ref={ref} className="rs-theme-light" css={style}>
      {children(() => ref.current)}
    </div>
  );
};

export default DangerousLightThemeWrapper;

// 실제 사용 예시
<DangerousLightThemeWrapper style={selectWrapper}>
  {(container) => <SelectPicker data={data} container={container} />}
</DangerousLightThemeWrapper>

위의 코드는 DangerousLightThemeWrapper를 사용하기 위해서는 인자로 넘어온 container를 SelectPicker에 주입해야한다라는 맥락이 추상화없이 노출되어서 아쉬웠습니다.
cloneElement를 사용하면 이 부분까지도 추상화할 수 있었는데요.

그냥 children으로 넘겨주기만 하면 light 테마가 적용되는 인터페이스가 DX에 좋을 것 같아
container props 를 추상화 해보기로 했습니다.

import { ComponentProps } from "react";
import { SelectPicker } from "rsuite";
import DangerousLightThemeWrapper from ".";

type RsuiteSelectPickerProps = ComponentProps<typeof SelectPicker>;

type Props = Omit<RsuiteSelectPickerProps, "container">;

const ForcedLightThemeSelectPicker = ({ ...props }: Props) => {
  return (
    <DangerousLightThemeWrapper>
      {(container) => (
        <SelectPicker {...props} container={container} />
      )}
    </DangerousLightThemeWrapper>
  );
};

export default ForcedLightThemeSelectPicker;

// 실사용 
<ForcedLightThemeSelectPicker
  data={projectTitleData}
  value={projectId}
  onChange={changeProjectId}
  size="lg"
/>

이렇게 추상화를 해두면 나중에 이렇게 해결하지 않아도 되는 솔루션을 찾았을 때
ForcedLightThemeSelectPicker 만 SelectPicker 로 바꿔주면 되어서 정상화시키키도 좋고
다른 개발자분에게 DangerousLightThemeWrapper를 사용하세요 + 저 컴포넌트가 children의 인자로 넘겨주는 container를 SelectPicker에 주입하세요라는 2개의 맥락을 전달하지 않고 ForcedLightThemeSelectPicker"를 쓰면 됩니다. 라는 맥락만 전달할 수 있어서
당장 문제를 해결하기는 더 편할 것 같습니다.

물론 이게 근본적인 문제 해결은 아니기 때문에 릴리즈가 끝나고 나면 추후에는 개선 방향성을 싱크할 예정입니다.

0개의 댓글