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

grunfeld

v0.0.20

Published

Grunfeld는 React 애플리케이션을 위한 **간단하고 직관적인 대화상자 관리 라이브러리**입니다. 복잡한 상태 관리 없이 몇 줄의 코드로 모달, 알림, 확인 대화상자를 구현할 수 있습니다.

Readme

Grunfeld

Grunfeld는 React 애플리케이션을 위한 간단하고 직관적인 대화상자 관리 라이브러리입니다. 복잡한 상태 관리 없이 몇 줄의 코드로 모달, 알림, 확인 대화상자를 구현할 수 있습니다.

✨ 주요 특징

  • 🚀 간단한 API - 복잡한 설정 없이 바로 사용 가능
  • 🎯 동기/비동기 지원 - 알림부터 사용자 입력까지 모든 시나리오 지원
  • 📱 유연한 위치 설정 - 9분할 그리드로 정확한 위치 배치
  • 🔄 스마트 스택 관리 - 논리적인 LIFO 순서로 대화상자 관리
  • Top-layer 지원 - 네이티브 <dialog> 요소 활용
  • 🎨 완전한 커스터마이징 - 스타일과 동작 자유롭게 설정

📦 설치

npm install grunfeld
# 또는
yarn add grunfeld

🚀 빠른 시작

1. Provider 설정

앱의 최상위에 GrunfeldProvider를 추가하세요:

import { GrunfeldProvider } from "grunfeld";

function App() {
  return <GrunfeldProvider>{/* 앱 내용 */}</GrunfeldProvider>;
}

2. 기본 사용법

import { grunfeld } from "grunfeld";

function MyComponent() {
  const showAlert = () => {
    // 간단한 사용 - React 요소 직접 반환
    grunfeld.add(() => <div>안녕하세요!</div>);
  };

  return <button onClick={showAlert}>알림 표시</button>;
}

3. 사용자 응답 받기

const showConfirm = async () => {
  const result = await grunfeld.add<boolean>((removeWith) => ({
    element: (
      <div>
        <p>정말 삭제하시겠습니까?</p>
        <button onClick={() => removeWith(true)}>확인</button>
        <button onClick={() => removeWith(false)}>취소</button>
      </div>
    ),
  }));

  if (result) {
    console.log("사용자가 확인을 클릭했습니다");
  } else {
    console.log("사용자가 취소를 클릭했습니다");
  }
};

📖 주요 사용 패턴

알림 대화상자

타입 매개변수를 생략하면 응답을 기다리지 않는 간단한 알림으로 사용됩니다:

// 기본 알림 - React 요소 직접 반환
grunfeld.add(() => (
  <div
    style={{
      padding: "20px",
      background: "white",
      borderRadius: "8px",
      textAlign: "center",
    }}
  >
    <p>저장이 완료되었습니다!</p>
    <button onClick={() => grunfeld.remove()}>확인</button>
  </div>
));

확인 대화상자

사용자의 선택을 기다리는 확인 대화상자:

const confirmed = await grunfeld.add<boolean>((removeWith) => ({
  element: (
    <div
      style={{
        padding: "20px",
        background: "white",
        borderRadius: "8px",
        textAlign: "center",
      }}
    >
      <p>정말 삭제하시겠습니까?</p>
      <div>
        <button onClick={() => removeWith(true)}>삭제</button>
        <button onClick={() => removeWith(false)}>취소</button>
      </div>
    </div>
  ),
}));

if (confirmed) {
  console.log("사용자가 삭제를 확인했습니다");
  // 삭제 로직 실행
} else {
  console.log("사용자가 취소했습니다");
}

입력 대화상자

사용자로부터 데이터를 입력받는 대화상자:

const InputModal = ({ onClose }: { onClose: (name: string) => void }) => {
  const [name, setName] = useState("");

  return (
    <div
      style={{
        padding: "20px",
        background: "white",
        borderRadius: "8px",
        minWidth: "300px",
      }}
    >
      <h2>이름을 입력하세요</h2>
      <input
        autoFocus
        type="text"
        placeholder="이름"
        value={name}
        onChange={(e) => setName(e.target.value)}
        onKeyDown={(e) =>
          e.key === "Enter" && name.trim() && onClose(name.trim())
        }
        style={{ width: "100%", padding: "8px", marginBottom: "10px" }}
      />
      <div>
        <button
          onClick={() => name.trim() && onClose(name.trim())}
          disabled={!name.trim()}
          style={{ marginRight: "10px" }}
        >
          확인
        </button>
        <button onClick={() => onClose("")}>취소</button>
      </div>
    </div>
  );
};

export default function GrunfeldPage() {
  return (
    <button
      onClick={async () => {
        const value = await grunfeld.add<string>((removeWith) => ({
          element: <InputModal onClose={removeWith} />,
        }));
        console.log(value);
      }}
    >
      테스트 버튼
    </button>
  );
}

비동기 처리

대화상자 생성 시 비동기 작업도 수행할 수 있습니다:

const result = await grunfeld.add<string>(async (removeWith) => {
  // 로딩 표시
  const loadingElement = (
    <div style={{ padding: "20px", textAlign: "center" }}>
      <p>사용자 정보를 불러오는 중...</p>
      <div>⏳</div>
    </div>
  );

  // 먼저 로딩 다이얼로그 표시
  setTimeout(() => {
    // 실제 데이터 로드 후 내용 업데이트
    fetch("/api/user")
      .then((res) => res.json())
      .then((data) => {
        // 성공적으로 로드된 후의 UI로 업데이트하려면
        // 새로운 다이얼로그를 생성하거나 상태 관리를 사용해야 합니다
      })
      .catch(() => {
        removeWith("로드 실패");
      });
  }, 100);

  return {
    element: loadingElement,
  };
});

// 더 실용적인 예제: 선택 리스트
const selectedItem = await grunfeld.add<string>(async (removeWith) => {
  const items = await fetch("/api/items").then((res) => res.json());

  return {
    element: (
      <div style={{ padding: "20px", minWidth: "250px" }}>
        <h3>항목을 선택하세요</h3>
        <ul style={{ listStyle: "none", padding: 0 }}>
          {items.map((item: any) => (
            <li key={item.id}>
              <button
                onClick={() => removeWith(item.name)}
                style={{
                  width: "100%",
                  padding: "8px",
                  marginBottom: "4px",
                  textAlign: "left",
                }}
              >
                {item.name}
              </button>
            </li>
          ))}
        </ul>
        <button onClick={() => removeWith("")}>취소</button>
      </div>
    ),
  };
});

⚙️ 설정 옵션

Provider 옵션

<GrunfeldProvider
  options={{
    defaultPosition: "center",           // 기본 위치
    defaultLightDismiss: true,           // 배경 클릭으로 닫기
    defaultRenderMode: "inline",         // 렌더링 모드
    defaultBackdropStyle: {              // 기본 백드롭 스타일
      backgroundColor: "rgba(0, 0, 0, 0.5)"
    }
  }}
>

개별 대화상자 옵션

grunfeld.add(() => ({
  element: <MyDialog />,
  position: "top-right", // 위치 (9분할 그리드)
  lightDismiss: false, // 배경 클릭 비활성화
  renderMode: "top-layer", // top-layer 렌더링
  backdropStyle: {
    // 커스텀 백드롭
    backgroundColor: "rgba(0, 0, 0, 0.7)",
    backdropFilter: "blur(5px)",
  },
  dismissCallback: () => {
    // 닫힐 때 실행할 함수
    console.log("대화상자가 닫혔습니다");
  },
}));

// 스타일링 예제
grunfeld.add(() => ({
  element: (
    <>
      <h2>🎉 축하합니다!</h2>
      <p>작업이 성공적으로 완료되었습니다.</p>
      <button onClick={() => grunfeld.remove()}>확인</button>
    </>
  ),
  position: "center",
  backdropStyle: {
    backgroundColor: "rgba(102, 126, 234, 0.1)",
    backdropFilter: "blur(8px)",
  },
}));

📍 위치 시스템

화면을 9분할로 나누어 정확한 위치에 대화상자를 배치할 수 있습니다:

top-left     | top-center     | top-right
center-left  | center         | center-right
bottom-left  | bottom-center  | bottom-right

참고: 중앙 위치는 center 또는 center-center 모두 사용 가능합니다.

사용 예시:

// 중앙 배치 - 두 방식 모두 동일하게 작동
grunfeld.add(() => ({
  element: <Modal />,
  position: "center", // 또는 "center-center"
}));

// 우상단 알림
grunfeld.add(() => ({
  element: <Notification />,
  position: "top-right",
}));

// 하단 액션 시트
grunfeld.add(() => ({
  element: <ActionSheet />,
  position: "bottom-center",
}));

🎨 렌더링 모드

Inline 렌더링 (기본값)

  • z-index 기반의 안정적인 방식
  • 모든 브라우저 지원
  • 커스터마이징 유연함
  • JavaScript 기반 ESC 키 처리

Top-layer 렌더링

  • 네이티브 <dialog> 요소 사용
  • z-index 충돌 없음
  • 브라우저 네이티브 ESC 키 처리
  • 최신 브라우저만 지원 (Chrome 37+, Firefox 98+, Safari 15.4+)
grunfeld.add(() => ({
  element: <MyDialog />,
  renderMode: "top-layer", // 네이티브 dialog 사용
}));

🛠 대화상자 제거

// 가장 최근 대화상자 제거
grunfeld.remove();

// 모든 대화상자 제거
grunfeld.clear();

// ESC 키로 닫기
// 또는 lightDismiss: true일 때 배경 클릭으로 닫기

대화상자는 LIFO(Last In First Out) 순서로 제거됩니다. 이는 대화상자들 간의 맥락적 관계를 유지하기 위함입니다.

Promise 중단 처리

grunfeld.remove() 또는 grunfeld.clear()를 호출하여 대화상자를 강제로 닫으면, 해당 대화상자의 Promise는 undefined로 resolve됩니다:

// Promise가 진행 중일 때 외부에서 제거하는 경우
const promise = grunfeld.add<boolean>((removeWith) => ({
  element: (
    <div>
      <p>확인하시겠습니까?</p>
      <button onClick={() => removeWith(true)}>예</button>
      <button onClick={() => removeWith(false)}>아니오</button>
    </div>
  ),
}));

// 다른 곳에서 대화상자를 강제로 제거
setTimeout(() => {
  grunfeld.remove(); // promise는 undefined로 resolve됨
}, 1000);

const result = await promise; // result는 undefined
if (result === undefined) {
  console.log("대화상자가 중단되었습니다");
} else if (result) {
  console.log("사용자가 예를 선택했습니다");
} else {
  console.log("사용자가 아니오를 선택했습니다");
}

실용적인 예제:

const showConfirmWithTimeout = async () => {
  const confirmPromise = grunfeld.add<boolean>((removeWith) => ({
    element: (
      <div>
        <p>10초 안에 응답해주세요. 확인하시겠습니까?</p>
        <button onClick={() => removeWith(true)}>확인</button>
        <button onClick={() => removeWith(false)}>취소</button>
      </div>
    ),
  }));

  // 10초 후 자동으로 제거
  const timeoutId = setTimeout(() => {
    grunfeld.remove(); // Promise는 undefined로 resolve됨
  }, 10000);

  const result = await confirmPromise;
  clearTimeout(timeoutId); // 사용자가 응답한 경우 타이머 제거

  if (result === undefined) {
    console.log("시간 초과로 대화상자가 닫혔습니다");
  } else if (result) {
    console.log("사용자가 확인을 선택했습니다");
  } else {
    console.log("사용자가 취소를 선택했습니다");
  }
};

이 동작은 메모리 누수를 방지하고 hanging Promise 문제를 해결합니다. 모든 Promise는 적절히 정리되므로 안전하게 사용할 수 있습니다.

🎯 실제 사용 예제

완전한 컴포넌트 예제

import React, { useState } from "react";
import { grunfeld, GrunfeldProvider } from "grunfeld";

function MyApp() {
  const [message, setMessage] = useState("");

  const showNotification = () => {
    grunfeld.add(() => ({
      element: <div>알림이 표시되었습니다!</div>,
      position: "top-right",
    }));

    // 2초 후 자동으로 제거
    setTimeout(() => grunfeld.remove(), 2000);
  };

  const showConfirm = async () => {
    const result = await grunfeld.add<boolean>((removeWith) => ({
      element: (
        <div
          style={{ padding: "20px", background: "white", borderRadius: "8px" }}
        >
          <h3>확인</h3>
          <p>정말 진행하시겠습니까?</p>
          <button onClick={() => removeWith(true)}>예</button>
          <button onClick={() => removeWith(false)}>아니오</button>
        </div>
      ),
    }));

    setMessage(result ? "확인됨" : "취소됨");
  };

  const showInput = async () => {
    const input = await grunfeld.add<string>((removeWith) => ({
      element: <InputDialog onSubmit={removeWith} />,
    }));

    setMessage(input ? `입력값: ${input}` : "취소됨");
  };

  return (
    <GrunfeldProvider>
      <div style={{ padding: "20px" }}>
        <h1>Grunfeld 예제</h1>
        <button onClick={showNotification}>알림 표시</button>
        <button onClick={showConfirm}>확인 대화상자</button>
        <button onClick={showInput}>입력 대화상자</button>
        <p>상태: {message}</p>
      </div>
    </GrunfeldProvider>
  );
}

const InputDialog = ({ onSubmit }: { onSubmit: (value: string) => void }) => {
  const [value, setValue] = useState("");

  return (
    <div style={{ padding: "20px", background: "white", borderRadius: "8px" }}>
      <h3>입력</h3>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="값을 입력하세요"
        autoFocus
      />
      <div style={{ marginTop: "10px" }}>
        <button onClick={() => onSubmit(value)}>확인</button>
        <button onClick={() => onSubmit("")}>취소</button>
      </div>
    </div>
  );
};

📋 API 참조

grunfeld.add<T>(dialogFactory)

매개변수:

  • dialogFactory: 대화상자를 생성하는 함수
    • (removeWith: (data: T) => void) => GrunfeldProps | Promise<GrunfeldProps>
    • GrunfeldProps는 다음 중 하나:
      • React 요소 직접 반환: React.ReactNode
      • 옵션이 포함된 객체: { element: React.ReactNode; position?: Position; ... }

반환값:

  • 항상 Promise<T> 반환 (내부적으로 TypeScript 조건부 타입 처리)

사용 예시:

// 1. 간단한 사용법 - React 요소 직접 반환
grunfeld.add(() => <div>간단한 알림</div>);

// 2. 옵션과 함께 사용 - 객체 반환
grunfeld.add(() => ({
  element: <div>위치가 지정된 알림</div>,
  position: "top-right",
  lightDismiss: false,
}));

// 3. 사용자 응답 받기
const result = await grunfeld.add<boolean>((removeWith) => ({
  element: (
    <div>
      <p>확인하시겠습니까?</p>
      <button onClick={() => removeWith(true)}>예</button>
      <button onClick={() => removeWith(false)}>아니오</button>
    </div>
  ),
}));

GrunfeldProps:

{
  element: React.ReactNode;              // 표시할 내용
  position?: Position;                   // 위치 (기본: "center")
  lightDismiss?: boolean;                // 배경 클릭으로 닫기 (기본: true)
  backdropStyle?: React.CSSProperties;   // 백드롭 스타일
  dismissCallback?: () => unknown;       // 닫힐 때 실행할 함수 (주의: 여기서 grunfeld.remove() 호출 금지)
  renderMode?: "inline" | "top-layer";   // 렌더링 모드
}

⚠️ 중요: dismissCallback은 대화상자가 제거될 때 실행되므로, 이 함수 내에서 grunfeld.remove()grunfeld.clear()를 호출하면 안 됩니다. 자동으로 사라지는 알림을 만들려면 setTimeout을 대화상자 생성 후에 별도로 실행하세요:

// ❌ 잘못된 방법
grunfeld.add(() => ({
  element: <div>알림</div>,
  dismissCallback: () => {
    setTimeout(() => grunfeld.remove(), 2000); // 무한 루프 위험
  },
}));

// ✅ 올바른 방법
grunfeld.add(() => ({
  element: <div>알림</div>,
}));
setTimeout(() => grunfeld.remove(), 2000);

grunfeld.remove()

가장 최근 대화상자를 제거합니다.

grunfeld.clear()

모든 대화상자를 제거합니다.

Position 타입

type PositionX = "left" | "center" | "right";
type PositionY = "top" | "center" | "bottom";

type Position = `${PositionY}-${PositionX}` | "center";

🌐 브라우저 호환성

Inline 렌더링: 모든 모던 브라우저 + IE 11+ Top-layer 렌더링: Chrome 37+, Firefox 98+, Safari 15.4+, Edge 79+