react-horizontal-scrolling-menu
Version:
Horizontal scrolling menu component for React with per-item visibility tracking. Supports mouse and touch devices, SSR-safe.
383 lines (303 loc) • 24 kB
Markdown
<!-- i18n:start -->
[English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · 한국어 · [Русский](./README.ru.md) · [Español](./README.es.md) · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
<!-- i18n:meta locale=ko source=README.md source-blob=8958730422d74e17cb64c668f1e52d7eeee19c63 status=translated -->
<!-- i18n:end -->
# React horizontal scrolling menu
[](https://www.npmjs.com/package/react-horizontal-scrolling-menu)


[](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/actions/workflows/main.yml)
[](https://asmyshlyaev177.dev)
브라우저 네이티브 스크롤과 항목별 가시성 추적 위에 구축된 React용 가로 스크롤
메뉴 컴포넌트입니다. 카테고리 행, 탭 스트립, 칩 필터, 갤러리 등 앱이 파악해야
하는 모든 행에 적합합니다. 항목은 여러분의 컴포넌트와 CSS로 만들 수 있고,
메뉴는 부모 너비에 반응하며, 스크롤바, 터치, 마우스 휠, 드래그 또는 제공한
화살표 컴포넌트로 탐색합니다. min+gzip 5.7 kB.

### [랜딩 페이지](https://react-horizontal-scrolling-menu.dev) · [라이브 예제 (Storybook, 브라우저에서 편집 가능)](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu) · [API](#프로퍼티와-콜백) · [AI 에이전트 스킬](#ai-코딩-에이전트와-함께-사용)
### 사용처
2만 개 이상의 리포지토리가 이 라이브러리에 의존합니다. 직접 살펴볼 수 있는 다섯 곳 —
각 링크는 `package.json`이 아니라 이 라이브러리를 사용하는 컴포넌트의 `import` 줄로
연결되며, 커밋에 고정되어 있습니다:
- [Our World in Data](https://github.com/owid/owid-grapher/blob/4a60a2fb4532a2d287a1ef5660339dcc32bcd483/site/gdocs/components/KeyInsights.tsx#L3) — 아티클 렌더러의 핵심 인사이트 슬라이더. [토픽 패싯](https://github.com/owid/owid-grapher/blob/4a60a2fb4532a2d287a1ef5660339dcc32bcd483/site/latest/LatestTopicFacets.tsx#L10)에서는 react-aria `ToggleButton`을 감쌉니다. `^8.2.0`
- [Precious Plastic / ONE ARMY](https://github.com/ONEARMY/community-platform/blob/90c1be6be0ad450a92d9483577433fdc8b09f477/packages/components/src/VerticalList/VerticalList.client.tsx#L6-L7) — 공용 컴포넌트 패키지의 `VerticalList`. 이 라이브러리 문서를 그대로 참고해 만들어졌습니다. `^8.2.0`
- [erxes](https://github.com/erxes/erxes/blob/efef0252d390f4072e21c0a188d289f01866b188/apps/posclient-front/components/ui/horizontalScrollMenu.tsx#L6) — POS 클라이언트의 카테고리 메뉴. `^4.0.4`
- [Reapit](https://github.com/reapit/foundations/blob/9edda57691befd398547bcdf4013916b85face52/packages/app-builder/src/components/ui/viewport/tab-bar.tsx#L4) — 앱 빌더의 뷰포트 탭 바. `^3.2.5`
- [AWS Performance Dashboard](https://github.com/aws-solutions/performance-dashboard-on-aws/blob/cffa9c822ac8288a44d13a9394a2255e574c7592/frontend/src/components/Tabs.tsx#L8) — 대시보드의 `Tabs` 컴포넌트. [`Arrows`](https://github.com/aws-solutions/performance-dashboard-on-aws/blob/cffa9c822ac8288a44d13a9394a2255e574c7592/frontend/src/components/Arrows.tsx#L9)는 `VisibilityContext`를 직접 사용합니다. 2024년 아카이브됨, `^2.1.1` 고정.
또한 [React Status #257](https://react.statuscode.com/issues/257)에서 소개되었습니다.
## 빠른 시작
```bash
npm install react-horizontal-scrolling-menu
```
```tsx
import React from 'react';
import {
ScrollMenu,
VisibilityContext,
type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';
const items = Array.from({ length: 10 }, (_, i) => `item-${i + 1}`);
export function App() {
return (
<ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((id) => (
<Card itemId={id} key={id} title={id} />
))}
</ScrollMenu>
);
}
function LeftArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isFirstVisible = visibility.useIsVisible('first', true);
return (
<button disabled={isFirstVisible} onClick={() => visibility.scrollPrev()}>
←
</button>
);
}
function RightArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isLastVisible = visibility.useIsVisible('last', false);
return (
<button disabled={isLastVisible} onClick={() => visibility.scrollNext()}>
→
</button>
);
}
function Card({ itemId, title }: { itemId: string; title: string }) {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isVisible = visibility.useIsVisible(itemId);
return (
<div style={{ width: '160px' }} data-visible={isVisible}>
{title}
</div>
);
}
```
예제가 의존하는 세 가지:
- 모든 항목에는 고유한 `itemId` 프로퍼티가 필요합니다. 가시성 추적이 이를 기반으로
동작합니다. React의 `key`는 폴백으로 동작합니다.
- `styles.css`는 별도로 임포트합니다. JS 번들이 CSS를 주입하는 일은 없습니다.
- 항목 너비는 여러분의 CSS에서 나옵니다. 메뉴가 측정하는 것은 없습니다.
일반 JavaScript를 쓰시나요? 타입 임포트를 빼고 평소처럼
`React.useContext(VisibilityContext)`를 사용하세요.
## AI 코딩 에이전트와 함께 사용
구버전으로 학습한 모델은 모두 제거된 `visibleElements`, `Separator` 항목,
`Arrows` 프로퍼티를 여전히 사용하려 하고, 존재한 적 없는 `autoplay` 프로퍼티를
만들어 냅니다. 이 패키지에는 이를 막기 위한 8개의 `SKILL.md` 파일이 포함되어
있습니다. [TanStack Intent](https://tanstack.com/intent/latest/docs/overview)를
통해 온디맨드로 로드되는 작업 범위 가이드로, 웹 페이지가 아니라 라이브러리와
함께 버전이 관리됩니다.
```bash
npm install react-horizontal-scrolling-menu
npx @tanstack/intent@latest install # 프로젝트당 한 번
```
`install`은 스킬 발견 기능을 에이전트 설정(`CLAUDE.md`, `.cursorrules` 등)에
추가합니다. 이후 에이전트는 `node_modules/react-horizontal-scrolling-menu/skills/`
에서 필요할 때 스킬을 로드합니다. `npx @tanstack/intent@latest list`와
`npx @tanstack/intent@latest load react-horizontal-scrolling-menu#menu-setup`으로
직접 나열하거나 로드할 수 있습니다.
| 스킬 | 로드되는 시점 |
| ---------------------- | ------------------------------------------------------------- |
| `menu-setup` | 처음 동작하는 메뉴, 화살표, 필수 CSS 임포트 |
| `menu-visibility` | 화면에 무엇이 있는지, 그리고 양 끝의 화살표 상태 |
| `menu-scrolling` | `scrollToItem`, `apiRef`, 한 번에 한 페이지씩 페이징 |
| `menu-interactions` | 드래그, 휠, 터치 — 그리고 해당 핸들러 팩토리 |
| `menu-recipes` | 자동 재생, 무한 루프, 더 불러오기: 프로퍼티가 아니라 레시피 |
| `menu-transitions-rtl` | 애니메이션 타이밍, 커스텀 이징, 오른쪽에서 왼쪽 |
| `menu-testing-ssr` | Next.js와 RSC, Jest 목, Playwright |
| `menu-migration` | v8 이전 코드 업그레이드, 그리고 모델이 여전히 만들어 내는 API |
소스는 [`skills/`](skills/)에 있습니다. Intent 스킬을 로드할 수 없는 에이전트는
대신 [llms.txt](https://react-horizontal-scrolling-menu.dev/llms.txt)를 읽어야
합니다. 같은 사실을 한 파일로 압축한 것입니다.
## 하는 일과 하지 않는 일
브라우저 네이티브 스크롤 위에 구축되었습니다. 관성, 스크롤바, 터치, 휠,
접근성은 물리 재구현이 아니라 브라우저에서 나옵니다. 그 위에 IntersectionObserver를
통한 항목별 가시성, `scrollToItem` / `scrollNext` / `scrollPrev`, 외부 제어를
위한 `apiRef`, Header와 Footer 슬롯, RTL, 동적 추가/제거 감지, 그리고 전반에
걸친 TypeScript 타입이 있습니다. SSR 안전 — [랜딩 페이지](https://react-horizontal-scrolling-menu.dev)는
모든 데모를 서버 렌더링합니다.
캐러셀 엔진은 없습니다. 스냅이나 스프링 물리도 없습니다. 전체 화면 이미지
슬라이더가 필요하다면 Embla나 Swiper를 사용하세요. 자동 재생과 무한 루프도
프로퍼티가 아닙니다. 공개 API 위에 각각 약 60줄로 작성하는 레시피로,
Storybook에서 라이브 편집할 수 있습니다
([무한 루프](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-infiniteloop--infinite-loop),
[자동 재생](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-autoplay--autoplay)).
무엇이 보이는지 아는 행이 필요하다면, 바로 이것입니다.
## 예제
모든 예제는
[Storybook](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu)에서
라이브 편집할 수 있습니다. 각 스토리에는 라이브러리의 실제 타입 정의를 로드한
Monaco 에디터가 함께 제공됩니다. 다룹니다: 기본 사용법, 한 번에 한 항목 스크롤,
마우스 드래그, 마운트 시 항목으로 스크롤, 클릭 시 가운데 정렬, 동적 항목 추가,
위치 저장/복원, 항목 애니메이션, 진행 점, 본문 스크롤 방지, 커스텀 전환,
무한 루프, 자동 재생, 세로 레이아웃, 푸터의 화살표, 모바일 스와이프, RTL,
그리고 5000개 항목 스트레스 테스트.
<!-- DOCS_START -->
### 헬퍼와 API
ScrollMenu 메인 컴포넌트의 자식(화살표, 헤더, 푸터, 항목)은 **VisibilityContext**를
사용해 상태와 콜백에 접근할 수 있습니다. 함수 콜백도 컨텍스트를 받습니다(예:
`onWheel`, `onScroll`).
## 프로퍼티와 콜백
| 프로퍼티 | 시그니처 |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| LeftArrow | 왼쪽 화살표용 React 컴포넌트 |
| RightArrow | 오른쪽 화살표용 React 컴포넌트 |
| Header | Header React 컴포넌트 |
| Footer | Footer React 컴포넌트 |
| onWheel | (VisibilityContext, event) => void |
| onScroll | (VisibilityContext, event) => void, 스크롤이 _안정되기 전에_ 발화 |
| onInit | (VisibilityContext) => void |
| onUpdate | (VisibilityContext) => void |
| apiRef | React.RefObject \| React.RefCallback |
| options | IntersectionObserver 옵션 — 요소를 보이는 것으로 간주할 `rootMargin`, `threshold`, `ratio` |
| containerRef | 스크롤 컨테이너용 React.RefObject \| React.RefCallback |
| onMouseDown | (VisibilityContext) => (React.MouseEventHandler) => void |
| onMouseLeave | (VisibilityContext) => (React.MouseEventHandler) => void |
| onMouseUp | (VisibilityContext) => (React.MouseEventHandler) => void |
| onMouseMove | (VisibilityContext) => (React.MouseEventHandler) => void |
| onTouchMove | (VisibilityContext) => (React.TouchEventHandler) => void |
| onTouchStart | (VisibilityContext) => (React.TouchEventHandler) => void |
| onTouchEnd | (VisibilityContext) => (React.TouchEventHandler) => void |
| itemClassName | Item의 ClassName |
| scrollContainerClassName | scrollContainer의 ClassName |
| wrapperClassName | 가장 바깥쪽 div의 ClassName |
| transitionDuration | 전환 시간(밀리초), 기본값 `500`, `noPolyfill={false}` 필요 |
| transitionBehavior | 'smooth' \| 'auto' \| 커스텀 함수, `noPolyfill={false}` 필요 |
| RTL | 오른쪽에서 왼쪽 방향 활성화 |
| noPolyfill | 기본값 `true`(네이티브 scrollIntoView); `false`로 설정하면 전환 프로퍼티 활성화 |
두 가지 콜백 형태에 유의하세요. `onWheel`과 `onScroll`은 단순한
`(context, event) => void`인 반면, 마우스와 터치 프로퍼티는 핸들러 팩토리, 즉
`(context) => (event) => void`입니다. 팩토리 패턴의 실제 사용은
[MouseDrag 스토리](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-mousedrag--mouse-drag)를
참조하세요.
### VisibilityContext
훅(훅 규칙에 따라 ScrollMenu 아래에서 렌더링되는 컴포넌트 안에서만 호출하세요):
| 훅 | 시그니처 |
| -------------------- | ------------------------------------------------------------------------ |
| useIsVisible | (itemId: string \| 'first' \| 'last', defaultValue?: boolean) => boolean |
| useLeftArrowVisible | () => boolean |
| useRightArrowVisible | () => boolean |
값과 함수:
| 프로퍼티 | 시그니처 |
| --------------------- | ------------------------------------------------------ |
| getItemById | itemId => IOItem \| undefined |
| getItemElementById | itemId => DOM Element \| null |
| getItemByIndex | index => IOItem \| undefined |
| getItemElementByIndex | index => DOM Element \| null |
| getNextElement | () => IOItem \| undefined |
| getPrevElement | () => IOItem \| undefined |
| isFirstItemVisible | boolean |
| isItemVisible | itemId => boolean |
| isLastItem | boolean |
| isLastItemVisible | boolean |
| menuVisible | { current: boolean } |
| scrollNext | (behavior, inline, block, ScrollOptions) => void |
| scrollPrev | (behavior, inline, block, ScrollOptions) => void |
| scrollToItem | (item, behavior, inline, block, ScrollOptions) => void |
| items | ItemsMap 클래스 인스턴스 |
| scrollContainer | Ref<OuterContainer> |
### items 클래스 인스턴스
ItemsMap은 모든 항목에 대한 정보를 저장하며, 현재 보이는 항목과 이전/다음 항목을
가져오는 메서드를 제공합니다. 업데이트를 구독할 수도 있습니다.
| 프로퍼티/메서드 | 설명 |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| subscribe | `itemId` 또는 `first`, `last`, `onInit`, `onUpdate` 이벤트 구독. 예: `items.subscribe('item5', (item) => setVisible(item.visible))` |
| unsubscribe | useEffect에서 정리용으로 사용, 동일한 콜백 인스턴스를 전달 |
| getVisible | 보이는 항목만 반환 |
| toItems | 모든 항목의 id 반환 |
| toArr | 모든 항목 반환 |
| first | 첫 번째 항목 반환 |
| last | 마지막 항목 반환 |
| prev | (itemId \| Item) => 이전 항목 \| undefined |
| next | (itemId \| Item) => 다음 항목 \| undefined |
### 전환과 애니메이션
`transitionDuration`과 `transitionBehavior`(`'smooth'`, `'auto'`, 또는 커스텀
함수)는 `scrollToItem`과 스크롤 헬퍼의 애니메이션 방식을 제어합니다. 둘 다
`noPolyfill={false}`가 필요합니다. 기본 네이티브 스크롤은 이를 무시합니다.
`RTL` 프로퍼티와는 함께 사용할 수 없습니다.
커스텀 이징 함수는
[CustomTransition 스토리](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-customtransition--custom-transition)를
참조하세요.
#### ScrollOptions
`scrollToItem`, `scrollPrev`, `scrollNext`의 마지막 인자는 해당 호출 한 번에
대해 전환 프로퍼티를 덮어씁니다:
```tsx
scrollToItem(getItemElementById('item-5'), 'smooth', 'center', 'nearest', {
duration: 800, // 밀리초
});
```
### 기타 헬퍼
#### slidingWindow
이전 또는 다음 보이는 항목 그룹을 가져옵니다:
```tsx
slidingWindow(allItems, visibleItems).prev();
// 또는 .next()
```
#### getItemsPos
그룹의 첫 번째, 가운데, 마지막 항목을 가져옵니다. 예: 이전 페이지의 중앙으로
스크롤:
```tsx
const prevGroup = slidingWindow(allItems, visibleItems).prev();
const { center } = getItemsPos(prevGroup);
scrollToItem(getItemById(center), 'smooth', 'center');
```
### apiRef
ScrollMenu에 ref를 전달하면 전체 VisibilityContext 값이 할당됩니다. `scrollToItem`
같은 함수를 메뉴 밖에서 발화하는 데 유용합니다. ref의 데이터 값은 오래될 수
있으므로 함수를 호출하는 편을 권장합니다:
```tsx
apiRef.current.scrollToItem(apiRef.current.getItemElementById('item-3'));
```
항목의 DOM 요소에는 ``document.querySelector(`[data-key='${itemId}']`)``로 직접
접근할 수도 있습니다.
[ScrollToItem 스토리](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-scrolltoitem--scroll-to-item)와
[AddItemAndScrollToIt 스토리](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-additemandscrolltoit--add-item-and-scroll-to-it)를
참조하세요.
<!-- DOCS_END -->
## SSR
이 라이브러리는 SSR 안전합니다. 첫 렌더링은 일반 마크업을 내보내고
IntersectionObserver는 클라이언트 측에서만 연결됩니다. `useIsVisible`의
`defaultValue` 인자가 서버 렌더링 상태를 제어합니다. 표준 화살표 패턴
(`('first', true)` / `('last', false)`)은 왼쪽 화살표는 비활성, 오른쪽 화살표는
활성으로 렌더링해 시작 위치로 스크롤된 행과 일치합니다.
### Next.js 참고
이 패키지는 ESM 우선입니다. 구형 Next.js 설정에서는
[“Cannot use import statement outside a module”](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/240)을
만날 수 있습니다. 패키지를
[`transpilePackages`](https://nextjs.org/docs/app/api-reference/config/next-config-js/transpilePackages)에
추가하면 해결됩니다.
## 브라우저 지원
**IntersectionObserver**와 **requestAnimationFrame**이 필요합니다. 모든 최신
브라우저입니다. IE 없음.
## 개발
```bash
git clone https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu
cd react-horizontal-scrolling-menu
pnpm run setup
pnpm run demo # 예제 앱 (Next.js, 포트 3003), 라이브러리를 watch 모드로
pnpm run demo-tanstack # 예제 앱 (TanStack Start SSR, 포트 3004)
pnpm run storybook # 예제
pnpm test # 유닛 + e2e + storybook 테스트
```
리포지토리에는 두 개의 통합 예제 앱(`example-nextjs`와 `example-tanstack`, 후자는
workerd에서 서버 렌더링되는 TanStack Start)이 있습니다. 둘 다 동일한 데모(마우스
드래그, 본문 스크롤 잠금, 컨트롤 패널이 있는 커스텀 애니메이션)를 렌더링하므로,
`e2e/`의 단일 e2e 스위트가 두 프레임워크 모두에서 라이브러리에 대해 실행되며,
서버 렌더링된 HTML에 메뉴가 이미 존재한다는 단언도 포함합니다.
기여와 수정을 환영합니다. 포크하고, 커밋하고, PR을 열고, 테스트를 잊지 마세요.
[CONTRIBUTING](./CONTRIBUTING.md)과 [CHANGELOG](./CHANGELOG.md)을 참조하세요.
레거시 [v1 API](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/tree/v1) 문서.
## 소개
2018년부터 **Aleksandr Smyshliaev**가 구축하고 유지합니다. 제 첫 npm 패키지이며,
React 16.8부터 19까지 동일한 공개 API를 유지하고 있습니다. 저는 프런트엔드
엔지니어(React / Next.js / TypeScript)이며 **계약 및 정규직 일을 받고 있습니다**.
- **연락처** —— [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
[asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
[LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
- **다른 프로젝트** —— [state-in-url](https://github.com/asmyshlyaev177/state-in-url)
(타입이 있는 URL 상태),
[test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
(Playwright용 기록/재생)
리포지토리에 ⭐️를 주면 더 많은 사람이 이 라이브러리를 찾을 수 있습니다.