--- url: /ko/guide/getting-started/overview.md --- # 개요 `@react-native-motion-kit/swipe-deck`는 React Native용 고성능 Tinder 스타일 swipe deck 라이브러리입니다. 부드러운 card stack, like/pass 버튼, progress 기반 overlay, type-safe compound API가 필요한 앱에 맞춰져 있습니다. ## 핵심 모델 주요 API는 typed compound component family입니다. - `createSwipeDeck()`가 item type에 맞는 deck family를 만듭니다. - `Root`는 data, gesture state, motion option, action, event, layout을 소유합니다. - `Card`는 bounded forward window 안에 mount된 item을 렌더링합니다. - Factory hook은 state, action, event, animated UI용 Reanimated shared value를 제공합니다. ```tsx import { createSwipeDeck } from '@react-native-motion-kit/swipe-deck'; type Profile = { id: string; name: string; }; const ProfileDeck = createSwipeDeck(); ``` Factory는 한 번 만들고 shared module에서 export하세요. 같은 factory의 `Root` 주변에서 그 factory hook을 사용해야 합니다. ## 이 라이브러리가 해결하는 것 - 전체 data set을 한 번에 렌더링하지 않습니다. - Stable key를 통해 promoted card가 item identity를 유지합니다. - Manual drag motion과 programmatic action motion을 따로 조정할 수 있습니다. - Interaction shared value는 UI thread에서 업데이트되어 gesture frame마다 React rerender를 만들지 않습니다. - Event는 swipe, undo, index change, end reached 같은 commit된 model 변화를 표현합니다. ## 핵심 개념 ### Bounded Forward Window 기본적으로 active index부터 최대 세 장을 mount합니다. 1. current card 2. next card 3. next buffered card 이 방식은 dismissed card를 다시 채우지 않으면서도 stack의 연속성을 유지합니다. `visibleCardCount`로 최대 budget을 조정할 수 있습니다. ### Stable Item Key `getKey`는 필수입니다. 같은 logical item은 swipe가 진행되어도 같은 key를 반환해야 하고, 서로 다른 item은 같은 key를 공유하지 않는 것이 좋습니다. Deck은 이 key를 각 mounted card의 React identity로 사용합니다. ### Commit Event와 Live Interaction Commit된 model 변화에는 event hook을 사용하세요. Card가 dragging, dismissing, restoring 중인지에 따라 frame-synchronous visual feedback이 필요하면 interaction shared value를 사용하세요. --- url: /ko/guide/getting-started/installation.md --- # 설치 패키지와 React Native gesture/animation peer dependency를 설치합니다. ```sh npm install @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` Yarn을 사용한다면: ```sh yarn add @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` ## 최소 지원 버전 | Package | Minimum | | ------------------------------ | -------- | | `react` | `18.0.0` | | `react-native` | `0.75.0` | | `react-native-gesture-handler` | `2.24.0` | | `react-native-reanimated` | `4.0.0` | | `react-native-worklets` | `0.5.0` | ## React Native 설정 사용 중인 React Native 또는 Expo 버전에 맞춰 Reanimated와 Worklets 설정을 완료하세요. Babel 설정에서는 `react-native-worklets/plugin`을 마지막 Babel plugin으로 추가해야 합니다. ```js title="babel.config.js" module.exports = { presets: ['module:@react-native/babel-preset'], plugins: ['react-native-worklets/plugin'], }; ``` Gesture Handler는 앱 surface가 `GestureHandlerRootView` 아래에 있어야 합니다. ```tsx import { GestureHandlerRootView } from 'react-native-gesture-handler'; export function AppRoot() { return {/* app */}; } ``` --- url: /ko/guide/getting-started/quick-start.md --- # 빠른 시작 Item type에 맞는 typed deck family를 하나 만드세요. 같은 factory의 `Root`, `Card`, hook, action, event가 같은 registry namespace를 공유합니다. ```tsx import { Text, View } from 'react-native'; import { createSwipeDeck, SwipeDeckMotion } from '@react-native-motion-kit/swipe-deck'; type Profile = { id: string; name: string; bio: string; }; const ProfileDeck = createSwipeDeck({ motion: SwipeDeckMotion.tinder({ drag: { mode: 'horizontal', liftYFactor: 0.15, }, rotation: { mode: 'grab-position', }, dismiss: { threshold: ({ width }) => width * 0.3, velocityThreshold: 800, minDuration: 300, maxDuration: 520, }, }), }); function ProfileDeckEvents() { ProfileDeck.useDeckEventListener('swipe', ({ item, direction, source }) => { console.log(item.name, direction, source); }); ProfileDeck.useDeckEventListener('endReached', () => { console.log('No more cards'); }); return null; } function ProfileCard({ profile, active }: { profile: Profile; active: boolean }) { return ( {active ? '현재 카드' : '다음 카드'} {profile.name} {profile.bio} ); } export function ProfileDeckScreen({ profiles }: { profiles: Profile[] }) { return ( <> item.id} visibleCardCount={3}> {({ item, isActive }) => } ); } ``` ## Control 추가 Counter와 button처럼 React로 렌더링되는 UI에는 state/action hook을 사용하세요. ```tsx function ProfileDeckControls() { const { activeIndex, count, canSwipe, canUndo, isCompleted } = ProfileDeck.useDeckState(); const { swipeLeft, swipeRight, undo } = ProfileDeck.useDeckActions(); const current = activeIndex >= 0 ? activeIndex + 1 : 0; return ( {isCompleted ? 'Done' : `${current} / ${count}`} Nope Undo Like ); } ``` Undo UX를 노출할 때만 `undoEnabled`를 켜세요. ```tsx item.id} undoEnabled> {({ item }) => } ``` --- url: /ko/guide/getting-started/ai.md --- # AI 사용 가이드 AI assistant에게 코드를 생성하게 할 때 아래 규칙을 함께 전달하세요. 가장 흔한 integration 실수를 줄일 수 있습니다. ## AI가 읽기 좋은 문서 이 사이트는 `rspress build` 시 AI 도구가 읽기 좋은 Markdown 파일을 함께 생성합니다. - [`/ko/llms.txt`](/ko/llms.txt): 페이지 링크와 요약을 담은 compact 문서 index입니다. Agent가 필요한 페이지만 골라 읽게 할 때 사용하세요. - [`/ko/llms-full.txt`](/ko/llms-full.txt): 한국어 문서 전체를 하나로 합친 Markdown 파일입니다. Agent의 context budget이 충분하고 전체 API를 이해해야 할 때 사용하세요. - [`/llms.txt`](/llms.txt)와 [`/llms-full.txt`](/llms-full.txt): English equivalents. 공개 문서 URL이 생기기 전에는 로컬에서 build한 뒤 `docs/doc_build/` 아래의 파일을 사용하세요. ## 추천 프롬프트 ```md Use @react-native-motion-kit/swipe-deck. - Create one createSwipeDeck() factory outside render. - Render Root and Card from that same factory. - Use a stable getKey(item) value, usually item.id. - Use factory hooks from the same factory instance as the Root. - Use useDeckState/useDeckActions for React UI. - Use useDeckInteraction for Reanimated UI-thread overlays. - Use useDeckEvent/useDeckEventListener for committed swipe, undo, indexChange, and endReached events. - Add undoEnabled only when the UI exposes undo. - Do not derive deck id from item ids, timestamps, or values that change each render. ``` ## 좋은 Factory 형태 ```tsx // profile-deck.ts import { createSwipeDeck } from '@react-native-motion-kit/swipe-deck'; export type Profile = { id: string; name: string; }; export const ProfileDeck = createSwipeDeck(); ``` ```tsx // ProfileDeckScreen.tsx import { ProfileDeck } from './profile-deck'; export function ProfileDeckScreen({ profiles }: { profiles: Profile[] }) { return ( item.id}> {({ item }) => } ); } ``` ## 피해야 할 실수 - Component render path 안에서 `createSwipeDeck()`을 호출하지 마세요. - 한 factory의 hook으로 다른 factory의 Root를 제어하지 마세요. - Item이 삽입/삭제/재정렬될 수 있다면 array index를 key로 쓰지 마세요. - Commit된 business logic에는 `useDeckInteraction`이 아니라 event hook을 사용하세요. - Undo control이 없다면 `undoEnabled`를 켜지 마세요. --- url: /ko/guide/usage/basic-usage.md --- # 기본 사용법 ## Factory API Factory API가 기본 API입니다. Deck state, action, event, interaction shared value, 또는 여러 named instance가 필요하다면 factory API를 사용하세요. ```tsx import { createSwipeDeck } from '@react-native-motion-kit/swipe-deck'; const ProfileDeck = createSwipeDeck(); function Screen() { return ( item.id}> {({ item }) => } ); } ``` Factory는 `Root`, `Card`, hook이 같은 item type을 공유하도록 해 JSX에서 generic을 반복하지 않아도 됩니다. ## Card Render Info `Card`는 mounted item마다 render info를 받습니다. | Field | 의미 | | ---------- | ----------------------------------------------------- | | `item` | 사용자의 `data` 배열에 있는 item입니다. | | `index` | `data` 안에서의 item index입니다. | | `offset` | active item으로부터의 card offset입니다. | | `role` | active card는 `'current'`, buffered card는 `'next'`입니다. | | `isActive` | active card에서만 `true`입니다. | ```tsx {({ item, role, isActive }) => } ``` ## Allowed Directions `allowedDirections`는 dismiss로 받아들일 방향을 제한합니다. ```tsx item.id} allowedDirections={['right']}> {({ item }) => } ``` - 생략하면 양방향을 모두 허용합니다. - `['left']` 또는 `['right']`는 한 방향만 허용합니다. - `[]`는 drag는 허용하지만 모든 dismiss release와 programmatic swipe action을 거절합니다. ## Static API Card rendering만 필요하고 factory hook이 필요 없다면 static API를 사용할 수 있습니다. ```tsx import { SwipeDeck } from '@react-native-motion-kit/swipe-deck'; function InlineDeck() { return ( item.id}> >{({ item }) => } ); } ``` Static `Root`는 `id`를 받지 않습니다. Static `Root`와 `Card`는 factory처럼 item type을 함께 고정하지 않으므로, typed render prop이 필요하면 `Card`에 item type을 직접 넘기세요. --- url: /ko/guide/usage/deck-hooks.md --- # Deck Hooks Factory hook은 Provider prop이나 controller object 없이 deck state, action, interaction 값을 제공합니다. ```tsx const ProfileDeck = createSwipeDeck(); ``` ## `useDeckState(id?)` React로 렌더링되는 deck state를 반환합니다. | Field | 의미 | | ------------- | -------------------------------------------- | | `activeIndex` | 현재 active item index입니다. attach 전에는 `-1`입니다. | | `count` | attach된 deck의 전체 item 수입니다. | | `isCompleted` | 모든 item을 소비했는지 여부입니다. | | `canSwipe` | dismiss action이 현재 받아들여질 수 있는지 여부입니다. | | `canUndo` | 최신 valid swipe를 현재 복원할 수 있는지 여부입니다. | ```tsx function Counter() { const { activeIndex, count, isCompleted } = ProfileDeck.useDeckState(); const current = activeIndex >= 0 ? activeIndex + 1 : 0; return {isCompleted ? 'Done' : `${current} / ${count}`}; } ``` 현재 item이 필요하면 사용자의 `data[activeIndex]`에서 직접 계산하세요. Deck state는 primitive하고 stable하게 유지됩니다. ## `useDeckActions(id?)` Stable action callback을 반환합니다. ```tsx function Controls() { const { canSwipe, canUndo } = ProfileDeck.useDeckState(); const { swipeLeft, swipeRight, undo } = ProfileDeck.useDeckActions(); return ( Nope Undo Like ); } ``` `swipeLeft()`와 `swipeRight()`는 action이 받아들여지면 `true`, deck이 unattached, disabled, animating, unmeasured, completed 상태이거나 direction이 허용되지 않으면 `false`를 반환합니다. `undo()`는 deck 완료 후에도 `canUndo`가 true이면 `true`를 반환합니다. ## `useDeckInteraction(id?)` Progress 기반 UI를 위한 Reanimated shared value를 반환합니다. ```tsx function SwipeReactionOverlay() { const { signedProgress } = ProfileDeck.useDeckInteraction(); const likeStyle = useAnimatedStyle(() => { const progress = Math.max(signedProgress.get(), 0); return { opacity: progress, transform: [{ scale: 0.9 + progress * 0.18 }], }; }); return ; } ``` Interaction value는 UI thread에서 업데이트되고 gesture frame마다 React rerender를 만들지 않습니다. | Value | 의미 | | ------------------ | ------------------------------------------------------ | | `progress` | `0`부터 `1`까지의 absolute swipe progress입니다. | | `signedProgress` | `-1`부터 `1`까지의 signed progress입니다. | | `direction` | raw live direction signal입니다. `-1`, `0`, `1`. | | `dismissDirection` | accepted dismiss side입니다. `'left'`, `'right'`, `null`. | | `translationX` | active card의 horizontal translation입니다. | | `translationY` | active card의 vertical translation입니다. | | `isDragging` | deck이 dragging 또는 dismissing 중인지 여부입니다. | | `phase` | `idle`, `dragging`, `dismissing`, `undoing`. | Frame-synchronous visual feedback에는 `phase`를 사용하세요. Commit된 state 변화에는 event hook을 사용하세요. --- url: /ko/guide/usage/handling-events.md --- # Event 처리 Event hook은 commit된 model event를 표현합니다. Live interaction value와 분리해서 생각하세요. ## Event Map | Event | Payload | | ------------- | ------------------------------------ | | `swipe` | `{ item, index, direction, source }` | | `undo` | `{ item, index, direction }` | | `indexChange` | `{ index }` | | `endReached` | `true` | `swipe.source`는 사용자가 pan gesture를 threshold/velocity 기준 이상으로 놓아 commit한 경우 `'gesture'`입니다. `actions.swipeLeft()` 또는 `actions.swipeRight()`로 commit된 경우 `'programmatic'`입니다. ## `useDeckEvent` `useDeckEvent(eventName, initialValue?, id?)`는 React 렌더링 UI를 위한 최신 commit event snapshot을 반환합니다. ```tsx function ProfileDeckStatus() { const lastSwipe = ProfileDeck.useDeckEvent('swipe', null); const endReached = ProfileDeck.useDeckEvent('endReached', false); return ( {endReached ? 'Done' : lastSwipe ? `Last swipe: ${lastSwipe.direction} from ${lastSwipe.source}` : 'No swipe yet'} ); } ``` 이 API는 latest-value API이며 event history가 아닙니다. Root가 attach/detach될 때 event snapshot은 clear됩니다. Initial value는 event payload shape, `null`, `undefined`, 또는 `endReached`의 `false`로 제한됩니다. `swipe` 같은 object event에는 `null`을 사용하세요. Initial value 없이 named deck을 읽어야 한다면 id를 두 번째 인자로 넘깁니다. ```tsx const lastSwipe = ProfileDeck.useDeckEvent('swipe', 'nearby'); ``` Initial value도 함께 넘기는 경우 id는 세 번째 인자입니다. ## `useDeckEventListener` `useDeckEventListener(eventName, listener, id?)`는 앱 코드에 별도 React state를 만들지 않고 imperative하게 event를 구독합니다. ```tsx function ProfileDeckEvents() { ProfileDeck.useDeckEventListener('swipe', ({ item, direction, source }) => { console.log(item, direction, source); }); ProfileDeck.useDeckEventListener('undo', ({ item }) => { console.log('Restored', item); }); ProfileDeck.useDeckEventListener('endReached', () => { console.log('No more cards'); }); return null; } ``` 성공한 swipe는 `swipe -> indexChange -> endReached` 순서로 emit됩니다. Undo는 `undo -> indexChange` 순서로 emit됩니다. --- url: /ko/guide/usage/programmatic-actions.md --- # Programmatic Actions Programmatic action은 `useDeckActions()`에서 가져옵니다. 버튼, 외부 control, 앱 로직에서 active card를 dismiss할 때 사용하세요. ```tsx function LikeButton() { const { swipeRight } = ProfileDeck.useDeckActions(); return ( Like ); } ``` ## Action Motion `motion`은 manual drag feel을 제어합니다. `actionMotion`은 `swipeLeft()`와 `swipeRight()` 같은 programmatic action만 제어합니다. ```tsx import { createSwipeDeck, SwipeDeckActionMotion, SwipeDeckMotion, } from '@react-native-motion-kit/swipe-deck'; const ProfileDeck = createSwipeDeck({ motion: SwipeDeckMotion.tinder(), actionMotion: SwipeDeckActionMotion.springboard({ anticipationDistance: ({ width }) => width * 0.04, anticipationDuration: 80, dismissDuration: 320, }), }); ``` ## Recipe ### `SwipeDeckActionMotion.direct(options?)` Action 방향으로 바로 dismiss합니다. 생략한 값은 deck에서 resolve된 dismiss duration, easing, offscreen multiplier를 재사용합니다. ```tsx actions.swipeLeft( SwipeDeckActionMotion.direct({ duration: 180, }), ); ``` ### `SwipeDeckActionMotion.springboard(options?)` 최종 방향의 반대쪽으로 살짝 움직인 뒤 화면 밖으로 dismiss합니다. Anticipation 동안 swipe progress와 live direction은 neutral 상태로 유지되어 반대쪽 overlay가 순간적으로 보이지 않습니다. ```tsx const actionMotion = SwipeDeckActionMotion.springboard({ anticipationDistance: 40, anticipationDuration: 160, dismissDuration: 500, }); ``` ## 우선순위 `actionMotion`은 deep merge가 아니라 replacement 방식입니다. 1. `createSwipeDeck({ actionMotion })`의 factory `actionMotion` 2. 해당 Root에서 factory default를 대체하는 `Root actionMotion` 3. `swipeLeft(recipe)` 또는 `swipeRight(recipe)`에 넘긴 per-call recipe Action은 callback으로 바로 넘겨도 안전합니다. React Native press event가 `swipeRight` 또는 `swipeLeft`로 전달되면 그 event 인자는 무시됩니다. --- url: /ko/guide/usage/undo.md --- # Undo Undo는 opt-in입니다. 해당 deck에서 undo/back-swipe UX를 제공할 때 Root에 `undoEnabled`를 추가하세요. ```tsx item.id} undoEnabled> {({ item }) => } ``` 활성화되면 성공한 swipe마다 key/index/direction metadata entry 하나를 LIFO undo stack에 저장합니다. Lookup은 현재 `data`에 대한 key-to-index map을 사용하고, data나 key가 바뀌면 invalid entry를 prune합니다. 생략하면 성공한 swipe도 undo metadata를 저장하지 않고, `canUndo`는 `false`를 유지하며, `actions.undo()`는 `false`를 반환합니다. ## Undo Button ```tsx function UndoButton() { const { canUndo } = ProfileDeck.useDeckState(); const { undo } = ProfileDeck.useDeckActions(); return ( Undo ); } ``` ## Undo Motion `SwipeDeckUndoMotion`으로 restored card가 들어오는 motion을 조정할 수 있습니다. ```tsx import { createSwipeDeck, SwipeDeckUndoMotion } from '@react-native-motion-kit/swipe-deck'; const ProfileDeck = createSwipeDeck({ undoMotion: SwipeDeckUndoMotion.spring({ springConfig: { damping: 36, stiffness: 300, mass: 3, }, }), }); ``` 제공되는 recipe는 다음과 같습니다. - `SwipeDeckUndoMotion.spring(options?)`: Reanimated `withSpring`으로 restore합니다. - `SwipeDeckUndoMotion.timing(options?)`: deterministic한 `withTiming`으로 restore합니다. 두 recipe는 공통으로 다음 옵션을 받습니다. - `from: 'auto' | 'left' | 'right'`: `auto`는 원래 swipe된 방향에서 card를 되돌립니다. - `entryDistance`: 화면 밖 시작 거리를 number 또는 layout callback으로 지정합니다. `timing`은 `duration`, `easing`을 추가로 받고 기본 duration은 `0`입니다. 별도 motion을 설정하지 않으면 card를 즉시 복원합니다. `spring`은 `springConfig`를 받습니다. ## 우선순위 `undoMotion`은 replacement 방식입니다. 1. `createSwipeDeck({ undoMotion })`의 factory `undoMotion` 2. 해당 Root에서 factory default를 대체하는 `Root undoMotion` 3. `actions.undo(recipe)`에 넘긴 per-call recipe Undo도 callback으로 바로 넘겨도 안전합니다. React Native press event가 `undo`로 전달되면 그 event 인자는 무시됩니다. --- url: /ko/guide/usage/motion.md --- # Motion `SwipeDeckMotion.tinder()`는 Tinder 스타일 card stack을 위한 기본 motion preset입니다. ```tsx import { SwipeDeckMotion } from '@react-native-motion-kit/swipe-deck'; const motion = SwipeDeckMotion.tinder({ drag: { mode: 'horizontal', liftYFactor: 0.15, }, rotation: { mode: 'grab-position', }, dismiss: { threshold: ({ width }) => width * 0.3, velocityThreshold: 800, minDuration: 300, maxDuration: 520, }, }); ``` ## Swipe Progress를 따라가는 것 Buffered next card는 swipe progress에 따라 애니메이션됩니다. - scale은 `1`에 가까워집니다. - opacity는 `1`에 가까워집니다. - `translateY`는 `0`에 가까워집니다. Swipe가 commit되면 dismissed card는 화면 밖으로 나가고, promoted next card는 item identity를 유지하며, 새로운 future item이 bounded window에 들어옵니다. ## Drag Mode `drag.mode`는 drag 중 active card가 손가락 translation을 어떻게 사용할지 제어합니다. | Mode | Active card 움직임 | | ------------ | ---------------------------- | | `free` | 손가락의 X/Y 이동을 모두 따라갑니다. | | `horizontal` | 손가락의 Y 이동은 무시하고 X 이동만 반영합니다. | `drag.liftYFactor`는 active card를 `abs(translationX) * liftYFactor`만큼 위로 올립니다. ## Rotation `rotation.mode`는 회전 기준점을 고정할지, gesture 시작 위치에서 계산할지 제어합니다. ### Fixed Rotation 모든 gesture가 같은 anchor를 사용해야 한다면 fixed rotation을 사용하세요. | Origin | 느낌 | | --------------- | --------------------------- | | `center` | 카드 중앙을 기준으로 회전합니다. | | `top-center` | 카드 위쪽 edge가 고정된 축처럼 느껴집니다. | | `bottom-center` | 카드 아래쪽 edge가 고정된 축처럼 느껴집니다. | ```tsx SwipeDeckMotion.tinder({ rotation: { mode: 'fixed', origin: 'bottom-center', direction: 'default', }, }); ``` 같은 고정 anchor에서 회전 부호만 반대로 만들고 싶다면 `direction: 'reverse'`를 사용하세요. ### Grab-Position Rotation Grab-position rotation은 기본 Tinder-like 동작입니다. ```tsx SwipeDeckMotion.tinder({ rotation: { mode: 'grab-position', direction: 'default', maxDegrees: 25, }, }); ``` 카드 위쪽에서 잡으면 top-center anchor와 default rotation sign을 사용합니다. 아래쪽에서 잡으면 bottom-center anchor와 reverse rotation sign을 사용합니다. `direction: 'reverse'`는 이 mapping을 뒤집습니다. ## Dismiss Target `dismiss.offscreenMultiplier`는 성공한 swipe의 release target을 제어합니다. - 성공한 swipe는 항상 화면 밖으로 dismiss됩니다. - 기본값 `1.5`는 카드를 `clearDistance * 1.5` 위치로 보냅니다. - `clearDistance`는 swipe direction, rotation mode, rotation direction, gesture start position으로 계산합니다. - `1`보다 작은 값은 `1`로 정규화됩니다. - `duration`을 생략하면 target까지 남은 거리로 velocity-derived timing을 계산합니다. 대부분의 앱은 `threshold`, `velocityThreshold`, `duration`, `minDuration`, `maxDuration`, `easing`만 조정해도 충분합니다. ## Motion 우선순위 Motion 값은 다음 순서로 resolve됩니다. 1. `createSwipeDeck({ motion })`의 factory motion default 2. 명시한 field만 부분 override하는 `Root motion` 3. `swipeThreshold`, `velocityThreshold` 같은 direct root prop Factory motion과 Root motion은 deep merge됩니다. `rotation.mode`를 바꿔도 `maxDegrees`, `inputRange` 같은 numeric rotation tuning은 reset되지 않습니다. ## Preset 안정성 Motion preset은 module-scope constant 또는 `useMemo`로 안정적으로 유지하세요. ```tsx const profileDeckMotion = SwipeDeckMotion.tinder({ rotation: { mode: 'fixed', origin: 'bottom-center', }, dismiss: { threshold: ({ width }) => width * 0.3, }, }); const ProfileDeck = createSwipeDeck({ motion: profileDeckMotion, }); ``` --- url: /ko/guide/usage/multi-instance-management.md --- # Multi-Instance Management 같은 factory에서 여러 Root를 렌더링할 때만 `id`를 사용하세요. ```tsx function MultiDeckScreen() { const nearbyState = ProfileDeck.useDeckState('nearby'); return ( <> item.id}> {({ item }) => } item.id}> {({ item }) => } {nearbyState.activeIndex + 1} ); } ``` `id`는 item key가 아니라 factory 안에서 deck instance를 구분하는 namespace입니다. 서로 다른 factory는 둘 다 default id를 써도 충돌하지 않지만, 같은 factory와 같은 id의 Root 두 개가 동시에 mount되는 것은 잘못된 사용입니다. ## Id 규칙 - 안정적이고 적은 개수의 id를 사용하세요. - `"nearby"`, `"recommended"` 같은 화면/용도 단위 이름을 사용하세요. - Item id, timestamp, 매 render마다 바뀌는 값, 일회성 route 값에서 id를 만들지 마세요. - Factory와 id는 render path 밖에서 안정적으로 만들고 유지하세요. Registry는 hook, action, interaction shared value의 identity를 안정적으로 유지하기 위해 factory lifetime 동안 id별 store를 유지합니다. ## Same-Factory Rule Hook, action, interaction은 같은 factory instance에서 만든 Root에만 연결됩니다. ```ts const ProfileDeck = createSwipeDeck(); export const { Root: ProfileDeckRoot, Card: ProfileDeckCard, useDeckState: useProfileDeckState, useDeckActions: useProfileDeckActions, useDeckInteraction: useProfileDeckInteraction, } = ProfileDeck; ``` Item type과 id가 같아도 `createSwipeDeck()`를 다시 호출하면 별도 registry namespace가 만들어집니다. --- url: /ko/guide/usage/visible-card-budget.md --- # Visible Card Budget `visibleCardCount`는 active card부터 forward 방향으로 mount할 card의 최대 개수를 제어합니다. ```tsx function CurrentOnlyDeck() { return ( item.id} visibleCardCount={1}> {/* 현재 카드만 렌더링 */} ); } function DefaultDeck() { return ( item.id} visibleCardCount={3}> {/* 더 자연스러운 next-card promotion을 위한 기본 budget */} ); } function DeepStackDeck() { return ( item.id} visibleCardCount={9}> {/* 더 깊은 stack UI */} ); } ``` ## 규칙 | 입력 | Mount되는 card | | ------------------------------------- | ------------------ | | `visibleCardCount={1}` | 현재 카드만 | | `visibleCardCount={2}` | 데이터가 충분할 때 최대 두 장 | | 남은 데이터가 10개이고 `visibleCardCount={20}` | 최대 남은 10장 | | 짝수 값 | 최대 budget으로 그대로 유지 | `1`보다 작은 값은 forward data가 충분할 때 `1`로 정규화됩니다. 실제 mount 개수는 active index부터 남은 data 개수를 넘지 않습니다. UI가 허용하는 한 작은 budget을 유지하세요. 더 깊은 stack이 실제로 보이거나 더 많은 card를 미리 mount해야 할 때만 늘리는 것을 권장합니다. --- url: /ko/guide/usage/api-reference.md --- # API 레퍼런스 ## 주요 export ```ts export { SwipeDeckActionMotion, SwipeDeckMotion, createSwipeDeck, SwipeDeck, SwipeDeckUndoMotion, } from '@react-native-motion-kit/swipe-deck'; ``` [Source 보기](https://github.com/react-native-motion-kit/react-native-swipe-deck/blob/main/src/index.tsx) ## `createSwipeDeck(config?)` Typed deck family를 만듭니다. ```tsx const ProfileDeck = createSwipeDeck({ motion: SwipeDeckMotion.tinder(), }); ``` 반환되는 instance는 다음을 포함합니다. - `Root` - `Card` - `useDeckState` - `useDeckActions` - `useDeckInteraction` - `useDeckEvent` - `useDeckEventListener` ## `SwipeDeck.Root` props [Source 보기](https://github.com/react-native-motion-kit/react-native-swipe-deck/blob/main/src/types.ts) | Prop | Type | 설명 | | ------------------- | ------------------------------------ | ------------------------------------------- | | `id` | `string` | Factory-scoped deck namespace입니다. | | `data` | `readonly T[]` | Deck이 렌더링할 ordered item입니다. | | `getKey` | `(item: T, index: number) => string` | 필수 stable item key입니다. | | `initialIndex` | `number` | 초기 active item index입니다. | | `disabled` | `boolean` | accepted action과 gesture를 비활성화합니다. | | `allowedDirections` | `AllowedDirections` | 허용할 dismiss direction을 제한합니다. | | `swipeThreshold` | `SwipeThreshold` | Root-level dismiss threshold override입니다. | | `velocityThreshold` | `number` | Root-level flick threshold override입니다. | | `motion` | `SwipeDeckMotionPreset` | Gesture-driven motion preset입니다. | | `actionMotion` | `SwipeDeckActionMotionRecipe` | Programmatic swipe motion recipe입니다. | | `undoMotion` | `SwipeDeckUndoMotionRecipe` | Programmatic undo restore motion recipe입니다. | | `undoEnabled` | `boolean` | Undo history tracking을 활성화합니다. | | `visibleCardCount` | `number` | Forward mounted card의 최대 budget입니다. | | `containerStyle` | `StyleProp` | Deck container에 적용할 style입니다. | | `children` | `ReactNode` | Matching `SwipeDeck.Card`를 포함해야 합니다. | ```ts type AllowedDirections = readonly ('left' | 'right')[]; type SwipeThreshold = number | ((layout: SwipeDeckLayout) => number); ``` Static `SwipeDeck.Root`는 `id`를 제외한 같은 props를 받습니다. ## `SwipeDeck.Card` props | Prop | Type | 설명 | | ---------- | ----------------------------------------- | -------------------------------- | | `style` | `StyleProp` | Absolute card container에 적용됩니다. | | `children` | `(info: SwipeRenderInfo) => ReactNode` | Bounded window의 item 하나를 렌더링합니다. | `SwipeRenderInfo`는 `item`, `index`, `offset`, `role`, `isActive`를 포함합니다. ## State와 Actions ```ts type SwipeDeckState = { activeIndex: number; count: number; isCompleted: boolean; canSwipe: boolean; canUndo: boolean; }; type SwipeDeckActions = { swipeLeft: SwipeDeckAction; swipeRight: SwipeDeckAction; undo: SwipeDeckUndoAction; }; ``` Programmatic action은 action이 받아들여졌는지 알려주는 `boolean`을 반환합니다. ## Interaction ```ts type SwipeDeckInteractionPhase = 'idle' | 'dragging' | 'dismissing' | 'undoing'; ``` `useDeckInteraction()`은 `progress`, `signedProgress`, `direction`, `dismissDirection`, `translationX`, `translationY`, `isDragging`, `phase` Reanimated shared value를 반환합니다. ## Events ```ts type SwipeDeckEventMap = { swipe: SwipeEvent; undo: UndoEvent; indexChange: { index: number }; endReached: true; }; ``` 최신 event snapshot에는 `useDeckEvent`를, imperative subscription에는 `useDeckEventListener`를 사용하세요. ## Motion Helpers - `SwipeDeckMotion.tinder(options?)` - `SwipeDeckActionMotion.direct(options?)` - `SwipeDeckActionMotion.springboard(options?)` - `SwipeDeckUndoMotion.spring(options?)` - `SwipeDeckUndoMotion.timing(options?)` 동작 세부 사항은 이 reference보다 guide page를 먼저 참고하는 것을 권장합니다. --- url: /ko/index.md --- # React Native Swipe Deck React Native용 Tinder 스타일 swipe deck > Reanimated, Worklets, Gesture Handler 기반의 고성능 card stack [빠른 시작](/ko/guide/getting-started/quick-start.html) | [GitHub](https://github.com/react-native-motion-kit/react-native-swipe-deck) ## Features - 🤌 **Gesture 중심 Deck UX**: Tinder 스타일 card stack에 맞춰 drag, flick, threshold, direction control을 조정합니다. - 🏎️ **고성능 애니메이션**: React Native Reanimated shared value와 worklet 기반으로 부드러운 card motion을 만듭니다. - 🪟 **Bounded Render Window**: 전체 데이터가 아니라 active card와 작은 forward stack만 mount합니다. - 🧬 **Item-Stable Promotion**: 안정적인 item key로 promoted card가 React Native view identity를 유지합니다. - 🧠 **Typed Compound API**: Root, Card, hook, action, event를 하나의 typed deck family로 묶습니다. - 🎛️ **외부 제어 API**: button이나 다른 UI에서 swipeLeft, swipeRight, undo를 programmatic하게 실행합니다. - 🎨 **Motion Recipes**: gesture motion, programmatic action, undo restore를 각각 독립적으로 조정합니다. - 🧩 **Multi-Instance Management**: 안정적인 factory-scoped id로 여러 deck root를 독립적으로 관리합니다. - ↩️ **Undo Support**: opt-in undo history와 action-safe restore motion으로 back-swipe UX를 만듭니다. - 🪄 **쉬운 API**: Root와 Card로 시작하고, control이나 event가 필요할 때만 hook을 추가합니다.