사진과 가구 정보를 조합하는 컴포넌트 구현하기
@styles/commonStyles
에 재활용 가능하도록 저장이번에 처음으로 타입스크립트와 리덕스를 적용해 과제를 진행했다.
기존에 JS에서도 타입을 보완하기 위해 propTypes
과 defaultProps
를 항상 넣어주긴 했지만, 컴포넌트 외에도 함수나 다른 모듈에서도 타입을 명확히 할 필요가 있었다.
여태까지 계속 새로운 스킬을 배우면서 바로 프로젝트에 진행해야 했기 때문에 상대적으로 러닝커브가 높은 타입스크립트는 계속 뒷전으로 미루게 되더라. 이번엔 개인과제 이기도 하고 이제 리액트는 조금 알 것도 같아서 다른 분들의 프로젝트 결과물과 구글링을 통해 열심히 적용했다....! 특히나 잘하는 분들의 프로젝트 코드를 보는 건 정말 많은 도움이 되었다.
이번과제에서 리덕스를 적용할 필요는 없다고 느껴졌지만, 다음 주부터 본격적으로 리덕스를 사용하라는 과제가 나올 것 같아서 미리 경험해볼 생각이었다. 사실 쉽지 않았다 😱. redux, redux-thunk, redux-saga, typesafe-actions, recoil, mobx
... 등 종류가 너무 다양해서 당장 뭐부터 배워야 하는지 헷갈렸다. 리덕스의 기본 개념을 잘 알지못해서 생기는 문제로 간단히 살펴보았다.
요약하면 리덕스는 스토어를 사용해 컴포넌트 외부에 상태를 두고 상태를 업데이트하거나 전달받을 수 있는 도구이다. 미들웨어는 dispatch(action 발생)된 액션을 스토어에 넘기기 전에 어떤 작업을 처리하고 싶을 때 사용한다. 리덕스가 동기적인 흐름이기 때문에 비동기적인 작업을 처리하고 싶을 때 미들웨어를 사용한다. 미들웨어는 대표적으로 redux-thunk
, redux-saga
가 있는데 contextAPI와 문법이 비슷한 redux-thunk
를 사용해보기로 했다.
리덕스를 사용하기 위해 셋업해야될 게 많고 복잡해서 이 프로젝트에 적용하기엔 투머치였다고 느꼈고, context API를 직전에 공부한 덕분에 쉽게 이해할 수 있었다. 폴더도 너무 많고 복잡하기 때문에 다음엔 좀 더 직관적인 mobx를 사용해볼 것이다.
useLocalStorage
훅과 useGetProductList 훅
을 조합해 로직을 작성했다. useEffect(() => {
const listener = (event) => {
const target = event.target.closest('.toggle');
const clickedId = target ? +target.dataset.id : 0;
++countClickRef.current;
if (saveClickedId.current === clickedId && countClickRef.current === 2) {
dispatch(updateActivedId(0));
// 0을 보내면 tooltip이 닫힘
}
if (saveClickedId.current !== clickedId || countClickRef.current === 1) {
dispatch(updateActivedId(clickedId));
}
if (countClickRef.current === 2) {
countClickRef.current = 0;
}
saveClickedId.current = clickedId;
dispatch(updateActivedId(clickedId));
};
// ... 이외 생략
(마치 이런 느낌....)
그러다 전체 컴포넌트를 가운데 정렬하고자 위치를 옮겼는데 그자리에 돋보기 버튼들이 그대로 있었다(?) 😱. Hㅏ....중복으로 컴포넌트를 넣어버린 것이다. document에 적용된 클릭 이벤트가 두 컴포넌트에 들어가면서 두번씩 호출된 걸 모르고 로직 자체 문제인 줄 알고 거진 하루동안 싹 돌면서 수정했는데 맥이 다 풀리더라 ㅋㅋㅋㅋㅋㅋㅋ. 중복된 컴포넌트를 삭제해줌으로써 해결할 수 있었다. 덕분에 위 로직은 아래와 같이 깔끔하게 정리되었다.
useEffect(() => {
const listener = (event) => {
const target = event.target.closest('.toggle');
const clickedId = target ? +target.dataset.id : 0;
const clickedSwipeIndex = target && +target.dataset.swipeIndex;
dispatch(updateDataSet({ clickedId, clickedSwipeIndex }));
};
// ... 이외 생략
};
position은 vertical, horizontal
값을 가진다.// Tooltip 컴포넌트
export type Vertical = 'bottom' | 'top';
export type Horizontal = 'left' | 'right';
export interface PositionType {
veritcal?: Vertical;
horizontal?: Horizontal;
}
const TooltipContain = ({ pointX, pointY }) => {
return (
<Tooltip position={getPositionOfTooltip(
// @NOTE: pointX, poinY 반대로 넣어줘야 함
pointY * rateOfImageDiff + theme.gap.image,
pointX * rateOfImageDiff
)}>
</Tooltip>
)
}
// position 객체를 리턴해주는 함수
export const getPositionOfTooltip = (pointX: number, pointY: number) => {
// @NOTE: 기본값이 left, bottom
// @NOTE: 기준값인 width(height)의 절반보다 point 값이 클 경우 top 혹은 left로 변화
const veritcal: Vertical =
pointY > theme.size.imageViewHeight / 2 ? 'top' : 'bottom';
const horizontal: Horizontal =
pointX > theme.size.imageViewWidth / 2 ? 'right' : 'left';
return { veritcal, horizontal };
};
실 이미지와 렌더링된 이미지 비율 계산하기
데이터의 pointX와 pointY가 x축 y축이 아닌 y축 x축이여서 반대로 넣어줬다. 또한 각 point들은 실제 image 사이즈에 맞게 받아지므로 렌더링된 이미지의 비율로 계산하여 위치를 재조정해줘야 한다. 관련 로직은 useImageRate 훅
에 작성했고 useImageRate는 ImageViewContent 컴포넌트
에서 사용하였다. theme.gap.image
는 받아온 데이터의 point들이 실제 서비스에 사용되는 point들과 11px
차이가 나는 것을 의미한다.
tooltip 위치 표시
left, bottom
형태이다.right
top
, horizontal: right
top
, horizontal: leftuseSwipe 훅
으로 분리해보았다. useSwipe 훅은 ImageViewSwiper 컴포넌트
에서 사용했다. 한 가지 추가적으로 오른쪽으로 스와이프할 때 마지막 컴포넌트가 다 보인다면 딱 거기까지만 스와이프가 되도록 로직을 추가했다.. useEffect(() => {
if (swipeRef.current) {
const overflowedX =
swipeRef.current?.scrollWidth -
theme.size.boxWidth * dataLength -
theme.gap.swiper * 2;
setDragOverflowedX(overflowedX);
}
}, [swipeRef.current]);
만약 기존 translate3d X값에 오른쪽으로 스와이프한 값을 더했을 때 overflowedX 값보다 크다면 위치를 overflowedX 값으로 옮겨준다.
// ... 생략
if (-draggedX <= -boxWidth / 2) {
// 드래그한 값이 아이템의 절반보다 클 때
if (CheckDragOverflowLast(draggedX)) {
// overflowed된 값보다 크다면 overflow된 값만큼만 이동
setPosition(-overflowedX);
} else {
shiftSlide('right');
}
// ... 생략
아쉽게도 데이터가 7개 뿐이라 스와이프하기에 충분히 많지 않아서 아이템 하나가 스와이프 되기 전에 overflowed가 되어버린다 ㅋㅋㅜ. 하지만 잘보면 아이템의 절반보다 크게 스와이프할 경우 옆으로 넘어가는 걸 확인할 수 있다.
// commonStyles
export const alignBackgroundImage = (size: string) => css`
background-position: center;
background-repeat: no-repeat;
background-size: ${size};
`;
// Badge 컴포넌트 Style
export const BadgeInner = styled.div`
position: absolute;
top: 0;
right: 5px;
width: 24px;
height: 28px;
text-align: center;
color: white;
background-image: url(${badge});
${alignBackgroundImage('contain')};
${fontBadge};
`;
써본 적없는 타입스크립트와 리덕스로 짧은 시간에 과제를 해야한다는 건 부담이었지만 막상 닥치면 뭐든 할 수 있다는 것을 배웠다 😇. 그 외에도 최대한 훅으로 만들거나 재사용성을 높이려고 노력한 프로젝트라 의미가 깊다.