웹 프로그램을 하다 보면 , 당근 빼 놓을 수 없는게 자잘한 애니메이션이당.
개발자의 더 큰 재미를 위해서도, 고객의 더 큰 만족을 위해서도...
마치 인생에 재미가 필요하듯, 화면엔 오버스럽지 않은 애니메이션이 필요하당.
이걸 편하게 해주는 모듈이 바로 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 |