사진과 가구 정보를 조합하는 컴포넌트 구현하기
@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: left
useSwipe 훅으로 분리해보았다. 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};
`;
써본 적없는 타입스크립트와 리덕스로 짧은 시간에 과제를 해야한다는 건 부담이었지만 막상 닥치면 뭐든 할 수 있다는 것을 배웠다 😇. 그 외에도 최대한 훅으로 만들거나 재사용성을 높이려고 노력한 프로젝트라 의미가 깊다.