Ayden's journal

유틸리티 컴포넌트 : Switch & Match

이전에 [ 유틸리티 컴포넌트 : Show & Map ]에서 잠깐 언급했었던 것처럼, 나는 꽤 오래 전부터 여러 조건을 일괄적으로 처리하는 Switch와 Match 컴포넌트를 만들고 싶어했다. 대강 어떻게 만들지에 대한 아이디어는 여럿 있었지만, 실제로 구현하려니 문제가 산더미였다.

처음에는 타입스크립트를 사용하려 했다. 그러나 수많은 시행 착오 끝에 나는 [ 타입스크립트에서의 컴포넌트 추론 ]의 결론에 도달할 수 있었고, 타입스크립트를 사용하여 Switch 컴포넌트를 정의하려는 나의 아이디어는 물거품이 되어버렸다. 머리를 열심히 굴려보았지만 방법이 없었다. 나는 런타임에서 에러를 던지는 것으로 스스로와 타협했다.

 

그렇게 만들어진 Switch 컴포넌트는 아래와 같다. 런타임에서 ReactElement는 객체가 되는데, 이러한 점을 이용해 children으로 들어온 ReactElement의 key가 중복되는 것이 있는지, Match 컴포넌트만을 사용하였는지 등을 확인하고, when과 일치하는 key를 가진 Match 컴포넌트를 랜더링한다. 일치하는 key가 없다면 fallback을 랜더링한다.

export type SwitchProps = {
  children: Array<ReactElement>,
  when: Key,
  fallback?: ReactNode
}

export function Switch({ children, when, fallback = null }: SwitchProps) {
  children.reduce((acc, value) => {
    if (value.type !== Match) throw new Error("Match 컴포넌트만 사용할 수 있습니다.");

    if (value.key) {
      if (acc.includes(value.key)) throw new Error(`Duplicate Match key: ${value.key}`);
      else acc.push(value.key)
    }

    return acc
  }, [] as Array<Key>)

  return children.find(value => value.key === when) ?? fallback
}

 

Match 컴포넌트는 간단하다. element 혹은 children으로 ReactNode를 받으며, 둘 중에 하나를 랜더링한다. ReactNode를 어떤 프로퍼티로 넘길 것인지는 개발자가 알아서 선택하면 되고, 둘 다 선택하면 타입 에러가 발생한다.

export type MatchProps = {
  children: ReactNode,
  element?: never
} | {
  children?: never,
  element: ReactNode
}

export const Match = ({ children, element }: MatchProps) => children ?? element;

 

ReactNode를 넘겨주는 props를 하나로 통일하지 않은 이유는 개발자 편의성과 코드 가독성 때문이다. 간단한 JSX를 사용하거나 외부에서 컴포넌트를 import 하는 경우라면 element로 전달하여 아래와 같이 Match를 한 줄로 처리하는 게 코드 가독성에도 좋고 개발자도 편하다. 그러나 JSX가 조금이라도 복잡해진다면 children으로 넘겨주는 게 더 나을 수 있다.

어느 쪽이든 대부분의 경우 개발자에게 선택권을 쥐여주는 것이 그렇지 않는 것보다 좋다고 생각한다. 

type CompProps = { currentStatus: 'loading' | 'error' | 'success' }
 
export default function Comp({ currentStatus }: CompProps) {
  return (
    <div>
      <h1>Condition Rendering with Switch & Match</h1>
      
      <Switch when={currentStatus} fallback={<p>Unknown Status</p>}>
        <Match key="loading" element={<p>Loading...</p>} />
        <Match key="error" element={<p>Error occurred!</p>} />
        <Match key="success" element={<p>Data loaded successfully!</p>} />
      </Switch>
    </div>
  );
};

 

 

이런 유틸리티 컴포넌트를 만들고 프로젝트에서 쓰는 건 좋은데, 새로운 프로젝트를 시작할 때마다 파일을 복붙하려니 귀찮아서 이전의 Show & Map 컴포넌트와 합쳐서 이를 utilinent라는 이름의 라이브러리로 npm에 배포하였다. 덕분에 유틸리티 컴포넌트를 한 곳에서 발전시켜나갈 수 있고 각각의 프로젝트에서는 최신 버전의 utilinent를 다운받기만 하면 된다.

이를 계기로 라이브러리라는 개념에 대해 다시 생각해볼 수 있었다. "라이브러리"라고 하면 나는 react 나 Zustand, 혹은 Tanstack-Query와 같이 많은 사람들에게 쓰이는 코드 묶음이라는 생각이 먼저 떠오른다. 하지만 라이브러리가 반드시 그래야 할 필요는 어디에도 없다. 물론 utilinent가 많은 사람들에게 사용된다면 아주 기쁘겠지만, 그렇지 않더라도 이미 소기의 목적은 달성한 셈이다. 야호!

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기