반응형
두가지 페이지에서 효과는 다르지만, 동일한 로직으로 슬라이더를 구현할 일이 생겼다.
원래는 하나의 페이지마다 코드를 작성했지만, 반복되는 영역이므로 이를 Hook으로 처리해 주자.
const useSlider = <T>(
data: T[],
duration?: number,
moveScale?: number,
opacity?: boolean,
type?: 'spring' | 'linear' | 'tween' | 'inertia',
): [index: number, direction: number, increase: () => void, decrease: () => void, animationVariant: Variants]=>{
...
}
나중에 슬라이더의 애니메이션을 커스텀 할수 있도록, duration(지속시간), moveScale(얼마나 움직일 것인지), opacity(투명도변화), type(애니메이션 효과 타입)을 입력받았다.
반환하는 결과는 해당 슬라이더의 index, 애니메이션이 진행될 방향, 증가시키는 함수, 감소함수, 애니메이션 효과 객체이다.
useState, increase, decrease
const [[index, direction], setIndex] = useState([0, 0]);
function increase() {
setIndex((prev) => (prev[0] > data.length - 2 ? [0, +1] : [index + 1, +1]));
}
function decrease() {
setIndex((prev) => (prev[0] < 1 ? [data.length - 1, -1] : [index - 1, -1]));
}
index, direction을 같은 state로 관리해주었다. 이들을 따로 state로 관리하다보니 setState가 비동기적으로 작동해서 애니메이션이 제대로 작동되지 않았기 때문이다.
하나의 state와 콜백함수를 통해 increase와 decrease 함수를 만들어주었다.
애니메이션 객체
const animationVariant = {
initial: (direction: number) => {
return {
x: moveScale ? (direction > 0 ? moveScale : -moveScale) : direction > 0 ? 1000 : -1000,
opacity: opacity && 0,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
};
},
visible: {
x: 0,
opacity: opacity && 1,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
},
exit: (direction: number) => {
return {
opacity: opacity && 0,
x: moveScale ? (direction < 0 ? moveScale : -moveScale) : direction < 0 ? 1000 : -1000,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
};
},
};
애니메이션은 direction에 의해 결정되기때문에 실제 움직임이 있어야 하는 initial과 exit을 함수로 만들어서 객체를 반환하도록 했다.
전체코드
import { Variants } from 'framer-motion';
import { useState } from 'react';
const useSlider = <T>(
data: T[],
duration?: number,
moveScale?: number,
opacity?: boolean,
type?: 'spring' | 'linear' | 'tween' | 'inertia',
): [index: number, direction: number, increase: () => void, decrease: () => void, animationVariant: Variants] => {
const [[index, direction], setIndex] = useState([0, 0]);
function increase() {
setIndex((prev) => (prev[0] > data.length - 2 ? [0, +1] : [index + 1, +1]));
}
function decrease() {
setIndex((prev) => (prev[0] < 1 ? [data.length - 1, -1] : [index - 1, -1]));
}
// 애니메이션 효과 관련 코드 initial은 초기(등장시 초기값), visible은 initial에서 랜더링될시까지 보여줄 애니메이션, exit은 visible에서 컴포넌트가 지워질때 보여줄 애니메이션
const animationVariant = {
initial: (direction: number) => {
return {
x: moveScale ? (direction > 0 ? moveScale : -moveScale) : direction > 0 ? 1000 : -1000,
opacity: opacity && 0,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
};
},
visible: {
x: 0,
opacity: opacity && 1,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
},
exit: (direction: number) => {
return {
opacity: opacity && 0,
x: moveScale ? (direction < 0 ? moveScale : -moveScale) : direction < 0 ? 1000 : -1000,
transition: {
duration: duration ? duration : 0.5,
type: type && type,
},
};
},
};
return [index, direction, increase, decrease, animationVariant as Variants];
};
export default useSlider;
적용 코드
상위컴포넌트
const ProjectSlider = () => {
const [index, direction, increase, decrease, animationVariants] = useSlider<IProjectInner>(
ProjectData,
0.5,
1000,
true,
'spring',
);
return (
<Wrapper>
<Left size={30} onClick={increase} />
<RelativeWrapper>
<Project ProjectData={ProjectData[index]} direction={direction} animationVaraints={animationVariants} />
</RelativeWrapper>
<Right size={30} onClick={decrease} />
</Wrapper>
);
};
하위 컴포넌트
const Project = ({
ProjectData,
direction,
animationVaraints,
}: {
ProjectData: IProjectInner;
direction: number;
animationVaraints: Variants;
}) => {
return (
<AnimatePresence initial={false} custom={direction}>
<ProjectWrapper
variants={animationVaraints}
initial="initial"
animate="visible"
exit="exit"
key={ProjectData.introduce}
custom={direction}
>
...
</ProjectWrapper>
</AnimatePresence>
);
};
반응형