Skip to content

Conversation

@youdaeng2
Copy link
Member

@youdaeng2 youdaeng2 commented Dec 6, 2025

📌 개요

투표 생성 페이지(SOS-49)에 애니메이션 기반 옵션 필드 추가/삭제 UX 개선을 적용하고,
커스텀 RoundCheckbox 컴포넌트를 새로 만들고 투표 설정 영역에 적용했습니다.


🗒 상세 설명

1. 투표 옵션 필드 애니메이션 적용

옵션 추가/삭제 시 화면이 순간적으로 튀는 문제를 해결하기 위해
motion.div, AnimatePresence를 활용하여 자연스러운 애니메이션을 추가했습니다.

🔧 핵심 구현사항

  • 옵션 추가/삭제에 fade-in / fade-out + y-offset 트랜지션 적용
  • layout을 사용하여 입력 필드 간 자연스러운 리플로우 제공
  • MAX_OPTIONS, MIN_OPTIONS로 옵션 개수 제한을 명확히 선언
  • watch + getValues 기반으로 RHF와 애니메이션 충돌 없는 구조로 조정

2. 옵션 개수 제한 로직 개선 (RHF watch 기반)

기존에는 fields.length만 의존해 불안정성이 있었는데
watch('voteOptions') 기반으로 변동을 안정적으로 감지하도록 수정했습니다.

🔧 핵심 구현사항

  • watch('voteOptions')로 실시간 옵션 개수 추적
  • canAddMore / canRemove 명확하게 조건 분리
  • append/remove 시 MIN_OPTIONS 조건 체크
  • 빠르게 연타했을 때도 최소 2개 / 최대 5개가 깨지지 않도록,
    버튼 노출 조건 + 실제 삭제 시점 둘 다에서 길이 검사 이중 가드 처리

watch를 쓰게 되었는지

  • 기존 구현은 useFieldArray에서 내려오는 fields.length만 보고 판단해서,

    • 애니메이션, 렌더 타이밍, RHF 내부 상태 업데이트 순서에 따라
      실제 폼 값(voteOptions)과 fields 길이가 잠깐씩 어긋날 수 있는 문제가 있었습니다.
  • 이 때문에 옵션 추가/삭제를 빠르게 반복하면

    • 버튼 표시 조건과 실제 삭제 로직 사이에서 옵션 개수가 엇갈리는 케이스가 발생할 여지가 있어,
    • 옵션 개수의 단일 소스로 폼 값 자체를 구독하는 방식으로 바꿨습니다.

watch('voteOptions')

  • watchreact-hook-form에서 특정 필드 값을 구독하는 함수

    • watch('voteOptions')"현재 폼에 들어있는 voteOptions 배열"을 그대로 가져옵니다.
  • 이번 PR에서는:

    • 옵션 개수 제한 로직(2~5개)
    • 삭제 버튼 노출 조건(canRemove)
    • 추가 버튼 노출 조건(canAddMore)
      를 모두 watch('voteOptions') 기준으로 맞춰
      → 애니메이션 / 버튼 노출 / 실제 데이터 상태가 한 소스에서 일관되도록 정리했습니다.

3. RoundCheckbox 컴포넌트 생성

디자인 시스템에 맞는 라운드형 체크박스 UI가 필요해
완전히 새 컴포넌트로 구현하여 투표 설정 영역에 적용했습니다.

🔧 핵심 구현사항

  • peer 기반으로 체크/언체크 상태 스타일 처리
  • 소소톤 브랜드 컬러(SOSO-500) 적용
  • label 텍스트를 포함하는 컴포넌트 형태로 구성
  • RHF register와 자연스럽게 연결 가능 (forwardRef 적용)

🧩 사용 예시

<RoundCheckbox
  label="복수 선택 허용"
  {...register('allowMultipleChoice')}
/>

4. 전체 UI 구조 정리 및 스크롤 문제 개선

  • 폼 영역에 overflow-y-auto 적용
  • 하단 버튼을 sticky bottom-0으로 고정해 UX 개선
  • 기존 absolute 버튼 → scroll 영역과 충돌하던 문제 해결

📸 스크린샷

AS-IS

  • 옵션 추가/삭제 시 화면 튐
  • 체크박스 기본 브라우저 스타일
  • 버튼 absolute로 화면 가림
as.mp4

TO-BE

  • 매끄러운 옵션 애니메이션
  • 커스텀 RoundCheckbox 적용
  • 하단 고정 버튼으로 작성 플로우 개선
  • (디자인 수정사항에 맞춰 체크박스 스타일을 수정했습니다 25.12.07)
checkbox.mp4
tobe.mp4

🔗 이슈

closes #SOS-49


✅ 체크리스트

  • 코드가 스타일 가이드를 따릅니다
  • 자체 코드 리뷰 완료
  • 핵심 로직에 주석 추가
  • 애니메이션 관련 부작용 여부 점검
  • 옵션 변경 관련 RHF 작동 확인
  • Vercel Preview로 동작 검증 완료

🧪 테스트 방법

  • 옵션 추가/삭제 시 애니메이션 자연스러운지 수동 테스트
  • 옵션 개수 제한 동작(2~5개) 확인
  • RoundCheckbox 동작 검증
  • 폼 제출 시 값 정상 반영되는지 확인

📝 추가 노트

  • 성능 저하되는 곳이 있을지 확인 필요
  • 옵션 개수 관련 제약은 MAX_OPTIONS, MIN_OPTIONS 상수로 분리돼 있어
    향후 정책 변경 시 해당 상수만 조정하면 됩니다.
  • 체크박스 디자인은 추후 pm님과 협의 후 변경될 수 있습니다.

🚀 후속 작업

  • 투표 수정 모드에서의 애니메이션 비활성화 여부 고려
  • RoundCheckbox를 다른 입력 UI(Form, 필터 등)에도 재사용하는 방향 검토 예정

@linear
Copy link

linear bot commented Dec 6, 2025

@youdaeng2 youdaeng2 requested a review from DreamPaste December 6, 2025 04:10
@youdaeng2 youdaeng2 self-assigned this Dec 6, 2025
@youdaeng2 youdaeng2 added Feat 💡 새로운 기능을 구현하고 추가합니다! 유진 labels Dec 6, 2025
@youdaeng2 youdaeng2 changed the title Feat/sos 49 vote create page animation Feat: 투표 작성 폼 애니메이션 추가 및 UX 개선, 커스텀 체크박스 생성 Dec 6, 2025
@github-actions
Copy link

github-actions bot commented Dec 6, 2025

@github-actions
Copy link

github-actions bot commented Dec 6, 2025

📦 번들 분석 결과

📊 번들 크기 요약

항목
📦 전체 번들 크기 3.7M
📄 JavaScript 크기 1.6M
🗂️ JavaScript 파일 수 61개

🔍 주요 청크 파일 (크기순)

fe98bb7c-75056deddb8826d9.js - 169K
framework-69e0f7d37422957b.js - 137K
main-448b0e12e0910adc.js - 130K
7147-4b792b1c613a0a9e.js - 122K
1762-0e7232d83dcb3887.js - 121K
polyfills-42372ed130431b0a.js - 110K
2877-2422cd6c39a97d65.js - 90K
6086-65069d4634f32053.js - 76K
page-3026af1cfd56a766.js - 31K
2906-7f73a91282e5669e.js - 28K

🤖 자동 생성된 번들 분석 리포트

@github-actions
Copy link

github-actions bot commented Dec 6, 2025

⚡ Lighthouse 성능 분석 결과

📊 전체 평균 점수

지표 점수
🚀 Performance 74점
♿ Accessibility 86점
✅ Best Practices 100점
🔍 SEO 100점

📈 측정 현황

  • 측정 성공: 15/16 페이지
  • 상태: success

📄 페이지별 상세 분석

🏠 커뮤니티 페이지: /main/community

지표 점수
🚀 Performance 69점
♿ Accessibility 78점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

👥 창업자 페이지: /main/founder

지표 점수
🚀 Performance 75점
♿ Accessibility 87점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🏡 홈 페이지: /main/home

지표 점수
🚀 Performance 75점
♿ Accessibility 91점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🗺️ 지도 페이지: /main/maps

지표 점수
🚀 Performance 75점
♿ Accessibility 87점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

👤 프로필 페이지: /main/profile

지표 점수
🚀 Performance 75점
♿ Accessibility 88점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🔗 전체 상세 분석 결과

📊 전체 상세 Lighthouse 분석 결과 보기

📄 측정된 페이지

  • /main/community
  • /main/founder
  • /main/home
  • /main/maps
  • /main/profile

모든 페이지에서 성능 측정이 완료되었습니다.


🤖 자동 생성된 Lighthouse 성능 리포트

@github-actions
Copy link

github-actions bot commented Dec 7, 2025

Copy link
Member

@DreamPaste DreamPaste left a comment

Choose a reason for hiding this comment

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

고생하셨어요~
화려한 투표 컴포넌트군요

Comment on lines +60 to +72
<AnimatePresence initial={false}>
{canRemove && (
<motion.button
key="remove"
type="button"
onClick={onRemove}
aria-label={`옵션 ${index + 1} 삭제`}
className="text-xs text-neutral-400"
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 8 }}
transition={{ duration: 0.15 }}
>
Copy link
Member

Choose a reason for hiding this comment

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

접근성과 상세한 애니메이션 설정이 대단하군요

Comment on lines 264 to 268
onClick={() => {
const current = getValues('voteOptions') ?? [];
if (current.length >= MAX_OPTIONS) return;
append({ content: '' });
}}
Copy link
Member

Choose a reason for hiding this comment

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

복잡한 클릭 이벤트 핸들러는 분리하는게 좋아보여요

Copy link
Member

Choose a reason for hiding this comment

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

또, useCallback의 필요성도 확인해주세요

Copy link
Member Author

@youdaeng2 youdaeng2 Dec 10, 2025

Choose a reason for hiding this comment

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

리뷰 감사합니다.
주신 피드백 반영하여 인라인으로 있던 핸들러는 분리하였고,
useCallback에 대해서도 검토해보았습니다!
useCallback은

  1. 메모된 하위 컴포넌트에 props로 넘겨 렌더를 줄이고 싶을 때
  2. 의존성 배열에 들어가는 함수를 안정화해야 할 때
    주로 사용하는데 현재는 해당하는 부분이 없어 제거하였습니다.

Comment on lines 302 to 305
onRemove={() => {
const current = getValues('voteOptions') ?? [];
if (current.length <= MIN_OPTIONS) return;
remove(index);
Copy link
Member

Choose a reason for hiding this comment

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

여기도 동일하게 분리하는게 좋아보여요

@github-actions
Copy link

@youdaeng2 youdaeng2 merged commit f65f867 into dev Dec 10, 2025
4 checks passed
@youdaeng2 youdaeng2 deleted the feat/SOS-49-vote-create-page-animation branch December 10, 2025 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feat 💡 새로운 기능을 구현하고 추가합니다! 유진

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants