drawer-kit
v1.0.3
Published
A declarative React drawer management library inspired by toss/overlay-kit
Maintainers
Readme
🎯 Drawer-Kit
React용 선언적 drawer 관리 라이브러리 - toss/overlay-kit에서 영감을 받아 제작
간단하고 직관적인 API로 drawer 컴포넌트를 관리할 수 있는 강력하고 유연한 React 라이브러리입니다. vaul을 기반으로 구축되었으며, toss/overlay-kit의 디자인에서 영감을 받았습니다.
✨ 주요 기능
- 🎨 선언적 API - 간단하고 직관적인 drawer 관리
- 🚀 TypeScript 우선 - 완전한 TypeScript 지원과 뛰어난 개발자 경험
- 📱 모바일 최적화 - 부드러운 모바일 상호작용을 위한 vaul 기반
- 🎭 다양한 방향 - 상단, 하단, 좌측, 우측 drawer 지원
- 🔄 비동기 지원 - Promise 기반 drawer 상호작용
- 🎪 이벤트 시스템 - drawer 생명주기 (lifecycle) 에 대한 이벤트 콜백
📦 설치
npm install drawer-kit
# 또는
yarn add drawer-kit
# 또는
pnpm add drawer-kit🚀 빠른 시작
1. Provider 설정
앱을 DrawerProvider로 감싸세요:
import { DrawerProvider } from "drawer-kit";
function App() {
return (
<DrawerProvider>
<YourApp />
</DrawerProvider>
);
}2. 기본 사용법
import { drawer } from "drawer-kit";
function MyComponent() {
const openDrawer = () => {
drawer.open(({ close, unmount }) => (
<div style={{ padding: "20px" }}>
<h2>안녕하세요!</h2>
<p>간단한 drawer 예제입니다.</p>
<button onClick={close}>닫기</button>
<button onClick={unmount}>언마운트</button>
</div>
));
};
return <button onClick={openDrawer}>Drawer 열기</button>;
}3. 비동기 Drawer
import { drawer } from "drawer-kit";
function MyComponent() {
const openAsyncDrawer = async () => {
try {
const result = await drawer.openAsync(({ close, unmount }) => (
<div style={{ padding: "20px" }}>
<h2>작업 확인</h2>
<p>진행하시겠습니까?</p>
<button onClick={() => close("confirmed")}>예</button>
<button onClick={() => close("cancelled")}>아니오</button>
</div>
));
console.log("사용자 선택:", result); // 'confirmed' 또는 'cancelled'
} catch (error) {
console.log("취소되었습니다");
}
};
return <button onClick={openAsyncDrawer}>비동기 Drawer 열기</button>;
}📚 API 참조
핵심 API
drawer.open(controller, options?)
주어진 컨트롤러 컴포넌트로 drawer를 엽니다.
const drawerId = drawer.open(MyDrawerComponent, {
direction: "bottom",
modal: true,
dismissible: true,
});drawer.openAsync(controller, options?)
drawer를 열고 결과와 함께 Promise를 반환합니다.
const result = await drawer.openAsync(MyAsyncDrawerComponent, {
direction: "right",
modal: false,
});drawer.close(drawerId)
ID로 특정 drawer를 닫습니다.
drawer.close("drawer-123");drawer.unmount(drawerId)
ID로 특정 drawer를 언마운트합니다.
drawer.unmount("drawer-123");drawer.closeAll()
모든 열린 drawer를 닫습니다.
drawer.closeAll();drawer.unmountAll()
모든 열린 drawer를 언마운트합니다.
drawer.unmountAll();DrawerOptions
| 옵션 | 타입 | 기본값 | 설명 |
| ------------------ | ---------------------------------------- | --------------- | ------------------------------------- |
| direction | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | drawer 열리는 방향 |
| modal | boolean | true | 모달 여부 (배경 상호작용 차단) |
| dismissible | boolean | true | 드래그나 외부 클릭으로 닫기 가능 여부 |
| container | HTMLElement | document.body | drawer 포털 컨테이너 요소 |
| repositionInputs | boolean | false | 키보드 표시 시 입력 필드 재배치 여부 |
| onOpenChange | (open: boolean) => void | - | 열림 상태 변경 콜백 |
| onClose | () => void | - | 닫힘 콜백 |
| onAnimationEnd | (open: boolean) => void | - | 애니메이션 종료 콜백 |
Controller Props
컨트롤러 컴포넌트는 다음 props를 받습니다:
interface DrawerControllerProps {
isOpen: boolean; // 열림 상태
close: () => void; // 닫기 함수
unmount: () => void; // 언마운트 함수
}
// 비동기 drawer용
interface DrawerAsyncControllerProps<T> {
isOpen: boolean;
close: (result: T) => void; // 결과와 함께 닫기
unmount: () => void;
}🎨 예제
방향별 예제
// 하단 drawer (기본값)
drawer.open(MyComponent, { direction: "bottom" });
// 상단 drawer
drawer.open(MyComponent, { direction: "top" });
// 좌측 drawer
drawer.open(MyComponent, { direction: "left" });
// 우측 drawer
drawer.open(MyComponent, { direction: "right" });모달 vs 비모달
// 모달 drawer (배경 상호작용 차단)
drawer.open(MyComponent, { modal: true });
// 비모달 drawer (배경 상호작용 허용)
drawer.open(MyComponent, { modal: false });이벤트 콜백
drawer.open(MyComponent, {
onOpenChange: (open) => {
console.log("상태:", open ? "열림" : "닫힘");
},
onClose: () => {
console.log("닫혔습니다");
},
onAnimationEnd: (open) => {
console.log("애니메이션 완료:", open ? "열림" : "닫힘");
},
});비닫기 가능한 Drawer
drawer.open(MyComponent, {
dismissible: false, // 외부 클릭이나 드래그로 닫기 불가
direction: "bottom",
});입력 필드 재배치
drawer.open(MyComponent, {
repositionInputs: true, // 키보드 표시 시 입력 필드 재배치
direction: "bottom",
});🧪 테스트
라이브러리에는 포괄적인 테스트 유틸리티가 포함되어 있습니다:
import { render, screen, fireEvent } from "@testing-library/react";
import { drawer } from "drawer-kit";
test("drawer 열기 및 닫기", () => {
const TestComponent = () => (
<button
onClick={() =>
drawer.open(({ close }) => (
<div>
<h1>테스트 Drawer</h1>
<button onClick={close}>닫기</button>
</div>
))
}
>
Drawer 열기
</button>
);
render(<TestComponent />);
fireEvent.click(screen.getByText("Drawer 열기"));
expect(screen.getByText("테스트 Drawer")).toBeInTheDocument();
fireEvent.click(screen.getByText("닫기"));
expect(screen.queryByText("테스트 Drawer")).not.toBeInTheDocument();
});🎯 고급 사용법
커스텀 컨테이너
const customContainer = document.getElementById("drawer-container");
drawer.open(MyComponent, {
container: customContainer,
});다중 Drawer
// 여러 drawer 열기
const drawer1 = drawer.open(Component1, { direction: "bottom" });
const drawer2 = drawer.open(Component2, { direction: "right" });
// 특정 drawer 닫기
drawer.close(drawer1);
// 모든 drawer 닫기
drawer.closeAll();복잡한 비동기 플로우
const handleComplexFlow = async () => {
try {
// 첫 번째 drawer - 사용자 확인 받기
const confirmed = await drawer.openAsync(({ close }) => (
<div>
<h2>항목 삭제</h2>
<p>이 항목을 삭제하시겠습니까?</p>
<button onClick={() => close(true)}>삭제</button>
<button onClick={() => close(false)}>취소</button>
</div>
));
if (!confirmed) return;
// 두 번째 drawer - 진행 상황 표시
const progressDrawer = drawer.open(({ close }) => (
<div>
<h2>삭제 중...</h2>
<div>삭제 중입니다...</div>
<button onClick={close}>닫기</button>
</div>
));
// 비동기 작업 시뮬레이션
await deleteItem();
drawer.close(progressDrawer);
// 세 번째 drawer - 성공 표시
await drawer.openAsync(({ close }) => (
<div>
<h2>성공!</h2>
<p>삭제가 완료되었습니다.</p>
<button onClick={() => close()}>확인</button>
</div>
));
} catch (error) {
console.error("취소되었습니다:", error);
}
};🎨 스타일링
Drawer-kit은 합리적인 기본값을 제공하지만 완전한 커스터마이징을 허용합니다:
/* 커스텀 drawer 스타일 */
[data-vaul-drawer] {
background-color: white;
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
}
[data-vaul-overlay] {
background-color: rgba(0, 0, 0, 0.5);
}
/* 방향별 스타일 */
[data-vaul-drawer][data-vaul-drawer-direction="left"] {
border-radius: 0 12px 12px 0;
}
[data-vaul-drawer][data-vaul-drawer-direction="right"] {
border-radius: 12px 0 0 12px;
}🔧 개발
사전 요구사항
- Node.js 18+
- pnpm (권장) 또는 npm
설정
# 저장소 클론
git clone https://github.com/jinseok9338/drawer-kit.git
cd drawer-kit
# 의존성 설치
pnpm install
# 개발 서버 시작
pnpm dev
# 테스트 실행
pnpm test
# 라이브러리 빌드
pnpm build프로젝트 구조
src/
├── drawer-kit/ # 메인 라이브러리 코드
│ ├── components/ # React 컴포넌트
│ ├── context/ # React context와 hooks
│ ├── events/ # 이벤트 시스템
│ ├── types/ # TypeScript 정의
│ └── utils/ # 유틸리티 함수
├── test-ui/ # 개발 테스트 UI
└── tests/ # 테스트 파일🤝 기여하기
기여를 환영합니다! 자세한 내용은 기여 가이드를 참조하세요.
- 저장소를 포크하세요
- 기능 브랜치를 만드세요 (
git checkout -b feature/amazing-feature) - 변경사항을 커밋하세요 (
git commit -m '멋진 기능 추가') - 브랜치에 푸시하세요 (
git push origin feature/amazing-feature) - Pull Request를 열어주세요
📄 라이선스
이 프로젝트는 MIT 라이선스 하에 있습니다. 자세한 내용은 LICENSE 파일을 참조하세요.
🙏 감사의 말
- toss/overlay-kit - 우아한 API 디자인 영감을 주셔서 감사합니다
- vaul - 부드러운 drawer 애니메이션과 모바일 상호작용을 위해
- React - 훌륭한 프레임워크를 위해
- TypeScript - 타입 안전성을 위해
📞 지원
- 📧 이메일: [email protected]
- 🐛 이슈: GitHub Issues
- 💬 토론: GitHub Discussions
