웹 프로그램을 하다 보면 , 당근 빼 놓을 수 없는게 자잘한 애니메이션이당.
개발자의 더 큰 재미를 위해서도, 고객의 더 큰 만족을 위해서도...
마치 인생에 재미가 필요하듯, 화면엔 오버스럽지 않은 애니메이션이 필요하당.
이걸 편하게 해주는 모듈이 바로 motion이당. 아래 링크에 잠시 방문하고 오장.
여기 설명은 당연 그 뿌리를 위 링크에 두고 있당. 긴 말은 사치당. 달려보장
먼저 vite로 react / javascript 프로젝트를 아무폴더에나 만들었다 치장.
추가 필요 모듈 설치 ( tailwindcss도 설치해서 자주 써 보도록 하장. 정말 좋당)
npm i motion
npm i tailwindcss @tailwindcss/vite
tailwindcss 설정법을 안드로메다에 두고 다니는 사람들이 있어 여기 적는당.
vite.config.js
import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], });
index.css
@import "tailwindcss";
만약 vscode에서 Tailwind CSS IntelliSense 플러그인이 잘 동작하지 않는다면
플러그인을 Disable -> Restart Extensions 클릭 -> Enable 하거낭
vscode를 재 구동 시키도록 하장. (요건 확실히 쪼메 마니 불편한 부분이당.)
본론으로 들어가장. 아래 틀을 눈으로 흡수하여 머리에서 소화시키장.
자주 사용되는 기본 틀
initial={{ rotate: 0, scale: 0, color: "gray" }} animate={{ rotate: 360, scale: 1, color: "white", transition: { duration: 1 }, }} whileHover={{ scale: 1.2 }} whileTap={{ scale: 0.9 }}
소화시킨 내용을 실행되는 모습과 매칭 시키장.(요게 기본적용)
BasicMotion.jsx
// eslint-disable-next-line no-unused-vars import { motion } from "motion/react"; function BasicMotion() { return ( <motion.div className="w-20 h-15 bg-pink-400 rounded-b-lg justify-center text-2xl flex items-center " initial={{ rotate: 0, scale: 0, color: "gray" }} animate={{ rotate: 360, scale: 1, color: "white", transition: { duration: 1 }, }} whileHover={{ scale: 1.2 }} whileTap={{ scale: 0.9 }} onHoverStart={() => { console.log("Hover Started"); }} > 호산나 </motion.div> ); } export default BasicMotion;
아래 소스에선 layout이란 글자가 핵심이당. (일단 담아두장)
ToggleButton.jsx
// eslint-disable-next-line no-unused-vars import { motion } from "motion/react"; import { useState } from "react"; function ToggleButton({ title }) { const [isOn, setIsOn] = useState(true); const toggleSwitch = () => setIsOn(!isOn); return ( <button className="flex p-0 cursor-pointer w-[100px] h-[50px] bg-[#cba0ee] rounded-full" style={{ justifyContent: "flex-" + (isOn ? "start" : "end"), }} onClick={toggleSwitch} > <motion.div className="w-[50px] h-[50px] bg-[#9911ff] rounded-full text-xs flex justify-center items-center text-white font-extrabold" layout transition={{ type: "spring", duration: 0.5, bounce: 0.8, }} > {title} </motion.div> </button> ); } export default ToggleButton;
아래에선 layoutId 와 <AnimatePresence> 가 핵심이당.
TabAni.jsx
// eslint-disable-next-line no-unused-vars import { AnimatePresence, motion } from "motion/react"; import { useState } from "react"; const sample = [ { icon: "🎃", label: "호박" }, { icon: "❤", label: "만남" }, { icon: "📆", label: "달력" }, ]; const [hobak, love, cal] = sample; const tabs = [hobak, love, cal]; function TabAni() { const [selectedTab, setSelectedTab] = useState(tabs[0]); return ( <div className="w-[480px] h-[60vh] max-h-[360px] rounded-3xl bg-violet-400 overflow-hidden flex flex-col shadow-lg shadow-blue-400"> <nav className="bg-[#e15d24] p-0 pt-5 pb-5 rounded rounded-br-none rounded-bl-none border-b-[#e5f6b6] border-b-1"> <ul className="list-none px-2 m-0 flex w-full"> {tabs.map((item) => ( <motion.li key={item.label} initial={false} animate={{ backgroundColor: item === selectedTab ? "#F3EE66FF" : "#B8490D00", }} className="select-none flex justify-between items-center list-none px-10 mx-0 font-bold text-sm rounded-md w-full relative " onClick={() => setSelectedTab(item)} > {`${item.icon} ${item.label}`} {item === selectedTab ? ( <motion.div className="absolute bottom-[-2] l-0 r-0 h-[3px]" layoutId="underline" id="underline" /> ) : null} </motion.li> ))} </ul> </nav> <main className="flex justify-center items-center flex-1"> <AnimatePresence mode="wait"> <motion.div key={selectedTab ? selectedTab.label : "empty"} initial={{ y: 15, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -10, opacity: 0 }} transition={{ duration: 0.3 }} className="text-9xl" > {selectedTab ? selectedTab.icon : "😋"} </motion.div> </AnimatePresence> </main> </div> ); } export default TabAni;
모두 적용하여 보장.
App.jsx
import BasicMotion from "./BasicMotion"; import TabAni from "./TabAni"; import ToggleButton from "./ToggleButton"; function App() { return ( <div className="flex flex-row justify-center items-center mt-5"> <div className="mr-8"> <h1 className="text-3xl bg-slate-600 text-white rounded-2xl px-5 py-5 mt-5 mb-5"> 쭈니 Motion 띠이작 </h1> <div className="flex justify-center items-center mb-5"> <BasicMotion /> </div> <div className="flex flex-col gap-4 flex-1"> <ToggleButton title={"지혀니"} /> <ToggleButton title={"영시니"} /> <ToggleButton title={"세이니"} /> <ToggleButton title={"누구니"} /> </div> </div> <div className="flex justify-center items-center mt-30"> <TabAni /> </div> </div> ); } export default App;
결과를 잘 확인하여... 꼬옥 소스와 연결시켜 이해하장.
제목이 무제라서 무죄일 수 없듯이
의도가 없었다 해서 무죄일 수 Up 뜻이
제 목이 사슴목이니 무죄라 우기는 건 우~간다당
무제로 무죄를 주장하고 싶은
그 마음만은 크게 와 닿는다.
난 특별한 의도가 없었지만
금수저는 거기에 특별히 의도를 넣었다.
내 잘못이다.
https://www.youtube.com/watch?v=9kaCAbIXuyg
React(리액트) 티키타카 34 (@tanstack/react-query) (0) | 2025.04.08 |
---|---|
React(리액트) 티키타카 33 (wx-react-gantt) SVAR Gantt (3) | 2025.03.27 |
React(리액트) 티키타카 31 (Recharts) (0) | 2025.03.25 |
React(리액트) 티키타카 30 (SweetAlert2) (0) | 2025.03.24 |
React(리액트) 티키타카 29 (CkEditor) (0) | 2025.03.22 |