라이브러리에 대한 나의 생각
라이브러리를 사용해서 똑같은 기능을 아주 쉽게 가져다가 쓸 수 있다.
하지만 나는 라이브러리를 사용해서 기능을 붙이는 것에 대해 보수적으로 생각한다.
왜냐하면 사용한 라이브러리는 내가 직접 조작할 수 없고 만들어 놓는 대로. 그대로 사용해야 한다.
나중에 그 라이브러리가 더 이상 업데이트를 하지않거나 업데이트 과정에서 핵심적인 기능이 변경되면 나는 그 라이브러리에 또 맞춰 코드를 수정해야 한다.
즉, 의존성이 생겨버린다.
이러한 이유로 최대한 라이브러리는 안 쓰려고 노력하고, 바닐라로 직접 구현하는 것을 선호한다.
그래서 이번 캐러셀도 직접 구현을 할 생각이다.
설계
우선 평평한 캐러셀 말고 이번엔 좀 독특하게 입체적으로 구현해보고 싶단 생각이 들었다.
각 카드가 원형으로 둘러싸여 있고 클릭하면 빙글빙글 도는 캐러셀을 구현해 보려고 한다.
MBTI는 총 16개가 있고 그 MBTI를 상태로 관리하려고 한다. 그리고 처음 눌렀을 땐 해당 카드가 앞으로 오고 두 번째도 동일한 카드를 클릭한다면 특정한 이벤트가 발생하도록 하려고 한다.
스타일링 단계별로 보기
1. 카드를 만든다.
2. position : absolute;로 하나로 합친다.
3. y축으로 각 카드를 돌려준다.
4. 중심에서 적절히 밀어준다.
5. 이벤트 발생 시 중심을 잡고 돌려준다.
위 5단계가 이번 입체적인 캐러셀의 원리이다.
구체적인 코드
부모컴포넌트, 자식컴포넌트 2개로 나눠서 작업했다.
부모 컴포넌트인 Carousel.tsx는 돌아가는 회전판을 담당하고
자식 컴포넌트인 MbtiCard.tsx는 각 카드를 담당한다.
Carousel의 스타일링은 360도/16개인 22.5 deg를 클릭 이벤트가 발생 시에 돌려주면 되므로
transform: `rotateX(-5deg) translateY(-20px) rotateY(${stackIndex * 22.5}deg)`
그리고
MbtiCard의 스타일링은 3번에서 설명했듯이 y축으로 동일하게 22.5도를 돌려준다.
transform: ${({ id }) => `rotateY(${id * 22.5}deg)`}
4번에서 설명한 중심으로부터 적절히 밀어야 하는데 얼마나 적절히 밀어야 하냐면
고등학교 수학에서 나오는 삼각형의 높이를 구하는 공식을 이용하면 된다.
하지만 나는 좀 여유롭게 간격을 주고 싶어서 카드 사이즈의 3배를 적용시켰더니 딱 좋았다
전체코드
// Carousel.tsx
import { useContext, useState } from 'react';
import MbtiCard, { Card } from '../styled-components/MbtiCard';
import styles from './style.module.scss';
import { MbtiContext } from '../../Context/MbtiContext';
import { useNavigate } from 'react-router-dom';
const Carousel = () => {
const { selectMbti } = useContext(MbtiContext);
const navigate = useNavigate();
const userId = localStorage.getItem('id');
const [mbtiList, setMbtiList] = useState([
{ id: 1, mbti: 'ESTP', isSelected: false },
{ id: 2, mbti: 'ESFP', isSelected: false },
{ id: 3, mbti: 'ENFP', isSelected: false },
{ id: 4, mbti: 'ENTP', isSelected: false },
{ id: 5, mbti: 'ESTJ', isSelected: false },
{ id: 6, mbti: 'ESFJ', isSelected: false },
{ id: 7, mbti: 'ENFJ', isSelected: false },
{ id: 8, mbti: 'ENTJ', isSelected: false },
{ id: 9, mbti: 'ISTJ', isSelected: false },
{ id: 10, mbti: 'ISFJ', isSelected: false },
{ id: 11, mbti: 'INFJ', isSelected: false },
{ id: 12, mbti: 'INTJ', isSelected: false },
{ id: 13, mbti: 'ISTP', isSelected: false },
{ id: 14, mbti: 'ISFP', isSelected: false },
{ id: 15, mbti: 'INFP', isSelected: false },
{ id: 16, mbti: 'INTP', isSelected: false },
]);
const [selectedCard, setSelectedCard] = useState<Card>({
mbti: '',
id: 0,
});
const [stackIndex, setStackIndex] = useState<number>(0);
const onSpinCard = (mbti: string) => {
const matchingCard = mbtiList.find((card) => card.mbti === mbti);
const relativeIndex = calculateRelativeIndex(selectedCard.id - 1, matchingCard.id - 1);
setStackIndex((prevStackIndex) => prevStackIndex + relativeIndex);
return matchingCard;
};
const updateMbtiList = (matchingCard) => {
const updatedMbtiList = mbtiList.map((card) =>
card.id === matchingCard.id ? { ...card, isSelected: true } : { ...card, isSelected: false }
);
setMbtiList(updatedMbtiList);
};
const onClickCard = (mbti: string) => {
if (mbtiList.some((card) => card.isSelected && card.mbti === mbti)) {
// eslint-disable-next-line no-alert
userId ? navigate(`/chat/${userId}`) : alert('로그인 해주세요');
}
// 빙글빙글 담당
const matchingCard = onSpinCard(mbti);
// 로컬 상태 업데이트
updateMbtiList(matchingCard);
// 전역 상태 업데이트
selectMbti(mbti);
setSelectedCard({ mbti, id: matchingCard.id, isSelected: true });
};
const calculateRelativeIndex = (currentIndex: number, matchingIndex: number): number => {
const distance = Math.abs(currentIndex - matchingIndex);
if (distance > 8) {
return matchingIndex > 8 + currentIndex ? 16 - distance : distance - 16;
}
return currentIndex - matchingIndex;
};
return (
<div className={styles.Carousel}>
<div
className={styles.Carousel__box}
style={{
transform: `rotateX(-5deg) translateY(-20px) rotateY(${stackIndex * 22.5}deg)`,
}}
>
{mbtiList &&
mbtiList.map((mbti): JSX.Element => {
return (
<MbtiCard
key={mbti.id}
mbti={mbti.mbti}
background={`var(--color-${mbti.mbti})`}
id={mbti.id}
onClickCard={onClickCard}
isSelected={mbti.isSelected}
/>
);
})}
</div>
</div>
);
};
export default Carousel;
// MbtiCard.tsx
import styled from 'styled-components';
export interface Card {
mbti: string;
id: number;
isSelected?: boolean;
}
interface MbtiCardProps extends Card {
background: string;
onClickCard: (mbti: string) => void;
isSelected: boolean;
}
interface StyledMbtiCardProps {
background: string;
id: any;
isSelected: boolean;
}
const MbtiCard = (props: MbtiCardProps) => {
const { mbti, background, id, onClickCard, isSelected } = props;
return (
<StyledMbtiCard
background={background}
id={id}
onClick={() => onClickCard(mbti)}
isSelected={isSelected}
>
<span>{mbti}</span>
</StyledMbtiCard>
);
};
const StyledMbtiCard = styled.div<StyledMbtiCardProps>`
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: var(--size-card);
height: calc(var(--size-card) * 2 / 3);
color: var(--color-text);
transform: ${({ id }) => `rotateY(${id * 22.5}deg)`} translateZ(calc(var(--size-card) * 3))
${({ isSelected }) => isSelected && 'scale(1.2)'};
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
border-radius: 6px;
transform-origin: center;
background: ${({ background }) => background};
cursor: pointer;
span {
background: ${({ background }) => background};
}
&:hover {
filter: brightness(0.8);
}
`;
export default MbtiCard;
추가설명
const onSpinCard = (mbti: string) => {
const matchingCard = mbtiList.find((card) => card.mbti === mbti);
const relativeIndex = calculateRelativeIndex(selectedCard.id - 1, matchingCard.id - 1);
setStackIndex((prevStackIndex) => prevStackIndex + relativeIndex);
return matchingCard;
};
이 함수는 클릭하면 돌아가게 하는 코드인데 id 16에서 id 1의 카드를 누르면 한 개만 돌면 되는데 판 전체가 돌아가서 UX해치는 이슈가 있어서 새로 추가한 함수이다
가까운 카드는 가까운 방향으로 돌 수 있게 해주는 함수이다.
'토이프로젝트 > ProjectSassy' 카테고리의 다른 글
지수 백오프(Exponential Backoff) 알아보기 (1) | 2023.07.28 |
---|---|
인프런에서의 첫 스터디..! (3) | 2023.05.05 |
리액트 웹소켓(Stomp) (0) | 2023.04.24 |