0. 개요

이번에 다루는 것은 framer-motion을 활용해서 위와 같은 메뉴바를 구현하는 방법이다.
지금 만들고 있는 내 포트폴리오의 Header에 이를 적용하기 위해서 구현 방법을 공부해봤고
실제로 이를 구현하는데 성공하였고, 나중을 위해서 구현하는 과정을 블로그에 정리해두기로 하였다.
1. 구현 과정
예제 코드와 실행 예시는 다음과 같다.
import {styled} from "styled-components";
import {motion} from "framer-motion";
import {useState} from "react";
interface I_ItemData {
ItemId: string;
ItemNm: string;
}
const Wrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, rgb(9, 132, 227), rgb(116, 185, 255));
`;
const MenuBox = styled.div`
display: flex;
flex-direction: row;
align-items: center;
background-color: white;
border-radius: 15px;
width: 30em;
height: 5em;
`;
const MenuItem = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
width: 7em;
margin: 0px 3px;
`;
const MenuText = styled.div`
font-size: 17px;
font-weight: bold;
`;
const FocusBar = styled(motion.div)`
width: 5em;
height: 0.4em;
background-color: black;
border-radius: 15px;
position: absolute;
bottom: -18px;
`;
export default function App() {
const [isMatch, setMatch] = useState("");
const ItemData: I_ItemData[] = [
{ ItemId: "home", ItemNm: "홈" },
{ ItemId: "series", ItemNm: "시리즈" },
{ ItemId: "movies", ItemNm: "영화" },
{ ItemId: "bests", ItemNm: "인기 컨텐츠" },
];
return (
<Wrapper>
<MenuBox>
{
ItemData.map((data) => {
return (
<MenuItem key={data.ItemId} onClick={() => setMatch(data.ItemId)}>
<MenuText>{data.ItemNm}</MenuText>
{data.ItemId === isMatch ? <FocusBar /> : null}
</MenuItem>
);
})
}
</MenuBox>
</Wrapper>
);
};

각기 다른 ItemId와 ItemNm을 가진 <MenuItem /> 컴포넌트 4개를 랜더링한다.
여기서 <MenuItem /> 컴포넌트는 <FocusBar />라는 하위 컴포넌트를 가지고 있는데
해당 컴포넌트는 사용자의 현재 위치, URL 값을 기억하는 상태 'isMatch'의 값과
ItemId의 값이 일치하는 경우에만 랜더링되는 컴포넌트이다.
즉, <FocusBar />가 랜더링된 위치가 사용자가 현재 있는 위치를 가리킨다는 것이다.
(position: absolute로 설정하여, 요소 흐름에 영향을 받지 않게 하였음)
이제 <FocusBar />가 움직이는 애니메이션을 추가해야 한다.
FocusBar가 생성될 때, 삭제될 때의 애니메이션을 따로 구현할 필요는 없다.
그냥 FocusBar에 'layoutId'를 추가하는 것으로 해결된다.
/** 기존 코드 */
export default function App() {
/** 기존 코드 */
return (
<Wrapper>
<MenuBox>
{
ItemData.map((data) => {
return (
<MenuItem key={data.ItemId} onClick={() => setMatch(data.ItemId)}>
<MenuText>{data.ItemNm}</MenuText>
{
data.ItemId === isMatch ? (
<FocusBar layoutId="FocusBar"/>
): null
}
</MenuItem>
);
})
}
</MenuBox>
</Wrapper>
);
};

<FocusBar />는 동일한 layoutId를 가지고 있기 때문에 해당 컴포넌트가 삭제 및 재생성될 때
애니메이션 효과가 자동으로 설정된 것을 위의 이미지를 통해서 확인할 수 있다.
CodeSandbox 예제 (HTML)
3. Reference
react framer-motion 메뉴바 / velog 해적왕
Layout Animation (framer-motion 노트)
'Frontend > Framer Motion' 카테고리의 다른 글
| [Framer-motion] Drag and Drop (0) | 2025.04.30 |
|---|---|
| Framer Motion 맛보기 (0) | 2025.01.01 |