Skip to content

[Feature] wheel picker 컴포넌트 구현#185

Merged
seseoju merged 1 commit intodevelopfrom
feature/183
Apr 7, 2026
Merged

[Feature] wheel picker 컴포넌트 구현#185
seseoju merged 1 commit intodevelopfrom
feature/183

Conversation

@seseoju
Copy link
Copy Markdown
Member

@seseoju seseoju commented Apr 6, 2026

🛠️ 변경 사항

실제로 어떤 작업을 했는지 구체적으로 작성해주세요.

  • UI 수정 (Design)
  • 기능 추가 (Feature)
  • 버그 수정 (Bug)
  • 리팩토링 (Refactor)
  • 성능 개선 (Performance)
  • 테스트 추가 (Chore)
  • 기타:

세부 변경 내용

WheelPicker 디자인 시스템 컴포넌트를 추가했습니다. 스토리북에서 테스트 가능합니다.

파일 구조

components/wheel-picker/
├── WheelPicker.tsx          ← UI 렌더링
├── WheelPicker.css.ts       ← 스타일
├── WheelPicker.stories.ts   ← Storybook
└── model/
  ├── types.ts
  ├── useWheelPicker.ts    ← 상태 + 포인터 이벤트 구현 (useScrollAnimation 기능 기반)
  └── useScrollAnimation.ts ← 애니메이션 + DOM 조작

애니메이션

  • scrollTo: Lerp(선형 보간)로 목표 위치까지 ease-out 감속 이동
  • startFlick: 빠른 드래그 시 관성 스크롤 후 가장 가까운 아이템을 중앙에 배치 (매 프레임 velocity * 0.92 감쇠)

터치/드래그

  • Pointer Events API로 마우스·터치 통합 처리
  • setPointerCapture로 영역 밖 드래그 유지
  • 최근 5개 velocity 샘플 평균으로 flick 여부 판단

성능 최적화

  • 드래그 중 DOM 직접 조작(classList, style.transform)으로 React 리렌더링 없이 60fps 유지
  • 애니메이션 관련 값도 useRef로 관리

loop 모드

  • 실제 아이템 복사본(ghost)을 앞뒤에 배치, offset wrap으로 끊김 없는 무한 루프 구현

🔍 관련 이슈

관련 이슈를 링크해주세요. ex) close #23, related #23


📸 스크린샷 / GIF (선택)

2026-04-07.12.35.45.mov

⚠️ 주의 사항 / 리뷰 포인트

리뷰어가 특히 봐줬으면 하는 부분이나 고민했던 지점을 작성해주세요.

🔄 연관 작업

후속 작업이나 연관된 PR이 있다면 링크해주세요.

- 터치/마우스 드래그 지원 (Pointer Events API)
- 관성 스크롤 및 snap 애니메이션 (RAF + Lerp)
- loop 모드 (ghost 아이템 기반 무한 루프)
- Controlled/Uncontrolled 모드 지원
- 키보드 접근성 (ArrowUp/Down, Home, End)
- 드래그 중 React 리렌더링 없이 DOM 직접 조작
@seseoju seseoju requested a review from yummjin April 6, 2026 15:40
@seseoju seseoju self-assigned this Apr 6, 2026
@seseoju seseoju added the ✨ Feature 신규 기능을 추가합니다. label Apr 6, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new WheelPicker component to the design system, featuring custom scroll and flick animations implemented via requestAnimationFrame for high performance. The implementation includes comprehensive state management for both controlled and uncontrolled modes, keyboard navigation support, and a looping feature. Review feedback highlights several areas for improvement to align with project standards, specifically regarding the use of absolute imports, constant type assertions (as const), and avoiding index-based keys in lists. Additionally, there are recommendations to enhance accessibility by correctly managing ARIA attributes such as aria-selected and aria-activedescendant within both the component and the performance-optimized DOM manipulation logic.

Comment on lines +38 to +46
<div
ref={containerRef}
className={clsx(container, disabled && containerDisabled)}
style={{ height: `${ITEM_HEIGHT * visibleCount}px` }}
role="listbox"
aria-orientation="vertical"
tabIndex={disabled ? -1 : 0}
{...handlers}
>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

🔧 개선 제안

  • 접근성 향상을 위해 aria-labelaria-activedescendant를 추가하는 것이 좋습니다. 현재 선택된 아이템의 ID를 추적하여 스크린 리더가 인식할 수 있게 합니다.
    <div
      ref={containerRef}
      className={clsx(container, disabled && containerDisabled)}
      style={{ height: `${ITEM_HEIGHT * visibleCount}px` }}
      role="listbox"
      aria-label="Wheel Picker"
      aria-orientation="vertical"
      aria-activedescendant={disabled ? undefined : `wheel-picker-item-${selectedIndex}`}
      tabIndex={disabled ? -1 : 0}
      {...handlers}
    >
References
  1. 접근성 준수 (aria, semantic tag) (link)

))}
{renderedItems.map(({ item: pickerItem, realIndex, isGhost }, i) => (
<div
key={`${isGhost ? 'ghost' : 'real'}-${i}`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

❌ 위반 사항

  • [index key 금지] 리스트 렌더링 시 index를 key로 사용하는 것은 금지되어 있습니다. renderedItems 모델에서 고유한 key를 생성하여 전달받는 방식을 권장합니다.
References
  1. index key 금지 (link)

@@ -0,0 +1,190 @@
import { useRef, useCallback, useEffect } from 'react';

export const ITEM_HEIGHT = 44;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

❌ 위반 사항

  • [as const 사용] 상수는 as const를 사용하여 타입을 고정해야 합니다.
Suggested change
export const ITEM_HEIGHT = 44;
export const ITEM_HEIGHT = 44 as const;
References
  1. as const 사용 (link)

@seseoju seseoju merged commit 2b29d8f into develop Apr 7, 2026
1 of 2 checks passed
@seseoju seseoju deleted the feature/183 branch April 7, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 신규 기능을 추가합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] wheel picker 컴포넌트 구현

1 participant