코딩기록

리액트, Next.js) height를 부드럽게 열고 닫는 framer-motion 라이브러리 본문

프론트/리액트

리액트, Next.js) height를 부드럽게 열고 닫는 framer-motion 라이브러리

뽀짝코딩 2025. 6. 26. 20:52
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 구조가 적절!

 

 

 

 

 

 

 

 

 

 

 

참고

쳇지피티

반응형
Comments