--- url: /ko/guide/getting-started/overview.md --- # 개요 ![react-native-swipe-deck preview](https://raw.githubusercontent.com/react-native-motion-kit/react-native-swipe-deck/main/assets/react-native-swipe-deck-preview.webp) `@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] npm @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` ```sh [yarn] yarn @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` ```sh [pnpm] pnpm @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` ```sh [bun] bun @react-native-motion-kit/swipe-deck react-native-gesture-handler react-native-reanimated react-native-worklets ``` ```sh [deno] deno @react-native-motion-kit/swipe-deck npm:react-native-gesture-handler npm:react-native-reanimated npm: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 버전에 맞춰 공식 setup guide를 확인하세요. - [React Native Reanimated getting started](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) - [React Native Gesture Handler installation](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation) 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를 공유합니다. ## 예제 & 데모 - [📁 예제 프로젝트](https://github.com/react-native-motion-kit/react-native-swipe-deck/tree/main/example) - 다양한 use case를 담은 실제 구현 코드입니다. - [🤖 Expo Snack](https://snack.expo.dev/@harang/react-native-swipe-deck) - Expo Snack에서 바로 체험할 수 있습니다. ```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, swipeUp, undo } = ProfileDeck.useDeckActions(); const current = activeIndex >= 0 ? activeIndex + 1 : 0; return ( {isCompleted ? 'Done' : `${current} / ${count}`} Nope Undo Like Up ); } ``` Up action을 사용하려면 `Root`에 `allowedDirections={['left', 'right', 'up']}`을 추가하세요. Upward gesture에는 `drag.mode: 'free'`도 필요합니다. 위의 horizontal 설정은 손가락의 수직 이동을 의도적으로 비활성화합니다. 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: - [`https://react-native-swipe-deck.pages.dev/ko/llms.txt`](https://react-native-swipe-deck.pages.dev/ko/llms.txt) - [`https://react-native-swipe-deck.pages.dev/ko/llms-full.txt`](https://react-native-swipe-deck.pages.dev/ko/llms-full.txt) - [`https://react-native-swipe-deck.pages.dev/llms.txt`](https://react-native-swipe-deck.pages.dev/llms.txt) - [`https://react-native-swipe-deck.pages.dev/llms-full.txt`](https://react-native-swipe-deck.pages.dev/llms-full.txt) ## 추천 프롬프트 ```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 }) => } ``` ## Interactive Card Content Card는 기본적으로 non-interactive입니다. 그래야 deck이 swipe gesture를 안전하게 소유할 수 있습니다. Active card 안에 CTA button이나 link 같은 touchable children이 있다면 `interactive`를 사용하세요. ```tsx {({ item }) => openAd(item)} />} ``` 일부 item에서만 touchable children을 열어야 한다면 predicate form을 사용하세요. ```tsx item.type === 'ad'}> {({ item }) => } ``` `interactive`는 active card에만 적용됩니다. 뒤쪽 card는 계속 non-interactive입니다. Card 내부 carousel이나 scroll view처럼 더 깊은 nested gesture 조정은 raw `pointerEvents`를 노출하기보다 별도의 gesture-coordination API로 다루는 것이 좋습니다. ## Allowed Directions `allowedDirections`는 dismiss로 받아들일 방향을 제한합니다. ```tsx item.id} allowedDirections={['left', 'right', 'up']} > {({ item }) => } ``` - 생략하면 left와 right를 허용합니다. - upward dismiss는 `'up'`을 명시적으로 포함해야 합니다. - gesture up은 기본값인 `SwipeDeckMotion.tinder({ drag: { mode: 'free' } })`에서 동작합니다. - `drag.mode: 'horizontal'`은 gesture up만 거절하며 `actions.swipeUp()`은 막지 않습니다. - `[]`는 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, swipeUp, undo } = ProfileDeck.useDeckActions(); return ( Nope Undo Like Up ); } ``` `swipeLeft()`, `swipeRight()`, `swipeUp()`은 action이 받아들여지면 `true`, deck이 unattached, disabled, animating, unmeasured, completed 상태이거나 direction이 허용되지 않으면 `false`를 반환합니다. `swipeUp()`은 `allowedDirections`에 `'up'`이 있어야 하지만 horizontal drag mode에는 막히지 않습니다. `undo()`는 deck 완료 후에도 `canUndo`가 true이면 `true`를 반환합니다. ## `useDeckInteraction(id?)` Progress 기반 UI를 위한 Reanimated shared value를 반환합니다. ```tsx function SwipeReactionOverlay() { const { intentDirection, progress, signedProgress } = ProfileDeck.useDeckInteraction(); const likeStyle = useAnimatedStyle(() => { const progress = Math.max(signedProgress.get(), 0); return { opacity: progress, transform: [{ scale: 0.9 + progress * 0.18 }], }; }); const upStyle = useAnimatedStyle(() => { const upProgress = intentDirection.get() === 'up' ? progress.get() : 0; return { opacity: upProgress, transform: [{ scale: 0.9 + upProgress * 0.18 }], }; }); return ( LIKE SUPER LIKE ); } ``` Interaction value는 UI thread에서 업데이트되고 gesture frame마다 React rerender를 만들지 않습니다. | Value | 의미 | | ------------------ | ----------------------------------------------------------------------------- | | `progress` | `0`부터 `1`까지의 dominant enabled-axis progress입니다. | | `signedProgress` | horizontal signed progress입니다. Up은 `0`입니다. | | `direction` | horizontal live signal입니다. `-1`, `0`, `1`; up은 `0`. | | `intentDirection` | policy-filtered live semantic intent입니다. `'left'`, `'right'`, `'up'`, `null`. | | `dismissDirection` | accepted dismiss side입니다. `'left'`, `'right'`, `'up'`, `null`. | | `translationX` | active card의 horizontal translation입니다. | | `translationY` | active card의 vertical translation입니다. | | `isDragging` | deck이 dragging 또는 dismissing 중인지 여부입니다. | | `phase` | `idle`, `dragging`, `dismissing`, `undoing`. | 한 번에 하나의 reaction overlay를 보여줄 때는 `intentDirection`을 사용하세요. 이 값은 drag frame마다 업데이트되고, accepted direction은 dismiss 중 유지되며, 다른 interaction value와 함께 reset됩니다. 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()`, `actions.swipeUp()`으로 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, swipeUp } = ProfileDeck.useDeckActions(); return ( <> Like Up ); } ``` `swipeUp()`은 Root의 `allowedDirections`에 `'up'`이 명시되어 있을 때만 받아들여집니다. Gesture up은 free drag mode가 필요하지만, programmatic `swipeUp()`은 horizontal drag mode에서도 사용할 수 있습니다. ## Action Motion `motion`은 manual drag feel을 제어합니다. `actionMotion`은 `swipeLeft()`, `swipeRight()`, `swipeUp()` 같은 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)`, `swipeUp(recipe)`에 넘긴 per-call recipe Action은 callback으로 바로 넘겨도 안전합니다. React Native press event가 `swipeRight`, `swipeLeft`, `swipeUp`으로 전달되면 그 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 규칙 ::: info mount된 deck마다 안정적인 id를 사용하세요 `id`는 item key가 아니라 factory 안에서 deck instance를 구분하는 namespace입니다. 서로 다른 factory는 같은 id를 안전하게 사용할 수 있지만, 같은 factory에서 동시에 mount되는 Root에는 서로 다른 id를 사용해야 합니다. 화면이 mount된 동안 바뀌지 않는 navigation route key를 사용할 수 있습니다. ::: - `"nearby"`, `"recommended"` 같은 화면/용도 단위 이름이나, 화면이 mount되어 있는 동안 변하지 않는 navigation route key를 사용하세요. - 같은 factory에서 동시에 mount되는 Root는 서로 다른 id를 가져야 합니다. Duplicate Root 규칙은 그대로입니다. 같은 factory와 같은 id의 Root 두 개가 동시에 mount되는 것은 잘못된 사용입니다. - Item id, timestamp, 매 render마다 바뀌는 값, mounted lifecycle 중에 바뀌는 값에서 id를 만들지 마세요. - Factory는 render path 밖에서 만들고, 각 id는 mounted lifecycle 동안 안정적으로 유지하세요. Registry는 하나 이상의 committed Root 또는 public hook consumer가 해당 id를 retain하는 동안 hook, action, interaction shared value의 identity를 안정적으로 유지합니다. 마지막 committed consumer가 cleanup되고 registry의 deferred eviction이 끝난 뒤 같은 id를 다시 사용하면 새 action과 interaction shared value를 받습니다. Never-committed abandoned render는 known best-effort cleanup limitation입니다. Render가 새 id를 읽었지만 React가 commit하지 않으면 library가 관찰할 committed cleanup path가 없습니다. 화면이 mount되어 있는 동안 바뀌는 값으로 id를 만들지 마세요. ## 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' | 'up')[]; type SwipeThreshold = number | ((layout: SwipeDeckLayout) => number); ``` Static `SwipeDeck.Root`는 `id`를 제외한 같은 props를 받습니다. 전체 id contract는 [Multi-Instance Management](/ko/guide/usage/multi-instance-management.md)를 참고하세요. `id`는 item key가 아닙니다. `"nearby"`, `"recommended"` 같은 안정적인 deck namespace나, 화면이 mount되어 있는 동안 변하지 않는 navigation route key를 사용하세요. 해당 id를 retain하는 committed Root 또는 public hook consumer가 하나 이상 mount되어 있는 동안 identity가 안정적으로 유지됩니다. 마지막 cleanup과 deferred eviction 이후 같은 id를 다시 사용하면 새 action과 interaction shared value를 받습니다. Item id, timestamp, mounted lifecycle 중에 바뀌는 값에서 id를 만들지 마세요. 같은 factory의 Root 두 개가 동시에 mount되려면 여전히 서로 다른 id가 필요합니다. ## `SwipeDeck.Card` props | Prop | Type | 설명 | | ------------- | ---------------------------------------------------- | ------------------------------------------ | | `interactive` | `boolean \| ((info: SwipeRenderInfo) => boolean)` | Active card 안의 touchable children을 활성화합니다. | | `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; swipeUp: SwipeDeckAction; undo: SwipeDeckUndoAction; }; ``` Programmatic action은 action이 받아들여졌는지 알려주는 `boolean`을 반환합니다. ## Interaction ```ts type SwipeDeckInteractionPhase = 'idle' | 'dragging' | 'dismissing' | 'undoing'; ``` `SwipeDirection`은 `'left' | 'right' | 'up'`입니다. `allowedDirections`를 생략하면 left와 right만 허용되고 upward dismiss는 opt-in입니다. `useDeckInteraction()`은 `progress`, `signedProgress`, `direction`, `intentDirection`, `dismissDirection`, `translationX`, `translationY`, `isDragging`, `phase` Reanimated shared value를 반환합니다. `intentDirection`은 overlay용 policy-filtered live semantic source이며 `'left'`, `'right'`, `'up'`, `null`이 될 수 있습니다. Centered upward gesture만 internal up cone에 들어가고, 일반적인 upper-left/upper-right diagonal은 horizontal intent로 남습니다. 해당 raw horizontal winner가 허용되지 않으면 `intentDirection`, `progress`, `signedProgress`는 neutral이고 card translation은 손가락을 그대로 따릅니다. `signedProgress`와 numeric `direction`은 horizontal-only라 upward intent/dismiss 중에는 `0`입니다. `dismissDirection`은 dismiss가 accepted된 뒤 `'left'`, `'right'`, `'up'`, `null`이 될 수 있습니다. ## 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, swipeUp, 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을 추가합니다.