npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-flowify

v0.1.3

Published

Declarative JSX components replacing React imperative patterns

Readme

react-flowify

React 컴포넌트에서 반복적으로 사용되는 명령형(imperative) 패턴을 선언형(declarative) JSX 컴포넌트로 대체하기 위해 만든 라이브러리입니다.

npm license

설치

npm install react-flowify
# or
pnpm add react-flowify
# or
yarn add react-flowify

왜 만들었는가

React 개발 시 조건부 렌더링, 리스트 반복, 비동기 상태 처리 등에서 삼항 연산자, && 연산자, switch문 등 명령형 패턴이 반복적으로 사용됩니다. 이런 패턴은 단순할 때는 괜찮지만, 조건이 중첩되거나 복잡해지면 가독성이 급격히 떨어집니다.

react-flowify는 이러한 패턴들을 선언형 JSX 컴포넌트로 감싸서, 코드의 의도를 명확하게 드러내고 일관된 구조를 유지하는 것을 목표로 합니다.

조건 분기, 리스트 반복, 비동기 처리 같은 흐름 제어를 JavaScript 로직이 아닌 JSX 컴포넌트에 맡기면, 컴포넌트의 렌더링 구조가 곧 UI의 흐름이 됩니다. 개발자는 "어떻게 분기할 것인가"가 아니라 "무엇을 보여줄 것인가"에 집중할 수 있습니다.


컴포넌트 목록

Show

단순 조건부 렌더링을 선언적으로 처리합니다.

삼항 연산자나 && 연산자는 조건이 단순할 때는 문제가 없지만, 조건이 여러 개 중첩되면 JSX 트리 안에서 어디가 조건이고 어디가 결과인지 구분하기 어려워집니다. Show는 조건과 fallback을 props로 분리하여 렌더링 의도를 명확하게 드러냅니다.

AS-IS

{isLoggedIn ? <Dashboard /> : <LoginButton />}

TO-BE

<Show when={isLoggedIn} fallback={<LoginButton />}>
  <Dashboard />
</Show>

Guard

조건부 렌더링과 동시에 타입 내로잉(type narrowing)을 수행합니다. when에 전달된 값이 truthy일 때, NonNullable<T> 타입으로 좁혀진 값을 render prop으로 전달합니다.

기존 삼항 연산자로 nullable 값을 처리하면, 분기 이후에도 TypeScript가 해당 값을 여전히 nullable로 추론하는 경우가 있어 불필요한 옵셔널 체이닝이나 단언(!)이 필요했습니다. Guard는 render prop을 통해 NonNullable<T> 타입을 보장하므로, 추가적인 타입 처리 없이 안전하게 값을 사용할 수 있습니다.

AS-IS

{user ? <Profile name={user.name} /> : <Login />}

TO-BE

<Guard when={user} fallback={<Login />}>
  {(u) => <Profile name={u.name} />}
</Guard>

Show와의 차이점: Show는 단순히 boolean 조건만 판단하지만, Guard는 값 자체를 검증하고 타입까지 좁혀서 전달합니다. nullable한 데이터를 안전하게 사용해야 할 때 적합합니다.

TypeScript 제네릭 사용

interface User {
  id: number;
  name: string;
  email: string;
}

const user: User | null = getUser();

// 제네릭을 명시하면 render prop의 인자가 User 타입으로 보장됩니다.
<Guard<User> when={user} fallback={<Login />}>
  {(u) => <Profile name={u.name} email={u.email} />}
</Guard>

Switch

여러 상태 값에 따라 다른 UI를 렌더링합니다. switch문이나 중첩 삼항 연산자를 대체합니다.

기존에는 상태 값이 3개 이상이면 삼항 연산자를 중첩하거나 && 조건을 나열해야 했고, switch문을 쓰려면 JSX 밖에서 별도 함수로 분리해야 했습니다. Switch는 value-to-UI 매핑을 하나의 객체로 표현하여 상태별 렌더링을 한눈에 파악할 수 있게 합니다. 또한 제네릭으로 타입을 명시하면 누락된 케이스를 컴파일 타임에 잡아낼 수 있습니다.

AS-IS

{status === "idle" && <IdleView />}
{status === "loading" && <Spinner />}
{status === "error" && <ErrorView />}
{status === "success" && <SuccessView />}

TO-BE

<Switch
  value={status}
  by={{
    idle: <IdleView />,
    loading: <Spinner />,
    error: <ErrorView />,
    success: <SuccessView />,
  }}
/>

TypeScript 제네릭 사용

type Status = "idle" | "loading" | "error" | "success";

// 제네릭을 명시하면 by 객체의 키가 Status 타입으로 제한되어
// 오타나 누락 시 컴파일 에러가 발생합니다.
<Switch<Status>
  value={status}
  by={{
    idle: <IdleView />,
    loading: <Spinner />,
    error: <ErrorView />,
    success: <SuccessView />,
  }}
/>

Each

배열 순회를 선언적으로 처리하며, 인덱스 외에도 isFirst, isLast, length 등의 메타 정보를 제공합니다.

기존 .map() 패턴은 빈 배열 처리를 별도의 삼항 연산자로 감싸야 하고, 첫 번째/마지막 아이템 여부를 판단하려면 index === 0, index === arr.length - 1 같은 계산을 매번 직접 해야 했습니다. Each는 빈 배열 fallback을 renderEmpty prop으로 분리하고, 위치 메타 정보를 자동으로 제공하여 반복 렌더링 로직을 간결하게 만듭니다.

AS-IS

{users.length === 0 ? (
  <p>유저가 없습니다.</p>
) : (
  users.map((user, index) => <UserCard key={user.id} user={user} />)
)}

TO-BE

<Each items={users} renderEmpty={<p>유저가 없습니다.</p>}>
  {(user, { index, isFirst, isLast }) => (
    <UserCard key={user.id} user={user} highlight={isFirst} />
  )}
</Each>

TypeScript 제네릭 사용

interface User {
  id: number;
  name: string;
  role: "admin" | "member";
}

// 제네릭을 명시하면 render prop의 item이 User 타입으로 추론됩니다.
<Each<User> items={users} renderEmpty={<p>유저가 없습니다.</p>}>
  {(user, { isFirst, isLast }) => (
    <UserCard key={user.id} name={user.name} role={user.role} />
  )}
</Each>

Use

React 19의 use() 훅을 활용하여 Promise를 선언적으로 언래핑합니다. 반드시 Suspense로 감싸서 사용해야 합니다.

React 19의 use() 훅은 Promise와 Context를 모두 읽을 수 있지만, Promise를 언래핑할 때는 Suspense 내부에서 사용해야 하므로 별도의 자식 컴포넌트를 만들어야 합니다. Use 컴포넌트는 이 불필요한 래퍼 컴포넌트 생성을 제거합니다.

AS-IS

// use()를 쓰기 위해 별도의 래퍼 컴포넌트를 만들어야 함
function UserContent({ promise }: { promise: Promise<User> }) {
  const user = use(promise);
  return <UserCard name={user.name} />;
}

function Page() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserContent promise={fetchUser(id)} />
    </Suspense>
  );
}

TO-BE

<Suspense fallback={<Spinner />}>
  <Use promise={fetchUser(id)}>
    {(user) => <UserCard name={user.name} />}
  </Use>
</Suspense>

TypeScript 제네릭 사용

interface User {
  id: number;
  name: string;
}

// 제네릭을 명시하면 render prop의 인자가 User 타입으로 보장됩니다.
<Suspense fallback={<Spinner />}>
  <Use<User> promise={fetchUser(id)}>
    {(user) => <UserCard name={user.name} />}
  </Use>
</Suspense>

AsyncBoundary

SuspenseErrorBoundary를 하나로 결합한 컴포넌트입니다.

기존에는 비동기 컴포넌트를 감쌀 때 ErrorBoundarySuspense를 각각 중첩해야 했고, 이 두 가지의 순서나 구조가 프로젝트 내에서 일관되지 않는 경우가 많았습니다. AsyncBoundary는 로딩 상태와 에러 상태 처리를 하나의 컴포넌트로 통합하여 비동기 경계의 구조를 일관되게 유지할 수 있습니다.

AS-IS

<ErrorBoundary fallback={<div>에러 발생</div>}>
  <Suspense fallback={<Skeleton />}>
    <DataView />
  </Suspense>
</ErrorBoundary>

TO-BE

<AsyncBoundary
  suspense={{ fallback: <Skeleton /> }}
  errorBoundary={{
    fallback: <div>에러 발생</div>,
    onError: (error) => console.error(error),
    resetKeys: [queryKey],
  }}
>
  <DataView />
</AsyncBoundary>

Responsive

CSS 미디어 쿼리 기반으로 조건부 렌더링을 수행합니다. 모바일/데스크탑 프리셋 컴포넌트를 제공합니다.

기존에는 반응형 분기를 위해 컴포넌트마다 useState, useEffect, matchMedia 리스너를 직접 작성해야 했고, 뷰포트 판별 로직이 비즈니스 로직과 섞여 컴포넌트가 복잡해졌습니다. Responsive는 미디어 쿼리 관리를 컴포넌트 외부로 분리하여, 모바일/데스크탑 분기를 신경 쓰지 않고 렌더링 로직에만 집중할 수 있게 합니다.

AS-IS

const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
  const mql = window.matchMedia("(max-width: 767px)");
  setIsMobile(mql.matches);
  const handler = (e) => setIsMobile(e.matches);
  mql.addEventListener("change", handler);
  return () => mql.removeEventListener("change", handler);
}, []);

{isMobile ? <MobileNav /> : <DesktopNav />}

TO-BE

<Responsive.Mobile>
  <MobileNav />
</Responsive.Mobile>

<Responsive.Desktop>
  <DesktopNav />
</Responsive.Desktop>

{/* 커스텀 쿼리도 가능 */}
<Responsive query="(min-width: 1024px)">
  <WideLayout />
</Responsive>

OutsideClick

요소 외부 클릭을 감지하여 콜백을 실행합니다. 드롭다운, 모달, 팝오버 닫기 등에 활용됩니다.

기존에는 외부 클릭 감지를 위해 useRef, useEffect, document.addEventListener를 조합한 보일러플레이트를 매번 작성해야 했고, 이벤트 리스너의 등록/해제 관리를 빠뜨리면 메모리 누수나 의도치 않은 동작이 발생했습니다. OutsideClick은 이 반복적인 패턴을 하나의 컴포넌트로 추상화하여 선언적으로 사용할 수 있게 합니다.

AS-IS

const ref = useRef(null);

useEffect(() => {
  const handler = (e) => {
    if (ref.current && !ref.current.contains(e.target)) {
      setOpen(false);
    }
  };
  document.addEventListener("mousedown", handler);
  return () => document.removeEventListener("mousedown", handler);
}, []);

<div ref={ref}>
  <Dropdown />
</div>

TO-BE

<OutsideClick onOutsideClick={() => setOpen(false)}>
  <Dropdown />
</OutsideClick>

Throw

렌더 시점에 즉시 에러를 throw합니다.

기존에는 특정 조건에서 ErrorBoundary를 트리거하려면 컴포넌트 내부에서 명령적으로 throw를 호출해야 했고, 이를 위한 별도 컴포넌트를 만들거나 상태 관리가 필요했습니다. Throw는 JSX 트리 안에서 선언적으로 에러를 발생시킬 수 있어, 테스트나 조건부 에러 처리를 간결하게 표현할 수 있습니다.

<ErrorBoundary fallback={<ErrorPage />}>
  <Throw error={new Error("Something went wrong")} />
</ErrorBoundary>

Suspend

렌더 시 무한히 suspend 상태를 유지합니다.

기존에는 Suspense의 fallback UI를 확인하려면 실제 비동기 요청을 지연시키거나 네트워크를 throttle해야 했습니다. Suspend는 의도적으로 무한 suspend 상태를 만들어, Storybook이나 테스트 환경에서 로딩 UI를 간편하게 시연하고 검증할 수 있습니다.

<Suspense fallback={<Skeleton />}>
  <Suspend />
</Suspense>

Peer Dependencies

  • react >= 19
  • react-dom >= 19

License

MIT