Jaeilit

토글버튼 만들어보기 본문

TIL

토글버튼 만들어보기

Jaeilit 2024. 6. 20. 01:53
728x90

 

미리보기

 

 

이 토글 버튼 UI는 처음엔 a,b 두개로만 동작을 해서 정말 토글버튼이였습니다만 리스트 갯수를 2개에서 더 많은 갯수로 늘려 다양하게 활용처에 사용하고싶은 마음에 코드를 수정한 결과 이런 결과물이 나왔습니다. 사실 토글버튼 보다 nav 버튼에 가깝지 않을까 싶지만 적당한 명칭을 정하지 못하여 최초의 이름인 토글로 지정하였습니다.

구조

컴포넌트마다 독립적인 구조를 위해서 폴더마다 tsx, hooks, provider, css, types 파일을 가지도록 했습니다.

📦components
 ┗ 📂toggle
 ┃ ┣ 📜hooks.ts
 ┃ ┣ 📜index.tsx
 ┃ ┣ 📜provider.tsx
 ┃ ┣ 📜styles.module.css
 ┃ ┗ 📜type.ts

 

- tsx: view 담당

- hooks: 로직 담당

- provider: 상태 담당

- type: 공통 상태 담당

 

1. tsx

 return (
    <div
      className={styles.toggleContainer}
      style={{ minWidth: `${toggleList.length * width}px` }}
    >
      {/* 음영 처리 */}
      <span
        data-index={currentIndex}
        className={styles.toggleSwitch}
        style={{ transform: `translateX(${currentIndex * width}px)` }}
      />

      {/* toggle 버튼 리스트 */}
      {toggleList.map(({ id, label, key }, idx) => (
        <button
          key={id}
          className={clsx(styles.toggleBtn, {
            [styles.textColor]: toggleState[idx],
          })}
          onClick={() => handleClick(id, label, key, idx)}
        >
          {label}
        </button>
      ))}
    </div>
  );

 

동작

span 태그는 파란 음영 처리를 하는 역할에 해당되는데 미리 설정해둔 최소 width 에 선택 된 index 만큼 곱해서 transform 으로 이동하는 원리입니다. 예를 들어 width를 80으로 지정해뒀다면 전체 길이가 3인 리스트의 min width 는 3*80인 240px이 되고 이동할때마다 0번째로 이동 시 translateX는 0 * 80 = 0 이되고 1은 80 2는 160이 됩니다. 마지막 선택은 인덱스가 3이니 240이 됩니다.

 

여기서 2가지 포인트가 있는데 첫번째로는 동적 스타일링을 위해서 인라인 스타일링을 사용했다는 것,

두번째로는 button list 에서 tag 를 button tag를 사용했다는것 입니다.

 

첫번째 같은 경우에는 css in js가 아닌 이상 동적 스타일링을 위해서는 인라인 스타일링이 불가피했습니다. 물론 전체 토글 리스트의 길이를 미리 정의해둔다면 각 사이즈마다 미리 css 를 작성해두는 방법도 있습니다. 예를들면 전체 길이를 5까지라고 가정하고 css 로 5까지의 경우의 수를 미리 만들어두는거죠,

 

인라인 스타일링은 CSP 정책에서도 보안상 취약점에도 해당되기에 지양해야하는 것이 맞습니다.

 

두번째로는 button tag 사용입니다. button tag 는 type attribute 에서도 명시되어있다시피 submit, reset, button 3가지로 구분 됩니다. 그렇기 때문에 항상 어떤 form 을 submit 하거나 reset 하는 경우 유저의 액션이 있을 때 사용하는 것이 올바른 사용처인데 지금의 UI로는 button tag 는 전혀 어울리지 않습니다. 차라리 a tag 나 router 에서 a tag 를 내부적으로 매핑한 Link가 어울리는데 이 부분에 대해서는 나중에 다형성 컴포넌트로 만들면서 변경하여 처리하도록 하겠습니다.

2. hooks

hooks 에서는 toggle swich 가 어떻게 동작되는지에 관한 상태를 관리하고 있습니다.

export const useToggleSwitch = (toggleList: ToggleType[]) => {
  const initState = Array(toggleList.length).fill(false);

  const [toggleState, setToggleState] = React.useState<boolean[]>(initState);

  const handleToggle = (idx: number) => {
    // 선택한 index 만 true 로 바꿔주기
    setToggleState((prev) => prev.map((_, prevIdx) => idx === prevIdx));
  };
  
  ....
};

 

 React.useEffect(() => {
    const init = toggleState.every((state) => !state);

    if (init) {
      setToggleState((prev) => [true, ...prev.slice(1)]);
    }
  }, [toggleState]);

 

혹시 모를 all false 일 경우를 대비해서 가장 첫번째에 해당하는 0번째를 true 로 변경하도록 하여 대비해줬습니다.

 

3. provider

상태를 좀 더 효율적으로 관리하기 위해서 context api 를 활용한 provider를 만들었습니다.

import { PropsWithChildren, createContext } from "react";
import { useToggleSwitch } from "./hooks";
import { ToggleType } from "./type";

interface ToggleContext {
  toggleList: ToggleType[];
}

type ToggleHooksReturnType = ReturnType<typeof useToggleSwitch>;

type ToggleContextProps = ToggleContext & ToggleHooksReturnType;

export const ToggleContext = createContext<ToggleContextProps>({
  handleToggle: () => {},
  toggleState: [],
  toggleList: [],
  currentValue: undefined,
});

interface ToggleProviderProps extends ToggleContext, PropsWithChildren {}

export const ToggleProvider = (props: ToggleProviderProps) => {
  const { toggleList, children } = props;

  const value = useToggleSwitch(toggleList);

  return (
    <ToggleContext.Provider value={value}>{children}</ToggleContext.Provider>
  );
};

 

간단한 UI라 코드가 많이 필요하지않지만 패턴만 본다면 아까 만들어둔 hooks를 여기서 호출하고 있습니다. 이렇게만 보면 역할이 MVC 중 module 에 가까워보입니다. store의 역할을 하는 것 처럼 보이기도 합니다.

4. tsx + provider

provider 로 tsx를 감싸는 컴포넌트입니다. 사용할때는 이 컴포넌트를 호출하여 사용하게 됩니다.

export const ToggleSwitchMain = () => {
  const { handleToggle, toggleState, toggleList, handleSelect } =
    React.useContext(ToggleContext);

  const width = 80;

  // actvie 된 index 찾기
  const findIdx = toggleState.findIndex((e) => e);

  // index 를 못찾았을 경우 (-1) 이면 0번째를 default 로 지정
  const currentIndex = findIdx !== -1 ? findIdx : 0;

  const handleClick = React.useCallback(
    (id: string, label: string, key: string, idx: number) => {
	  // toggle change 함수
	  handleToggle(idx);
      
      // 부모 컴포넌트에서 현재 셀렉터 된 row 가져갈 함수
      handleSelect({ id, label, key });
    },
    [handleToggle, handleSelect]
  );
  
  // ...tsx
}




export const ToggleSwitch = (props: ToggleSwitchProps) => {
  return (
    <ToggleProvider {...props}>
      <ToggleSwitchMain />
    </ToggleProvider>
  );
};

 

5. 사용하기

import { ToggleSwitch } from "../components/toggle";

const toggleList = [
  { id: "1", key: "key", label: "label1" },
  { id: "2", key: "key", label: "label2" },
  { id: "3", key: "key", label: "label3" },
];

const toggleList2 = [
  { id: "1", key: "key", label: "label1" },
  { id: "2", key: "key", label: "label2" },
  { id: "3", key: "key", label: "label3" },
  { id: "4", key: "key", label: "label4" },
  { id: "5", key: "key", label: "label5" },
];

export default function Root() {
  return (
    <div>
      <h2>3개</h2>
      <ToggleSwitch
        handleSelect={(select) => console.log(select, "select1")}
        toggleList={toggleList}
      />
      <hr />
      <h2>5개</h2>
      <ToggleSwitch
        handleSelect={(select) => console.log(select, "select2")}
        toggleList={toggleList2}
      />
    </div>
  );
}

선택

handleSelect의 console 값이 해당 셀렉터 된 버튼의 값만 받아오는데도 성공했습니다.

 

마무리

간단히 토글버튼을 만들어봤습니다. 간단한거라 코드가 많이 필요하진 않았지만 여러 패턴들을 연습해볼 수 있는 기회가 된 것 같습니다. 

728x90