UNPKG

two-dimension-scroll

Version:

A smooth scroll library that detects both horizontal and vertical scroll and converts them to vertical smooth scrolling

1,378 lines (1,125 loc) โ€ข 35.3 kB
# TwoDimensionScroll ๐Ÿ“ฑ๐Ÿ–ฑ๏ธ ๊ฐ€๋กœ์™€ ์„ธ๋กœ ์Šคํฌ๋กค์„ ๋ชจ๋‘ ๊ฐ์ง€ํ•˜์—ฌ ๋ถ€๋“œ๋Ÿฌ์šด ์„ธ๋กœ ์Šคํฌ๋กค๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ˜์‹ ์ ์ธ JavaScript ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ - **๐ŸŽฏ 2์ฐจ์› ์Šคํฌ๋กค ๊ฐ์ง€**: ํŠธ๋ž™ํŒจ๋“œ๋‚˜ ๋งค์ง๋งˆ์šฐ์Šค์˜ ๊ฐ€๋กœ ์Šคํฌ๋กค์„ ์ž๋™์œผ๋กœ ์„ธ๋กœ ์Šคํฌ๋กค๋กœ ๋ณ€ํ™˜ - **๐ŸŒŠ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜**: requestAnimationFrame์„ ์‚ฌ์šฉํ•œ 60fps ๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค - **๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”**: ํ™˜๊ฒฝ๋ณ„ ์ตœ์ ํ™”๋œ ์Šคํฌ๋กค ๊ฒฝํ—˜ (PC/๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ) - **โš›๏ธ React ์™„๋ฒฝ ์ง€์›**: ๊ณต์‹ React Hook๊ณผ TypeScript ํƒ€์ž… ์ •์˜ ์ œ๊ณต - **๐ŸŽญ ๋ชจ๋‹ฌ ์นœํ™”์ **: ๋ชจ๋‹ฌ ์ƒํƒœ์—์„œ ์™„๋ฒฝํ•œ ์Šคํฌ๋กค ์ œ์–ด (๋ฐ”๋”” ์ฐจ๋‹จ + ๋‚ด๋ถ€ ํ—ˆ์šฉ) - **โ™ฟ ์ ‘๊ทผ์„ฑ ์ง€์›**: `prefers-reduced-motion`, ์Šคํฌ๋ฆฐ ๋ฆฌ๋”, ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์™„๋ฒฝ ์ง€์› - **๐ŸŽ›๏ธ ๋‹ค์–‘ํ•œ ์˜ต์…˜**: ๊ฐ๋„, ์ง€์†์‹œ๊ฐ„, ์ด์ง• ํ•จ์ˆ˜ ๋“ฑ ์„ธ๋ฐ€ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• - **โšก ๊ณ ์„ฑ๋Šฅ**: ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ด๊ณ  ๋ฐฐํ„ฐ๋ฆฌ ์นœํ™”์ ์ธ ๊ตฌ์กฐ - **๐Ÿ”ง TypeScript ์ง€์›**: ์™„์ „ํ•œ ํƒ€์ž… ์ •์˜ ํฌํ•จ ## ๐Ÿš€ ์‹œ์ž‘ํ•˜๊ธฐ ### ์„ค์น˜ ```bash npm install two-dimension-scroll ``` ### Vanilla JavaScript ์‚ฌ์šฉ๋ฒ• ```javascript // ES Modules (๊ถŒ์žฅ) import TwoDimensionScroll from 'two-dimension-scroll'; // CommonJS const TwoDimensionScroll = require('two-dimension-scroll').default; // ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์ดˆ๊ธฐํ™” const scroll = new TwoDimensionScroll(); // ์ปค์Šคํ…€ ์˜ต์…˜์œผ๋กœ ์ดˆ๊ธฐํ™” const scroll = new TwoDimensionScroll({ duration: 1200, horizontalSensitivity: 1.5, verticalSensitivity: 1.0, debug: true }); ``` ### React Hook ์‚ฌ์šฉ๋ฒ• React์—์„œ๋Š” **๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•**์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค: > โš ๏ธ **Vite/Webpack ํ™˜๊ฒฝ**: ๋™์  require ๋ฏธ์ง€์›์œผ๋กœ **๋ฐฉ๋ฒ• 2 (ScrollClass ์ง์ ‘ ์ „๋‹ฌ) ํ•„์ˆ˜** > ๐Ÿ’ก **Create React App**: ๋ฐฉ๋ฒ• 1, 2 ๋ชจ๋‘ ์ง€์› > ๐Ÿ”ง **Next.js/SSR**: ๋ฐฉ๋ฒ• 2 ๊ถŒ์žฅ #### ๋ฐฉ๋ฒ• 1: ๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ (์ž๋™ ๊ฐ์ง€) โš ๏ธ Vite ๋ฏธ์ง€์› ```tsx import { useTwoDimensionScroll } from 'two-dimension-scroll/react'; function App() { const { isReady, scrollPosition, scrollTo, scrollInfo } = useTwoDimensionScroll({ duration: 1200, desktop: { lerp: 0.1, sensitivity: 1.2 }, mobile: { lerp: 0.15, sensitivity: 0.8 } }); // ScrollClass ์ „๋‹ฌ ์—†์ด๋„ ์ž๋™์œผ๋กœ ํด๋ž˜์Šค๋ฅผ ์ฐพ์•„์„œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค! // โš ๏ธ ์ฃผ์˜: Vite, Webpack ํ™˜๊ฒฝ์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. if (!isReady) return <div>Loading...</div>; ``` #### ๋ฐฉ๋ฒ• 2: ๋ช…์‹œ์  ์ „๋‹ฌ (๊ถŒ์žฅ, โœ… Vite ํ˜ธํ™˜) ```tsx import TwoDimensionScroll from 'two-dimension-scroll'; import { useTwoDimensionScroll } from 'two-dimension-scroll/react'; function App() { const { isReady, scrollPosition, scrollTo, scrollInfo } = useTwoDimensionScroll( { duration: 1200, desktop: { lerp: 0.1, sensitivity: 1.2 }, mobile: { lerp: 0.15, sensitivity: 0.8 } }, { ScrollClass: TwoDimensionScroll } // ํด๋ž˜์Šค ์ง์ ‘ ์ „๋‹ฌ๋กœ ์ตœ๋Œ€ ์•ˆ์ •์„ฑ ๋ณด์žฅ ); if (!isReady) return <div>Loading...</div>; return ( <div> <p>ํ˜„์žฌ ์œ„์น˜: {scrollPosition}px</p> <button onClick={() => scrollTo(0)}>๋งจ ์œ„๋กœ</button> </div> ); } ``` ### CDN ์‚ฌ์šฉ ```html <script src="path/to/dist/bundle-simple.js"></script> <script> const scroll = new TwoDimensionScroll({ duration: 1000, debug: true }); </script> ``` ## โš›๏ธ React์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ TwoDimensionScroll์€ React ํ™˜๊ฒฝ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•˜์—ฌ ์ตœ๊ณ ์˜ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ### React ์„ค์น˜ ๋ฐ ์„ค์ • ```bash # npm์œผ๋กœ ์„ค์น˜ npm install two-dimension-scroll # ๋˜๋Š” yarn์œผ๋กœ ์„ค์น˜ yarn add two-dimension-scroll ``` ```tsx // React Hook import import { useTwoDimensionScroll, useModalScroll, useScrollToTop, useScrollProgress } from 'two-dimension-scroll/react'; ``` ### ๊ธฐ๋ณธ React Hook ์‚ฌ์šฉ๋ฒ• ```tsx import React from 'react'; import { useTwoDimensionScroll } from 'two-dimension-scroll/react'; function App() { const { isReady, scrollPosition, scrollTo, scrollInfo } = useTwoDimensionScroll({ debug: process.env.NODE_ENV === 'development', desktop: { duration: 1000, lerp: 0.1 }, mobile: { duration: 800, lerp: 0.15 } }); if (!isReady) { return <div>Loading scroll system...</div>; } return ( <div> <div style={{ height: '200vh' }}> <h1>Smooth Scroll with React</h1> {/* ์Šคํฌ๋กค ์ •๋ณด ํ‘œ์‹œ */} <div style={{ position: 'fixed', top: 10, right: 10 }}> <p>์œ„์น˜: {Math.round(scrollPosition)}px</p> <p>์ง„ํ–‰๋ฅ : {Math.round((scrollInfo?.progress || 0) * 100)}%</p> </div> {/* ์Šคํฌ๋กค ์ œ์–ด ๋ฒ„ํŠผ */} <button onClick={() => scrollTo(0, 1000)}> ๋งจ ์œ„๋กœ </button> <button onClick={() => scrollTo(scrollInfo?.maxPosition || 0)}> ๋งจ ์•„๋ž˜๋กœ </button> </div> </div> ); } ``` ### TypeScript ์ง€์› ์™„๋ฒฝํ•œ TypeScript ์ง€์›์„ ์œ„ํ•ด ํƒ€์ž… ์ •์˜๋ฅผ ํฌํ•จํ•˜์„ธ์š”: ```tsx import { useTwoDimensionScroll, TwoDimensionScrollOptions, ScrollInfo } from 'two-dimension-scroll/react'; interface AppProps {} const App: React.FC<AppProps> = () => { const options: TwoDimensionScrollOptions = { duration: 1200, desktop: { lerp: 0.08, sensitivity: 1.2 }, accessibility: { reducedMotion: true, keyboardNavigation: true } }; const { isReady, scrollPosition, scrollInfo, scrollTo, disable, enable } = useTwoDimensionScroll(options); return ( // JSX ๋‚ด์šฉ ); }; ``` ### ๋ชจ๋‹ฌ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ๋ชจ๋‹ฌ์ด ์—ด๋ฆฐ ์ƒํƒœ์—์„œ ๋ฐ”๋”” ์Šคํฌ๋กค์„ ์ฐจ๋‹จํ•˜๊ณ  ๋ชจ๋‹ฌ ๋‚ด๋ถ€๋งŒ ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•˜๊ฒŒ: ```tsx import { useModalScroll } from 'two-dimension-scroll/react'; function ModalExample() { const { isModalOpen, openModal, closeModal } = useModalScroll(); return ( <div> <button onClick={openModal}>๋ชจ๋‹ฌ ์—ด๊ธฐ</button> {isModalOpen && ( <div className="modal-overlay" role="dialog" aria-modal="true" onClick={closeModal} > <div className="modal-content" onClick={e => e.stopPropagation()} style={{ overflow: 'auto', maxHeight: '80vh', padding: '20px' }} > <h2>์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ๋ชจ๋‹ฌ</h2> {/* ๊ธด ์ฝ˜ํ…์ธ  */} {Array(50).fill(0).map((_, i) => ( <p key={i}>๋ชจ๋‹ฌ ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ  {i + 1}</p> ))} <button onClick={closeModal}>๋‹ซ๊ธฐ</button> </div> </div> )} </div> ); } ``` ### Next.js์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ SSR ํ™˜๊ฒฝ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•: ```tsx // pages/_app.tsx import { useEffect } from 'react'; import Script from 'next/script'; export default function MyApp({ Component, pageProps }) { return ( <> {/* TwoDimensionScroll ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋“œ */} <Script src="/js/bundle-simple.js" strategy="beforeInteractive" /> <Component {...pageProps} /> </> ); } // components/ScrollProvider.tsx import { useEffect, useState } from 'react'; import { useTwoDimensionScroll } from 'two-dimension-scroll/react'; export function ScrollProvider({ children }) { const [isClient, setIsClient] = useState(false); const { isReady } = useTwoDimensionScroll({ debug: process.env.NODE_ENV === 'development' }); useEffect(() => { setIsClient(true); }, []); if (!isClient || !isReady) { return <div>Loading...</div>; } return <>{children}</>; } // pages/index.tsx import { ScrollProvider } from '../components/ScrollProvider'; export default function Home() { return ( <ScrollProvider> <div style={{ height: '300vh' }}> {/* ํŽ˜์ด์ง€ ์ฝ˜ํ…์ธ  */} </div> </ScrollProvider> ); } ``` ### ์Šคํฌ๋กค ์ง„ํ–‰๋ฅ  ์ถ”์  ์‹ค์‹œ๊ฐ„ ์Šคํฌ๋กค ์ง„ํ–‰๋ฅ ์„ ์ถ”์ ํ•˜๋Š” ๋ฐฉ๋ฒ•: ```tsx import { useScrollProgress } from 'two-dimension-scroll/react'; function ScrollProgress() { const [progress, setProgress] = useState(0); useScrollProgress((data) => { setProgress(data.percentage); }, 50); // 50ms ์Šค๋กœํ‹€๋ง return ( <div style={{ position: 'fixed', top: 0, left: 0, width: `${progress}%`, height: '4px', background: 'linear-gradient(90deg, #ff6b6b, #4ecdc4)', transition: 'width 0.1s ease', zIndex: 9999 }} /> ); } ``` ### React Hook API ์™„์ „ ๊ฐ€์ด๋“œ #### `useTwoDimensionScroll(options, config)` ```tsx // ์ƒˆ๋กœ์šด API (๊ถŒ์žฅ) const { // ์ƒํƒœ ์ •๋ณด instance, // TwoDimensionScroll ์ธ์Šคํ„ด์Šค isReady, // ์ดˆ๊ธฐํ™” ์™„๋ฃŒ ์—ฌ๋ถ€ scrollPosition, // ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜ (์‹ค์‹œ๊ฐ„) scrollInfo, // ์Šคํฌ๋กค ์ƒ์„ธ ์ •๋ณด { position, maxPosition, progress, isScrolling } // ์ œ์–ด ํ•จ์ˆ˜ scrollTo, // (position, duration?) => void pauseForModal, // () => void - ๋ชจ๋‹ฌ์šฉ ์Šคํฌ๋กค ์ •์ง€ resumeFromModal, // () => void - ๋ชจ๋‹ฌ ์Šคํฌ๋กค ์žฌ๊ฐœ disable, // () => void - ์Šคํฌ๋กค ์™„์ „ ๋น„ํ™œ์„ฑํ™” enable, // () => void - ์Šคํฌ๋กค ํ™œ์„ฑํ™” updateOptions, // (options) => void - ์‹ค์‹œ๊ฐ„ ์˜ต์…˜ ๋ณ€๊ฒฝ // React ์ „์šฉ getReactInfo // () => ReactCompatibilityInfo } = useTwoDimensionScroll(options, { ScrollClass, deps }); // config ๋งค๊ฐœ๋ณ€์ˆ˜: // { // ScrollClass?: TwoDimensionScrollClass, // ํด๋ž˜์Šค ์ง์ ‘ ์ „๋‹ฌ (๊ถŒ์žฅ) // deps?: DependencyList // React ์˜์กด์„ฑ ๋ฐฐ์—ด // } // ๊ธฐ์กด API (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) const { ... } = useTwoDimensionScroll(options, deps); ``` #### `useModalScroll()` ```tsx const { isModalOpen, // boolean - ๋ชจ๋‹ฌ ์ƒํƒœ openModal, // () => void - ๋ชจ๋‹ฌ ์—ด๊ธฐ + ๋ฐ”๋”” ์Šคํฌ๋กค ์ฐจ๋‹จ closeModal, // () => void - ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + ๋ฐ”๋”” ์Šคํฌ๋กค ๋ณต์› toggleModal // () => void - ๋ชจ๋‹ฌ ํ† ๊ธ€ } = useModalScroll(); ``` #### `useScrollToTop()` ```tsx const scrollToTop = useScrollToTop(); // ์‚ฌ์šฉ๋ฒ• <button onClick={() => scrollToTop(1500)}> ๋งจ ์œ„๋กœ (1.5์ดˆ) </button> ``` #### `useScrollProgress(callback, throttle)` ```tsx useScrollProgress((data) => { console.log('์Šคํฌ๋กค ์ง„ํ–‰๋ฅ :', data.percentage + '%'); console.log('ํ˜„์žฌ ์œ„์น˜:', data.position); console.log('์ง„ํ–‰๋ฅ  (0-1):', data.progress); }, 100); // 100ms ๊ฐ„๊ฒฉ์œผ๋กœ ํ˜ธ์ถœ ``` ## ๐Ÿ“– API ๋ฌธ์„œ ### ์ƒ์„ฑ์ž ์˜ต์…˜ ```typescript interface TwoDimensionScrollOptions { // ๊ธฐ๋ณธ ์˜ต์…˜ duration?: number; // ์Šค๋ฌด์Šค ์Šคํฌ๋กค ์ง€์† ์‹œ๊ฐ„ (๊ธฐ๋ณธ๊ฐ’: 1000ms) easing?: (t: number) => number; // ์ด์ง• ํ•จ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: easeOutCubic) horizontalSensitivity?: number; // ๊ฐ€๋กœ ์Šคํฌ๋กค ๊ฐ๋„ (๊ธฐ๋ณธ๊ฐ’: 1) verticalSensitivity?: number; // ์„ธ๋กœ ์Šคํฌ๋กค ๊ฐ๋„ (๊ธฐ๋ณธ๊ฐ’: 1) disabled?: boolean; // ์Šคํฌ๋กค ๋น„ํ™œ์„ฑํ™” ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: false) debug?: boolean; // ๋””๋ฒ„๊ทธ ๋ชจ๋“œ (๊ธฐ๋ณธ๊ฐ’: false) // ํ™˜๊ฒฝ๋ณ„ ์ตœ์ ํ™” ์˜ต์…˜ desktop?: { duration?: number; // PC์šฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์†์‹œ๊ฐ„ lerp?: number; // ์„ ํ˜• ๋ณด๊ฐ„ ๊ฐ’ (0-1, ๋‚ฎ์„์ˆ˜๋ก ๋” ๋ถ€๋“œ๋Ÿฌ์›€) sensitivity?: number; // ์ „์ฒด์ ์ธ ๊ฐ๋„ wheelMultiplier?: number; // ํœ  ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ touchMultiplier?: number; // ํ„ฐ์น˜ ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ }; mobile?: { duration?: number; // ๋ชจ๋ฐ”์ผ์šฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์†์‹œ๊ฐ„ lerp?: number; // ์„ ํ˜• ๋ณด๊ฐ„ ๊ฐ’ sensitivity?: number; // ์ „์ฒด์ ์ธ ๊ฐ๋„ wheelMultiplier?: number; // ํœ  ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ (๋ชจ๋ฐ”์ผ์—์„œ๋Š” ํ„ฐ์น˜ ํœ ) touchMultiplier?: number; // ํ„ฐ์น˜ ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ touchStopThreshold?: number; // ํ„ฐ์น˜ ์ •์ง€ ์ž„๊ณ„๊ฐ’ flingMultiplier?: number; // ํ”Œ๋ง ์ œ์Šค์ฒ˜ ๋ฐฐ์ˆ˜ }; tablet?: { duration?: number; // ํƒœ๋ธ”๋ฆฟ์šฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์†์‹œ๊ฐ„ lerp?: number; // ์„ ํ˜• ๋ณด๊ฐ„ ๊ฐ’ sensitivity?: number; // ์ „์ฒด์ ์ธ ๊ฐ๋„ wheelMultiplier?: number; // ํœ  ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ touchMultiplier?: number; // ํ„ฐ์น˜ ์ด๋ฒคํŠธ ๋ฐฐ์ˆ˜ }; // ์ ‘๊ทผ์„ฑ ์˜ต์…˜ accessibility?: { reducedMotion?: boolean; // prefers-reduced-motion ์ž๋™ ๊ฐ์ง€ (๊ธฐ๋ณธ๊ฐ’: true) screenReader?: boolean; // ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์› (๊ธฐ๋ณธ๊ฐ’: true) keyboardNavigation?: boolean; // ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› (๊ธฐ๋ณธ๊ฐ’: true) ariaLiveRegion?: boolean; // ARIA Live Region ์ƒ์„ฑ (๊ธฐ๋ณธ๊ฐ’: true) focusManagement?: boolean; // ํฌ์ปค์Šค ๊ด€๋ฆฌ ์ž๋™ํ™” (๊ธฐ๋ณธ๊ฐ’: true) }; // UI/UX ์˜ต์…˜ ui?: { hideScrollbar?: boolean; // ์Šคํฌ๋กค๋ฐ” ์ˆจ๊น€ (๊ธฐ๋ณธ๊ฐ’: true) showScrollProgress?: boolean; // ์Šคํฌ๋กค ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ (๊ธฐ๋ณธ๊ฐ’: false) customScrollbarStyle?: string; // ์ปค์Šคํ…€ ์Šคํฌ๋กค๋ฐ” CSS }; } ``` ### ๋ฉ”์„œ๋“œ #### ๊ธฐ๋ณธ ์ œ์–ด ๋ฉ”์„œ๋“œ ##### `scrollTo(position: number, options?: object): void` ํŠน์ • ์œ„์น˜๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์Šคํฌ๋กคํ•ฉ๋‹ˆ๋‹ค. ```javascript // ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• scroll.scrollTo(1000); // 1000px ์œ„์น˜๋กœ ์Šคํฌ๋กค // ์˜ต์…˜๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ scroll.scrollTo(1000, { duration: 2000, // 2์ดˆ ๋™์•ˆ ์Šคํฌ๋กค immediate: false // ์ฆ‰์‹œ ์Šคํฌ๋กค ์—ฌ๋ถ€ }); // React์—์„œ ์‚ฌ์šฉ const { scrollTo } = useTwoDimensionScroll(); scrollTo(0, { duration: 1500 }); // 1.5์ดˆ ๋™์•ˆ ๋งจ ์œ„๋กœ ``` ##### `on(callback: ScrollCallback): void` ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS scroll.on((data) => { console.log('์Šคํฌ๋กค ์œ„์น˜:', data.scroll || data.scrollTop); console.log('์Šคํฌ๋กค ๋ฐฉํ–ฅ:', data.direction); console.log('์ด๋ฒคํŠธ ํƒ€์ž…:', data.type); console.log('์ง„ํ–‰๋ฅ :', data.progress); }); // React Hook์€ ์ž๋™์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ง์ ‘ on/off ํ˜ธ์ถœ ๋ถˆํ•„์š” const { scrollPosition, scrollInfo } = useTwoDimensionScroll(); ``` ##### `updateOptions(options: Partial<TwoDimensionScrollOptions>): void` ์‹ค์‹œ๊ฐ„์œผ๋กœ ์˜ต์…˜์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS scroll.updateOptions({ duration: 1500, desktop: { lerp: 0.05, sensitivity: 1.5 }, accessibility: { reducedMotion: true } }); // React Hook const { updateOptions } = useTwoDimensionScroll(); updateOptions({ mobile: { duration: 600 } }); ``` #### ๋ชจ๋‹ฌ ๋ฐ ์ƒํƒœ ์ œ์–ด ๋ฉ”์„œ๋“œ ##### `pauseForModal(): void` / `resumeFromModal(): void` ๋ชจ๋‹ฌ ์ƒํƒœ์—์„œ ์Šคํฌ๋กค์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS function openModal() { scroll.pauseForModal(); // ๋ฐ”๋”” ์Šคํฌ๋กค ์ฐจ๋‹จ, ์Šคํฌ๋กค ์œ„์น˜ ์ €์žฅ showModal(); } function closeModal() { hideModal(); scroll.resumeFromModal(); // ๋ฐ”๋”” ์Šคํฌ๋กค ๋ณต์›, ์œ„์น˜ ๋ณต์› } // React Hook (๊ถŒ์žฅ) const { openModal, closeModal } = useModalScroll(); // ์ž๋™์œผ๋กœ pauseForModal/resumeFromModal ์ฒ˜๋ฆฌ ``` ##### `disable(): void` / `enable(): void` ์Šคํฌ๋กค์„ ์™„์ „ํžˆ ๋น„ํ™œ์„ฑํ™”/ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS scroll.disable(); // ๋ชจ๋“  ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋น„ํ™œ์„ฑํ™” scroll.enable(); // ์Šคํฌ๋กค ์žฌํ™œ์„ฑํ™” // React Hook const { disable, enable } = useTwoDimensionScroll(); ``` #### ์ •๋ณด ์กฐํšŒ ๋ฉ”์„œ๋“œ ##### `getCurrentPosition(): number` / `getMaxPosition(): number` ํ˜„์žฌ ๋ฐ ์ตœ๋Œ€ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS const current = scroll.getCurrentPosition(); const max = scroll.getMaxPosition(); const progress = current / max; // React Hook (์‹ค์‹œ๊ฐ„ ์ž๋™ ์—…๋ฐ์ดํŠธ) const { scrollPosition, scrollInfo } = useTwoDimensionScroll(); console.log('ํ˜„์žฌ ์œ„์น˜:', scrollPosition); console.log('์ตœ๋Œ€ ์œ„์น˜:', scrollInfo.maxPosition); console.log('์ง„ํ–‰๋ฅ :', scrollInfo.progress); ``` ##### `getReactCompatibilityInfo(): ReactCompatibilityInfo` (React ์ „์šฉ) React ํ™˜๊ฒฝ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ```javascript const { getReactInfo } = useTwoDimensionScroll(); const info = getReactInfo(); console.log('React ํ™˜๊ฒฝ:', info.isReactEnvironment); console.log('Router ๊ฐ์ง€:', info.hasReactRouter); console.log('์ด๋ฒคํŠธ ์ˆ˜:', info.eventListenerCount); ``` #### ํ™˜๊ฒฝ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฉ”์„œ๋“œ ##### `updateEnvironmentOptions(environment: string, options: object): void` ํŠน์ • ํ™˜๊ฒฝ์˜ ์˜ต์…˜๋งŒ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ```javascript // ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ๋งŒ ๋ณ„๋„ ์ตœ์ ํ™” scroll.updateEnvironmentOptions('mobile', { duration: 500, lerp: 0.2, touchStopThreshold: 5 }); // ๋ฐ์Šคํฌํ†ฑ ์„ฑ๋Šฅ ์ตœ์ ํ™” scroll.updateEnvironmentOptions('desktop', { lerp: 0.08, wheelMultiplier: 1.5 }); ``` ##### `applyPerformancePreset(preset: string): void` ๋ฏธ๋ฆฌ ์ •์˜๋œ ์„ฑ๋Šฅ ํ”„๋ฆฌ์…‹์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ```javascript scroll.applyPerformancePreset('smooth'); // ๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค ์šฐ์„  scroll.applyPerformancePreset('performance'); // ์„ฑ๋Šฅ ์šฐ์„  scroll.applyPerformancePreset('accessibility'); // ์ ‘๊ทผ์„ฑ ์šฐ์„  ``` #### ์ •๋ฆฌ ๋ฐ ํ•ด์ œ ๋ฉ”์„œ๋“œ ##### `destroy(): void` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์™„์ „ํžˆ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ```javascript // Vanilla JS scroll.destroy(); // React Hook์€ useEffect cleanup์—์„œ ์ž๋™ ํ˜ธ์ถœ // ์ˆ˜๋™ ํ˜ธ์ถœ ์‹œ: const { instance } = useTwoDimensionScroll(); useEffect(() => { return () => { if (instance) { instance.destroy(); } }; }, []); ``` ##### `cleanup(): () => void` (React ์ „์šฉ) React useEffect cleanup ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ```javascript const { instance } = useTwoDimensionScroll(); useEffect(() => { // ๋‹ค๋ฅธ ์ดˆ๊ธฐํ™” ๋กœ์ง... return instance?.cleanup?.(); // ์•ˆ์ „ํ•œ cleanup }, [instance]); ``` ## ๐ŸŽจ ์ด์ง• ํ•จ์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋‚ด์žฅ๋œ ์ด์ง• ํ•จ์ˆ˜๋“ค: ```javascript import { Easing } from 'two-dimension-scroll'; const scroll = new TwoDimensionScroll({ easing: Easing.easeInOutCubic // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์˜ต์…˜๋“ค: // Easing.linear // Easing.easeInQuad // Easing.easeOutQuad // Easing.easeInOutQuad // Easing.easeInCubic // Easing.easeOutCubic // Easing.easeInOutCubic }); // ์ปค์Šคํ…€ ์ด์ง• ํ•จ์ˆ˜ const scroll = new TwoDimensionScroll({ easing: (t) => t * t * t // ์ปค์Šคํ…€ cubic easing }); ``` ## ๐Ÿ’ก ์‚ฌ์šฉ ์‚ฌ๋ก€ ๋ฐ ์‹ค์ „ ์˜ˆ์ œ ### 1. ๊ธฐ๋ณธ ๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค ```javascript // Vanilla JS const scroll = new TwoDimensionScroll({ duration: 800, easing: Easing.easeOutQuad, desktop: { lerp: 0.1 }, mobile: { lerp: 0.15 } }); ``` ```tsx // React Hook function SmoothScrollApp() { const { isReady } = useTwoDimensionScroll({ duration: 800, desktop: { lerp: 0.1 }, mobile: { lerp: 0.15 } }); return ( <div style={{ height: '300vh' }}> {isReady ? '๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค ์ค€๋น„ ์™„๋ฃŒ!' : 'Loading...'} </div> ); } ``` ### 2. ํ™˜๊ฒฝ๋ณ„ ๊ฐ๋„ ์ตœ์ ํ™” ```javascript // Vanilla JS - ์„ธ๋ฐ€ํ•œ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • const scroll = new TwoDimensionScroll({ desktop: { sensitivity: 1.2, // PC์—์„œ ๋” ๋ฏผ๊ฐํ•˜๊ฒŒ wheelMultiplier: 1.5, // ํœ  ์Šคํฌ๋กค ๋ฐฐ์ˆ˜ duration: 1000 }, mobile: { sensitivity: 0.8, // ๋ชจ๋ฐ”์ผ์—์„œ ๋œ ๋ฏผ๊ฐํ•˜๊ฒŒ touchMultiplier: 1.0, // ํ„ฐ์น˜ ์Šคํฌ๋กค ๋ฐฐ์ˆ˜ duration: 600, touchStopThreshold: 5 // ํ„ฐ์น˜ ์ •์ง€ ๊ฐ๋„ }, tablet: { sensitivity: 1.0, // ํƒœ๋ธ”๋ฆฟ ์ค‘๊ฐ„๊ฐ’ duration: 800 } }); ``` ```tsx // React Hook - ๋ฐ˜์‘ํ˜• ์„ค์ • function ResponsiveScrollApp() { const { scrollPosition, updateOptions } = useTwoDimensionScroll({ desktop: { sensitivity: 1.2, duration: 1000 }, mobile: { sensitivity: 0.8, duration: 600 } }); // ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ ์„ค์ • ๋™์  ๋ณ€๊ฒฝ useEffect(() => { const handleResize = () => { if (window.innerWidth < 768) { updateOptions({ mobile: { sensitivity: 0.6 } // ์ž‘์€ ํ™”๋ฉด์—์„œ ๋” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ }); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [updateOptions]); return <div>ํ˜„์žฌ ์Šคํฌ๋กค: {scrollPosition}px</div>; } ``` ### 3. ๊ณ ๊ธ‰ ๋ชจ๋‹ฌ ์Šคํฌ๋กค ์ œ์–ด ```javascript // Vanilla JS - ์ •๊ตํ•œ ๋ชจ๋‹ฌ ์ œ์–ด class ModalManager { constructor() { this.scroll = new TwoDimensionScroll({ debug: true }); this.modals = []; } openModal(modalId) { this.scroll.pauseForModal(); this.modals.push(modalId); console.log('๋ชจ๋‹ฌ ์—ด๋ฆผ, ๋ฐ”๋”” ์Šคํฌ๋กค ์ฐจ๋‹จ'); } closeModal(modalId) { this.modals = this.modals.filter(id => id !== modalId); if (this.modals.length === 0) { this.scroll.resumeFromModal(); console.log('๋ชจ๋“  ๋ชจ๋‹ฌ ๋‹ซํž˜, ๋ฐ”๋”” ์Šคํฌ๋กค ๋ณต์›'); } } } ``` ```tsx // React Hook - ์ค‘์ฒฉ ๋ชจ๋‹ฌ ์ง€์› function AdvancedModalApp() { const [modals, setModals] = useState([]); const { pauseForModal, resumeFromModal } = useTwoDimensionScroll(); const openModal = useCallback((modalId) => { setModals(prev => { if (prev.length === 0) { pauseForModal(); // ์ฒซ ๋ฒˆ์งธ ๋ชจ๋‹ฌ์ผ ๋•Œ๋งŒ ์Šคํฌ๋กค ์ฐจ๋‹จ } return [...prev, modalId]; }); }, [pauseForModal]); const closeModal = useCallback((modalId) => { setModals(prev => { const newModals = prev.filter(id => id !== modalId); if (newModals.length === 0) { resumeFromModal(); // ๋งˆ์ง€๋ง‰ ๋ชจ๋‹ฌ์ผ ๋•Œ๋งŒ ์Šคํฌ๋กค ๋ณต์› } return newModals; }); }, [resumeFromModal]); return ( <div> <button onClick={() => openModal('modal1')}>๋ชจ๋‹ฌ 1 ์—ด๊ธฐ</button> <button onClick={() => openModal('modal2')}>๋ชจ๋‹ฌ 2 ์—ด๊ธฐ</button> {modals.includes('modal1') && ( <Modal onClose={() => closeModal('modal1')}> <button onClick={() => openModal('modal2')}>์ค‘์ฒฉ ๋ชจ๋‹ฌ ์—ด๊ธฐ</button> </Modal> )} {modals.includes('modal2') && ( <Modal onClose={() => closeModal('modal2')}> ์ค‘์ฒฉ๋œ ๋ชจ๋‹ฌ ๋‚ด์šฉ </Modal> )} </div> ); } ``` ### 4. ์‹ค์‹œ๊ฐ„ ์Šคํฌ๋กค ์ง„ํ–‰๋ฅ  ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ```javascript // Vanilla JS - ์„น์…˜ ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜ const scroll = new TwoDimensionScroll({ debug: false }); const sections = document.querySelectorAll('.section'); let currentSection = 0; scroll.on((data) => { const progress = data.scroll / scroll.limit; const percentage = Math.round(progress * 100); // ์ง„ํ–‰๋ฅ  ๋ฐ” ์—…๋ฐ์ดํŠธ document.getElementById('progress-bar').style.width = `${percentage}%`; // ํ˜„์žฌ ์„น์…˜ ๊ฐ์ง€ sections.forEach((section, index) => { const rect = section.getBoundingClientRect(); if (rect.top <= window.innerHeight / 2 && rect.bottom >= window.innerHeight / 2) { if (currentSection !== index) { currentSection = index; updateNavigation(index); } } }); }); function updateNavigation(activeIndex) { document.querySelectorAll('.nav-item').forEach((item, index) => { item.classList.toggle('active', index === activeIndex); }); } ``` ```tsx // React Hook - ๊ณ ๊ธ‰ ์ง„ํ–‰๋ฅ  ์ปดํฌ๋„ŒํŠธ function ProgressiveScrollApp() { const { scrollPosition, scrollInfo } = useTwoDimensionScroll(); const [currentSection, setCurrentSection] = useState(0); const [progress, setProgress] = useState(0); const sectionsRef = useRef([]); useScrollProgress((data) => { setProgress(data.percentage); }, 16); // 60fps๋กœ ์—…๋ฐ์ดํŠธ useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const index = sectionsRef.current.findIndex( ref => ref.current === entry.target ); if (index !== -1) { setCurrentSection(index); } } }); }, { threshold: 0.5 } ); sectionsRef.current.forEach(ref => { if (ref.current) observer.observe(ref.current); }); return () => observer.disconnect(); }, []); return ( <div> {/* ์ง„ํ–‰๋ฅ  ๋ฐ” */} <div style={{ position: 'fixed', top: 0, left: 0, width: `${progress}%`, height: '4px', backgroundColor: '#007bff', zIndex: 1000, transition: 'width 0.1s ease' }} /> {/* ์„น์…˜ ๋„ค๋น„๊ฒŒ์ด์…˜ */} <nav style={{ position: 'fixed', right: 20, top: '50%' }}> {[1, 2, 3, 4].map((num, index) => ( <div key={num} style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: currentSection === index ? '#007bff' : '#ccc', margin: '8px 0', cursor: 'pointer' }} onClick={() => { const section = sectionsRef.current[index]?.current; if (section) { const top = section.offsetTop; scrollTo(top, { duration: 1000 }); } }} /> ))} </nav> {/* ์„น์…˜๋“ค */} {[1, 2, 3, 4].map((num, index) => ( <section key={num} ref={el => sectionsRef.current[index] = { current: el }} style={{ height: '100vh', padding: '50px' }} className="section" > <h2>์„น์…˜ {num}</h2> <p>ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜: {Math.round(scrollPosition)}px</p> <p>์Šคํฌ๋กค ์ง„ํ–‰๋ฅ : {progress}%</p> </section> ))} </div> ); } ``` ### 5. ์ ‘๊ทผ์„ฑ ์ตœ์ ํ™” ์Šคํฌ๋กค ```javascript // ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค์ • const accessibleScroll = new TwoDimensionScroll({ accessibility: { reducedMotion: true, // prefers-reduced-motion ์ž๋™ ๊ฐ์ง€ screenReader: true, // ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์› keyboardNavigation: true, // ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ariaLiveRegion: true, // ARIA ๋ผ์ด๋ธŒ ๋ฆฌ์ „ focusManagement: true // ํฌ์ปค์Šค ๊ด€๋ฆฌ }, desktop: { duration: 800 // ์ ๋‹นํ•œ ์†๋„ }, mobile: { duration: 600 // ๋ชจ๋ฐ”์ผ์—์„œ ๋น ๋ฅด๊ฒŒ } }); ``` ```tsx // React - ์ ‘๊ทผ์„ฑ ์นœํ™”์  ์Šคํฌ๋กค function AccessibleScrollApp() { const { scrollTo, scrollPosition } = useTwoDimensionScroll({ accessibility: { reducedMotion: true, keyboardNavigation: true, screenReader: true } }); const handleKeyDown = useCallback((e) => { switch (e.key) { case 'Home': e.preventDefault(); scrollTo(0, { duration: 800 }); break; case 'End': e.preventDefault(); scrollTo(document.body.scrollHeight, { duration: 800 }); break; case 'PageUp': e.preventDefault(); scrollTo(scrollPosition - window.innerHeight, { duration: 600 }); break; case 'PageDown': e.preventDefault(); scrollTo(scrollPosition + window.innerHeight, { duration: 600 }); break; } }, [scrollTo, scrollPosition]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); return ( <div role="main" aria-label="๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค ํŽ˜์ด์ง€"> <div style={{ height: '300vh', padding: '20px' }}> <h1>์ ‘๊ทผ์„ฑ ์นœํ™” ์Šคํฌ๋กค</h1> <p>ํ‚ค๋ณด๋“œ๋กœ ์Šคํฌ๋กคํ•˜์„ธ์š”: Home, End, PageUp, PageDown</p> <button onClick={() => scrollTo(0)} aria-label="ํŽ˜์ด์ง€ ์ƒ๋‹จ์œผ๋กœ ์ด๋™" > ๋งจ ์œ„๋กœ </button> </div> </div> ); } ``` ## ๐ŸŒ ๋ธŒ๋ผ์šฐ์ € ์ง€์› - โœ… Chrome 60+ - โœ… Firefox 55+ - โœ… Safari 12+ - โœ… Edge 79+ - โœ… iOS Safari 12+ - โœ… Android Chrome 60+ ## ๐Ÿ“ฑ ํ™˜๊ฒฝ๋ณ„ ์ตœ์ ํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์‚ฌ์šฉ์ž์˜ ํ™˜๊ฒฝ(PC/๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ)์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ ์ตœ์ ์˜ ์Šคํฌ๋กค ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ### ์ž๋™ ํ™˜๊ฒฝ ๊ฐ์ง€ ```javascript // ํ™˜๊ฒฝ๋ณ„ ์ž๋™ ์ตœ์ ํ™” const scroll = new TwoDimensionScroll({ desktop: { duration: 1000, lerp: 0.1, sensitivity: 1.2 }, mobile: { duration: 600, // ๋ชจ๋ฐ”์ผ์—์„œ ๋” ๋น ๋ฅธ ๋ฐ˜์‘ lerp: 0.15, // ๋” ์ง์ ‘์ ์ธ ๋ฐ˜์‘ touchStopThreshold: 3 // ํ„ฐ์น˜ ์ •์ง€ ๊ฐ๋„ }, tablet: { duration: 800, // ์ค‘๊ฐ„๊ฐ’ lerp: 0.12 } }); // ํ˜„์žฌ ํ™˜๊ฒฝ ์ •๋ณด ํ™•์ธ console.log(scroll.getEnvironmentInfo()); ``` ### React์—์„œ ํ™˜๊ฒฝ๋ณ„ ๋Œ€์‘ ```tsx function ResponsiveScrollComponent() { const { instance, scrollPosition } = useTwoDimensionScroll({ desktop: { duration: 1000 }, mobile: { duration: 600 }, tablet: { duration: 800 } }); useEffect(() => { if (instance) { const envInfo = instance.getEnvironmentInfo(); console.log('ํ˜„์žฌ ํ™˜๊ฒฝ:', envInfo.currentEnvironment); console.log('ํ„ฐ์น˜ ์ง€์›:', envInfo.isTouchDevice); } }, [instance]); return <div>ํ™˜๊ฒฝ ์ตœ์ ํ™” ์Šคํฌ๋กค ํ™œ์„ฑํ™”</div>; } ``` ## โš›๏ธ React ๊ฐœ๋ฐœ ํŒ ### ์„ฑ๋Šฅ ์ตœ์ ํ™” ```tsx // 1. ๋ฉ”๋ชจ์ด์ œ์ด์…˜์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ const ScrollComponent = memo(() => { const { scrollPosition, scrollTo } = useTwoDimensionScroll(); return ( <div>์œ„์น˜: {scrollPosition}</div> ); }); // 2. ์Šคํฌ๋กค ์ฝœ๋ฐฑ ์ตœ์ ํ™” function OptimizedScrollApp() { const scrollCallbackRef = useRef(); const { instance } = useTwoDimensionScroll(); useEffect(() => { if (!instance) return; // ์Šค๋กœํ‹€๋ง๋œ ์ฝœ๋ฐฑ scrollCallbackRef.current = throttle((data) => { console.log('์Šคํฌ๋กค:', data.scroll); }, 100); instance.on(scrollCallbackRef.current); return () => { if (scrollCallbackRef.current) { instance.off(scrollCallbackRef.current); } }; }, [instance]); } ``` ### ๋””๋ฒ„๊น… ๋ฐ ๊ฐœ๋ฐœ ๋ชจ๋“œ ```tsx // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋””๋ฒ„๊น… ํ™œ์„ฑํ™” const { instance, getReactInfo } = useTwoDimensionScroll({ debug: process.env.NODE_ENV === 'development' }); // React ํ˜ธํ™˜์„ฑ ์ •๋ณด ํ™•์ธ useEffect(() => { if (process.env.NODE_ENV === 'development') { console.log('React ํ˜ธํ™˜์„ฑ:', getReactInfo()); } }, [getReactInfo]); ``` ### ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ #### 1. SSR ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ```tsx // Next.js์—์„œ hydration ์˜ค๋ฅ˜ ๋ฐฉ์ง€ function SSRSafeScrollApp() { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); const { isReady } = useTwoDimensionScroll(); if (!isClient || !isReady) { return <div>Loading...</div>; } return <div>์Šคํฌ๋กค ์ค€๋น„ ์™„๋ฃŒ</div>; } ``` #### 2. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ```tsx // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ํ™•์‹คํ•œ ์ •๋ฆฌ function SafeScrollComponent() { const scrollRef = useRef(null); useEffect(() => { return () => { // ๊ฐ•์ œ ์ •๋ฆฌ if (scrollRef.current) { scrollRef.current.destroy(); scrollRef.current = null; } }; }, []); const { instance } = useTwoDimensionScroll(); scrollRef.current = instance; return <div>์•ˆ์ „ํ•œ ์Šคํฌ๋กค ์ปดํฌ๋„ŒํŠธ</div>; } ``` #### 3. ์ƒํƒœ ๋™๊ธฐํ™” ```tsx // ์™ธ๋ถ€ ์ƒํƒœ์™€ ์Šคํฌ๋กค ์œ„์น˜ ๋™๊ธฐํ™” function SyncedScrollApp() { const [externalScroll, setExternalScroll] = useState(0); const { scrollPosition, scrollTo } = useTwoDimensionScroll(); // ์™ธ๋ถ€ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์Šคํฌ๋กค ์œ„์น˜ ์—…๋ฐ์ดํŠธ useEffect(() => { if (Math.abs(externalScroll - scrollPosition) > 10) { scrollTo(externalScroll); } }, [externalScroll, scrollPosition, scrollTo]); // ์Šคํฌ๋กค ์œ„์น˜ ๋ณ€๊ฒฝ ์‹œ ์™ธ๋ถ€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ useEffect(() => { setExternalScroll(scrollPosition); }, [scrollPosition]); } ``` ### TypeScript ํƒ€์ž… ํ™œ์šฉ ```tsx import type { TwoDimensionScrollOptions, ScrollInfo, TwoDimensionScrollHookReturn } from 'two-dimension-scroll/react'; interface ScrollPageProps { initialOptions?: TwoDimensionScrollOptions; onScrollChange?: (info: ScrollInfo) => void; } const ScrollPage: React.FC<ScrollPageProps> = ({ initialOptions, onScrollChange }) => { const scrollHook: TwoDimensionScrollHookReturn = useTwoDimensionScroll( initialOptions ); useEffect(() => { if (scrollHook.scrollInfo && onScrollChange) { onScrollChange(scrollHook.scrollInfo); } }, [scrollHook.scrollInfo, onScrollChange]); return <div>ํƒ€์ž… ์•ˆ์ „ํ•œ ์Šคํฌ๋กค ํŽ˜์ด์ง€</div>; }; ``` ## ๐Ÿ”ง ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • ### ์˜์กด์„ฑ ์„ค์น˜ ```bash npm install ``` ### ๋นŒ๋“œ ```bash npm run build ``` ### ๊ฐœ๋ฐœ ๋ชจ๋“œ (watch) ```bash npm run dev ``` ### ๋ฐ๋ชจ ์‹คํ–‰ ```bash npm run serve ``` ๋ธŒ๋ผ์šฐ์ €์—์„œ `http://localhost:3000`์œผ๋กœ ์ ‘์†ํ•˜์—ฌ ๋ฐ๋ชจ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ## ๐ŸŽฎ ๋ฐ๋ชจ ํŽ˜์ด์ง€ ํ”„๋กœ์ ํŠธ์— ํฌํ•จ๋œ `index.html` ํŒŒ์ผ์„ ํ†ตํ•ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ฒดํ—˜ํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: - ๋‹ค์–‘ํ•œ ์Šคํฌ๋กค ๋ฐฉ์‹ ํ…Œ์ŠคํŠธ - ์‹ค์‹œ๊ฐ„ ์„ค์ • ๋ณ€๊ฒฝ - ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋กœ ์ด๋ฒคํŠธ ํ™•์ธ - ๋ชจ๋ฐ”์ผ๊ณผ ๋ฐ์Šคํฌํ†ฑ ํ™˜๊ฒฝ ๋ชจ๋‘ ์ง€์› ## ๐Ÿค ๊ธฐ์—ฌํ•˜๊ธฐ 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request ## ๐Ÿ“„ ๋ผ์ด์„ ์Šค MIT License. ์ž์„ธํ•œ ๋‚ด์šฉ์€ [LICENSE](LICENSE) ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜์„ธ์š”. ## ๐Ÿ™ ๊ฐ์‚ฌ์˜ ๋ง ์ด ํ”„๋กœ์ ํŠธ๋Š” [Lenis](https://github.com/studio-freight/lenis)์™€ ๊ฐ™์€ ํ›Œ๋ฅญํ•œ ์Šคํฌ๋กค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์—์„œ ์˜๊ฐ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. --- **Made with โค๏ธ for developers who love smooth scrolling** ## ๐Ÿš€ Build Scripts ### Development Build (์ฝ˜์†” ๋กœ๊ทธ ์œ ์ง€) ```bash npm run build:dev ``` - ์ฝ˜์†” ๋กœ๊ทธ ์œ ์ง€ (๋””๋ฒ„๊น…์šฉ) - ์†Œ์Šค๋งต ํฌํ•จ - ์••์ถ• ์—†์Œ ### Production Build (์ฝ˜์†” ๋กœ๊ทธ ์ œ๊ฑฐ) ```bash npm run build:prod # ๋˜๋Š” npm run build ``` - **๋ชจ๋“  ์ฝ˜์†” ๋กœ๊ทธ ์ž๋™ ์ œ๊ฑฐ** (`console.log`, `console.info`, `console.debug` ๋“ฑ) - `console.error`, `console.warn`์€ ์œ ์ง€ (์˜ต์…˜) - ์ฝ”๋“œ ์••์ถ• ๋ฐ ์ตœ์ ํ™” - ์†Œ์Šค๋งต ์ œ๊ฑฐ ### Watch Mode (๊ฐœ๋ฐœ์šฉ) ```bash npm run dev ``` - ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ๋นŒ๋“œ - ๊ฐœ๋ฐœ ๋ชจ๋“œ๋กœ ๋นŒ๋“œ --- ## ๐Ÿ“ฆ Build Configuration ### ์ฝ˜์†” ๋กœ๊ทธ ์ œ๊ฑฐ ์„ค์ • - **ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ**: ๋ชจ๋“  ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ž๋™ ์ œ๊ฑฐ - **๊ฐœ๋ฐœ ๋นŒ๋“œ**: ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์œ ์ง€ (๋””๋ฒ„๊น… ํŽธ์˜์„ฑ) - **Error/Warn**: ํ”„๋กœ๋•์…˜์—์„œ๋„ ์œ ์ง€ (์„ ํƒ ๊ฐ€๋Šฅ) ### ๋นŒ๋“œ ๋„๊ตฌ - **Rollup**: ๋ชจ๋“ˆ ๋ฒˆ๋“ค๋ง - **Babel**: ์ฝ”๋“œ ๋ณ€ํ™˜ ๋ฐ ์ฝ˜์†” ๋กœ๊ทธ ์ œ๊ฑฐ - **TypeScript**: ํƒ€์ž… ์ •์˜ ์ƒ์„ฑ - **Terser**: ์ฝ”๋“œ ์••์ถ• (ํ”„๋กœ๋•์…˜) ---