Notice
Recent Posts
Recent Comments
Link
반응형
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
Tags
- 중첩배열평탄화
- 프론트엔드 스쿨
- 우분투 시간 변경
- 단어 제거
- js 문자열을 문자배열로
- 중복 문자열
- 깃 토큰 만료
- 레디스 확인
- 문자열순서바꾸기
- 제로베이스
- lastIndexOf()
- 코딩 어?
- 객체의 밸류값만 찾기
- 시퀄 문법
- ...점점점문법
- 객체의키값만 찾기
- 배엘에서 스왑
- 5.3.8 Modifying Queries
- 중복단어제거
- @Moditying @Query
- 중첩배열
- 스프링 데이타 JPA
- 중복된 단어
- 아래로펼치기
- 재귀스왑
- ubuntu타임존
- 문자열 중복
- sql 문자열 패턴 검색
- indexOf()
- 중복문자제거
Archives
- Today
- Total
코딩기록
리액트, Next.js) height를 부드럽게 열고 닫는 framer-motion 라이브러리 본문
728x90
현재 작업중인 리액트, Next.js, Tailwind.css 프로젝트에서 쿠폰 번호 등록을 모달대신 버튼 클릭으로 아래로 입력창이 열리게 구현했다.
✅ 완성된 결과물
✅ 애니메이션 적용한 코드
[CouponHistory.tsx]
{/* 쿠폰번호등록 */}
<AnimatePresence>
{isOpen && (
<motion.div
key="couponForm"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.4 }}
className="overflow-hidden"
>
<div className="tracking-widest px-4 pb-6 flex flex-col justify-between mt-10 mb-10">
<h3 className="font-medium text-lg mb-2">쿠폰번호등록</h3>
<div className="border-t border-b border-black text-center">
<div className="text-xs mt-2 text-left">
<p>시리얼번호는 영문자+숫자의 조합이며, 총 16자리 입니다.</p>
<p>예)AB12-CD23-EF56-123A</p>
</div>
<div className="p-8">
{[...Array(4)].map((_, idx) => (
<span key={idx}>
<input
type="text"
maxLength={5}
className="border border-peach-300 rounded outline-none w-[100px] py-1 px-2"
/>
{idx < 3 && <span className="mx-2 text-gray-700">-</span>}
</span>
))}
</div>
<div className="text-sm m-6 text-center">
<Link
href=""
className="border border-peach-300 bg-peach-300 text-gray-800 rounded py-4 px-10 "
>
쿠폰등록
</Link>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
🔧 사용방법
✅ 1단계: framer-motion 설치
npm install framer-motion
✅2단계: 코드 적용
<AnimatePresence>
{isOpen && (
<motion.div
key="couponForm"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.4 }}
className="overflow-hidden"
>
...
</motion.div>
)}
</AnimatePresence>
✅핵심 포인트 요약
부분 | 설명 |
AnimatePresence | 조건부 렌더링 시 exit 애니메이션까지 실행되도록 함 |
motion.div | 열고 닫을 영역에 애니메이션 적용 |
initial, animate, exit | 열릴 때와 닫힐 때 상태 정의 |
transition.duration | 애니메이션 속도 조절 (초 단위) |
✅ 언제 motion.div, motion.input을 쓰는가?
요소motion 적용 가능 여부설명
<div> | ✅ motion.div로 바꿔 사용 | |
<input> | ✅ motion.input 가능 (예: 포커스 효과, 등장 애니메이션 등) | |
<Link> | ❌ 직접 불가능 (Next.js 컴포넌트이기 때문, 대신 <motion.a>) |
✅ 예시 1: div를 부드럽게 펼치고 닫기
import { motion } from "framer-motion";
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.4 }}
className="overflow-hidden"
>
<div className="p-4">내용</div>
</motion.div>
✅ 예시 2: motion.input로 등장 애니메이션 적용
<motion.input
type="text"
maxLength={5}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="border border-peach-300 rounded w-[100px] py-1 px-2"
/>
단순히 "입력창이 툭 나타나거나 사라지게" 할 때 유용하다.
⚠️ 주의: motion.a 사용
Next.js의 <Link>는 내부적으로 <a>를 감싸기 때문에
<motion.Link>는 지원되지 않는다. 대신 이렇게 써야 한다.
<Link href="/some-path">
<motion.a
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
className="block"
>
쿠폰등록
</motion.a>
</Link>
✅ 요약
목표 | 사용 방식 |
div, input, button에 애니메이션 | motion.div, motion.input, motion.button |
Next.js Link에 애니메이션 | <Link><motion.a>...</motion.a></Link> |
꼭 애니메이션을 줄 요소만 motion으로 | 불필요한 요소는 motion으로 감싸지 않아도 됨 |
✅ transition은 안되나?
transition으로
height: 0 → height: 500px
처럼 정해진 수치 간 변화는 가능하지만,
height: 0 → height: auto
처럼 자동 크기 계산값으로 전환되는 경우에는 transition이 작동하지 않음.
✅ 그래서 대안은?
방법 1: max-height 트릭 (조건부 class + Tailwind transition 사용) ✅ 가능은 함
tsx
코드 복사
<div className={`transition-all duration-500 overflow-hidden ${ isOpen ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0' }`} > ...내용... </div>
✅ 단점: max-height를 너무 작게 잡으면 콘텐츠가 잘릴 수 있고
너무 크게 잡으면 느려짐. 딱 적당한 max-h 추정치가 필요하다.
방법 2: framer-motion 사용 → height: auto 지원 💯 추천 방식
- 내부 콘텐츠 크기에 딱 맞게 부드럽게 펼쳐지며
- opacity, translateY, scale 등을 함께 조합할 수 있어 훨씬 자연스럽다.
✅ 결론 요약
방법가능 여부설명
Tailwind transition-all + height | ❌ height: auto는 안 됨 | |
max-height + transition | ⭕ 되긴 하지만 제약 많음 | |
framer-motion | ✅ 가장 자연스럽고 유연함 |
여기서 질문❗ 아코디언은 무엇이냐 🤔❓
motion.div로 펼치고 닫는 것과 아코디언(Accordion) 방식은 비슷해 보이지만,
사용 목적과 UX 특징에서 차이점이 있다.
✅ 1. 아코디언(Accordion)이란?
여러 개의 항목 중에서 하나씩만 펼칠 수 있는 UI 구조.
📌 예시:
- 자주 묻는 질문(FAQ)
- 사이드 메뉴 리스트
- 설정 항목
[ + ] 배송 정보
[ - ] 반품/교환 안내
[ + ] 고객센터 연락처
✅ 2. motion.div vs 아코디언 구조 차이
항목 motion.div 사용 예 아코디언 사용 예
목적 | 단일 영역을 부드럽게 열고 닫기 | 여러 항목 중 하나만 열기 |
열릴 수 있는 개수 | 보통 1개 (혹은 자유롭게 여러 개) | 1개만 열리는 게 기본 |
상태 관리 | useState<boolean> 하나 | 항목 개수만큼 index 또는 activeId 관리 |
UI 예시 | 쿠폰번호 입력, 검색창 토글 | FAQ 리스트, 드롭다운 메뉴 |
✅ 예시 비교
✅ motion.div 예 (단일 영역 토글)
const [isOpen, setIsOpen] = useState(false);
{isOpen && (
<motion.div animate={{ height: "auto" }}>내용</motion.div>
)}
✅ 아코디언 예 (리스트 중 하나만 열림)
const [activeIndex, setActiveIndex] = useState<number | null>(null);
{faqList.map((item, i) => (
<div key={i}>
<button onClick={() => setActiveIndex(i === activeIndex ? null : i)}>
{item.question}
</button>
<AnimatePresence>
{i === activeIndex && (
<motion.div animate={{ height: "auto" }}>{item.answer}</motion.div>
)}
</AnimatePresence>
</div>
))}
✅ 결론 요약
질문 답변
motion.div와 아코디언은 같은 기능인가요? | ❌ 유사하지만 목적이 다릅니다 |
motion.div는 어떤 상황에 쓰나요? | 단일 컴포넌트의 열기/닫기 토글 |
아코디언은 어떤 상황에 쓰나요? | 여러 항목 중 하나만 펼치고 나머지는 닫는 구조 |
💡 "쿠폰번호등록"은
👉 motion.div + 단일 toggle 구조가 적절!
참고
쳇지피티
반응형
'프론트 > 리액트' 카테고리의 다른 글
TDZ) ReferenceError: Cannot access 'bucket' before initialization (0) | 2025.07.09 |
---|---|
텍스트 커서-깜빡이는 막대기 " | " 없애기 (0) | 2025.06.26 |
색상에 따라 텍스트 배경 바꾸기 (0) | 2025.06.20 |
Next.js) 폴더명에 []대괄호 사용- 동적라우팅 (1) | 2025.06.20 |
Carousel) Swiper(스와이퍼)- 반응형 슬라이드(캐러셀) 라이브러리 (0) | 2025.06.18 |
Comments