Ayden's journal

Utilinent - 유틸리티 컴포넌트 라이브러리

[ Github / NPM ]

 

Utilinent는 SolidJS의 간결하고 직관적인 Control Flow API를 React에 도입하고 발전시켜, 개발자들이 더욱 생산적이고 효율적으로 작업할 수 있도록 돕는 것을 목표로 합니다. React에서 자주 사용되는 패턴과 조건부 렌더링 로직을 컴포넌트로 감싸는 방식으로 재사용성을 높이며, 이를 통해 대규모 프로젝트에서도 코드 스타일을 일관되게 유지할 수 있습니다. 또한, 가독성이 뛰어난 코드를 작성하고 복잡한 로직을 단순화하여 유지보수를 더욱 편리하게 만듭니다.

 

install 및 import

npm i utilinent
import { Show, For, Switch, Match, Mount } from "utilinent"

 

Show

React에서 조건부 랜더링을 처리하는 방식은 삼항 연산자, 널 병합 연산자 또는 AND 연산자 등 무궁무진합니다. 이러한 다양성은 코드 스타일을 일관적으로 유지하는 것을 어렵게 만듭니다. 특히 복잡한 조건부 로직이 추가되면 코드는 중첩되고 가독성이 떨어지며, 유지보수가 어려워질 수 있습니다. Show 컴포넌트는 이러한 문제를 해결하며 대규모 프로젝트에서도 코드의 명확성과 재사용성을 동시에 고려한 일관된 접근법을 제공합니다.

function Show<T>({
  when: T;
  fallback?: ReactNode;
  children: ReactNode | ((item: T) => ReactNode);
}): ReactNode;
// 삼항 연산자 대체
<Show when={isLoggedIn} fallback={<LoginButton />}>
  <UserProfile />
</Show>

// AND 연산자 대체
<Show when={isLoading}>
  <Spinner />
</Show>

// OR 연산자 대체
<Show when={value} fallback={<DefaultValue />}>
  {(item) => item}
</Show>

 

For

React에서 배열을 렌더링할 때 흔히 사용하는 방식은 Array.prototype.map 메서드입니다. 이 방식은 배열의 각 요소를 순회하며 렌더링할 수 있는 강력한 도구지만, 몇 가지 단점도 존재합니다. JSX 코드와 배열 순회 로직이 밀접하게 얽혀 있어 코드가 복잡해지고, 특히 빈 배열을 처리해야 할 경우 추가적인 조건문이 필요해 가독성과 유지보수성이 떨어질 수 있습니다. For 컴포넌트는 이러한 문제를 해결하고, 배열 랜더링과 빈 배열 처리를 간소화하며, 선언적인 코드 작성 방식을 지원합니다.

function For<T extends Array<unknown>>({
  each: T | null | undefined; 
  fallback?: ReactNode;
  children: (item: T[number], index: number) => ReactNode;
}): ReactNode;

each에 제공된 값이 undefined 혹은 null일 경우, For 컴포넌트는 지정된 fallback 콘텐츠를 렌더링하여 오류를 방지합니다.

// type of users is Array<{ id: number, name: string }> | undefined
const { data: userList } = useQuery({ ... })

<Map each={userList} fallback={<p>No users available.</p>}>
  {({ id, name }) => <p key={id}>User: {name}</p>}
</Map>

 

Switch & Match

랜더링 조건이 여럿이라면 보통은 if문이나 switch문을 사용하게 됩니다. 하지만 조건이 복잡해질 수록 코드가 쉽게 복잡해지며, 가독성은 떨어지고 유지보수하기 어려워집니다. Switch와 Match 컴포넌트는 이러한 문제를 해결하고, 조건부 렌더링을 더 간결하고 선언적으로 처리할 수 있는 방법을 제공합니다.

type Case = string | number | boolean | null | undefined;

function Switch({
  when: Case;
  fallback?: ReactNode;
  children: Array<ReactElement>;
}): ReactNode;

function Match({
  case: Case;
  children: ReactNode;
  element?: never;
} | {
  case: Case;
  children?: never;
  element: ReactNode;
}): ReactNode;

Switch는 주어진 when 값에 맞는 첫 번째 Match 컴포넌트를 찾아 렌더링합니다. Match 컴포넌트는 case prop으로 각 조건을 정의하며, 조건에 맞는 콘텐츠를 children이나 element 중 원하는 방식으로 제공할 수 있습니다. 아래는 switch문을 사용하여 조건부 랜더링을 처리하는 코드와, 동일한 로직을 Switch와 Match 컴포넌트로 다시 작성한 것입니다.

export function SearchTitle(props: { searchType: SearchType, keyword: string }) {
  switch (props.searchType) {
    case "report":
      return <SearchReport />

    case "book":
      return <SearchBook />;

    case "tag":
      return <SearchTag />;
  }
};
export function SearchTitle(props: { searchType: SearchType, keyword: string }) {
  return (
    <Switch when={props.searchType}>
      <Match case="report" element={<SearchReport />} />
      <Match case="book" element={<SearchBook />} />
      <Match case="tag" element={<SearchTag />} />
    </Switch>
  )
};

Switch와 Match 컴포넌트를 사용할 때는 몇 가지 주의할 점이 있습니다. Switch에는 둘 이상의 Match 컴포넌트를 제공해야 하며, Match 컴포넌트 외의 ReactElement는 사용할 수 없습니다. 또한, 개발자는 각 Match 컴포넌트의 case 값을 명확하고 고유하게 정의하여 예측 가능한 동작을 유지해야 합니다. 이를 위반하면 코드의 가독성과 안정성이 저하될 수 있으므로 각별한 주의가 필요합니다.

 

Mount

Next.js 프로젝트에서는 DOM이 마운트된 이후에만 특정 코드를 실행하거나 UI를 렌더링해야 하는 경우가 있습니다. 이를 위해 보통 useEffect와 useState를 사용하여 컴포넌트가 마운트되었는지 확인합니다. 그러나 이 패턴을 여러 컴포넌트에서 반복적으로 구현하면 코드의 가독성과 유지보수성이 저하될 수 있습니다. Mount 컴포넌트는 이러한 과정을 추상화하여, 간결하고 재사용 가능한 방식으로 관리할 수 있도록 돕습니다.

function Mount({
  fallback?: ReactNode;
  children: ReactNode | (() => ReactNode | Promise<ReactNode>);
}): ReactNode;

Mount 컴포넌트는 ReactNode를 리턴하는 비동기 함수를 children으로 받을 수 있습니다. 이 경우, 비동기 함수가 반환하는 ReactNode가 렌더링되기 전까지는 fallback으로 전달된 컴포넌트를 화면에 표시합니다. 이는 데이터 로딩이나 비동기 작업이 필요한 UI를 처리할 때 유용하며, 비동기 작업이 완료된 후에만 UI가 렌더링되도록 제어할 수 있습니다. 아래는 useState와 useEffect를 사용하여 마운트 상태와 비동기 작업 결과를 관리하는 코드와, 동일한 로직을 Mount 컴포넌트로 다시 작성한 것입니다.

export default function Comp() {
  const [content, setContent] = useState("Loading...");
   
  useEffect(() => {
    const loadContent = async () => {
      await new Promise(resolve => setTimeout(resolve, 1000));
      setContent("Loaded!");
    };
    loadContent();
  }, []);
   
  return <div>{content}</div>;
}
export default function Comp() {
  return (
    <Mount fallback={<div>Loading...</div>}>
      {async () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
      
        return <div>Loaded!</div>
      }}
    </Mount>
  )
}

앞서 언급된 다른 유틸리티 컴포넌트들과 달리 Mount 컴포넌트는 내부적으로 state를 사용하기에 Next.js의 app router에서는 클라이언트 컴포넌트로 취급됩니다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기