모달 제목 애니메이션 구현하기 - boostcampwm-2022/web33-Mildo GitHub Wiki
- ‘반응형 웹’을 구현하고자 했었기 때문에, 모든 디바이스에서 편하게 서비스를 이용할 수 있어야 했습니다.
- 그러나 데스크탑이 아닌 모바일 화면에서는 모달의 제목이 모바일 화면보다 더 커서 잘리게 되는 현상이 발생합니다.
- 다음 화면은 갤럭시 폴드의 예시입니다. 모달 제목의 글씨 길이가 너무 길어서 화면에 잘리는 현상이 발생하는 것을 알 수 있습니다.
이제 본격적으로 모달창에 애니메이션을 적용하는 과정에 대해 생각해 보아야 했습니다.
- 가장 먼저 어떤 경우에 애니메이션을 주지 않아도 전혀 문제 없이 사용할 수 있고, 어떤 경우에 애니메이션을 주어아 하는 지에 대한 명확한 기준을 세워야 했습니다. 그 기준은 바로 ‘실제 콘텐츠와 모달의 길이를 비교해서 콘텐츠의 길이가 더 길다면 애니메이션을 주자!’ 였습니다.
- 실제 콘텐츠(제목)의 너비 → useRef.current의 clientWidth 속성
- 모달 너비 속성 → window.innerWidth(어차피 애니메이션 구간은 모달 너비와 전체 화면 너비가 일치했을 경우에만 적용되기 때문입니다.)
- 그런 다음에는, 애니메이션의 슬라이딩을 무한 반복처럼 보이게 해야 했습니다. 즉, @keyframes를 적용했을 때 처음(0%, from)과 끝(100%, to) 부분이 서로 맞아야 했습니다. 이를 위해서는 다음과 같이 작업을 해야 했습니다.
- 애니메이션을 주어야 하는 경우에 한해 두 개의 Title 컴포넌트를 넣어주었으며, 이를 Flexbox로 설정
- @keyframes의 0%와 100%의 transform의 x값을 현재 제목의 길이를 기반으로 설정
- gap을 50%로 설정
export const Title = styled.h1`
...
/* width: 100%; */
...
`;
- 정확한 길이를 알아내기 위해 width: 100%를 제거하였습니다.
const titleWidthRef = useRef<HTMLHeadingElement>(null);
...
useEffect(() => {
if (titleWidthRef && titleWidthRef.current) {
setTitleWidth(titleWidthRef.current.clientWidth);
}
}, [firstLevelInfo]);
- 제목의 길이를 구하기 위해 useRef를 사용하였습니다.
- useEffect를 사용해, 모달 제목이 바뀌는 경우에만 useEffect 내부의 함수를 실행시킬 수 있도록 했습니다. 이 때, setTitleWidth()를 통해 상태값을 바꿀 수 있도록 했습니다. setTitleWidth()는 useState()를 사용하여 상태를 바꿀 수 있는 dispatch 함수입니다.
- 여기에 Ref 엘리먼트 내부의 값인 clientWidth를 넣습니다.
const [windowWidth, setWindowWidth] = useState<number>(window.innerWidth);
useEffect(() => {
const checkViewportWidth = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', checkViewportWidth);
// 클린업 함수
return () => {
window.removeEventListener('resize', checkViewportWidth);
};
}, []);
- 모달의 너비는 애니메이션이 존재한다는 전제 하에 화면의 너비가 바뀔 시 따라서 바뀝니다.
- ‘resize’ 이벤트 핸들러를 통해 화면의 너비가 바뀌면 checkViewportWidth 함수를 실행시킬 수 있도록 합니다.
- 컴포넌트가 언마운트되는 시점과 재렌더링이 되는 시점 사이에 이벤트 핸들러를 해제시켜 메모리 누수를 방지합니다.
const [slidable, setSlidable] = useState<boolean>(true);
...
useEffect(() => {
if (titleWidth + 50 > windowWidth) {
setSlidable(true);
return;
}
setSlidable(false);
}, [titleWidth, windowWidth]);
- titleWidth와 windowWidth라는 두 속성을 기반으로 비교해서 slidable의 여부를 설정합니다.
const marquee = (width: number) => keyframes`
0% {
transform: translate(${width}px, 0);
}
100% {
transform: translate(-${width}px, 0);
}
`;
...
<Title ref={titleWidthRef} slide={slidable} textWidth={titleWidth}>
현재
<TitleLocation
populationLevel={firstLevelInfo[1].populationLevel}>
{firstLevelInfo[0]}
</TitleLocation>
{INFO_DETAIL_TITLE[firstLevelInfo[1].populationLevel]}
</Title>
{slidable ? (
<Title
ref={titleWidthRef}
slide={slidable}
textWidth={titleWidth}>
현재
<TitleLocation
populationLevel={firstLevelInfo[1].populationLevel}>
{firstLevelInfo[0]}
</TitleLocation>
{INFO_DETAIL_TITLE[firstLevelInfo[1].populationLevel]}
</Title>
) : (
<></>
)}
...
export const Title = styled.h1<TitleTypes>`
display: block;
text-align: center;
font-size: 1rem;
white-space: nowrap;
will-change: transform;
animation: ${props =>
props.slide
? css`
${marquee(props.textWidth)} 10s linear infinite
`
: ''};
`;
- 가장 복잡한 ‘최종 애니메이션 적용’ 부분입니다. 적용 여부에 따라 Title의 개수를 다르게 둡니다.
- 두 개의 Title 컴포넌트는 현재 gap이 50%인 flexbox에 감싸여져 있습니다.
- 애니메이션의 @keyframes의 처음과 끝은 해당 텍스트의 크기를 기반으로 translate 값이 설정됩니다.
- 결과적으로 처음과 끝이 서로 맞물리는 듯한 애니메이션을 구현할 수 있기 때문에 자연스러운 애니메이션이 만들어질 수 있습니다.
- 결과적으로 다음과 같이 Title과 Gap을 구성할 수 있습니다.
최종적으로 다음과 같은 화면을 구현할 수 있게 됩니다.