- 프로젝트: 채식 지도(가명)
- 키워드: snail array, transform translate, algorithm
- 상황
- 리액트에서 무한 영역 UI 만들기(feat. requestAnimationFrame, useRef) 에서 만들었던 피드백 페이지가 있다.
- 새로운 피드백을 작성했을 때, 포스트잇 하나가 추가되어야한다.
- 기존 포스트잇들이 달팽이 모양으로 움직여서 새로운 포스트잇을 위한 공간을 만든다면 재미있는 UI가 될 것이다.
- 해결 과정
- 먼저 한 변의 길이가 n인 달팽이 배열을 만드는 함수를 작성한다.
달팽이 배열 문제는 여러 가지 방법으로 풀 수 있을 것인데, 짧지만 어려운 방법 대신 길지만 직관적인 알고리즘으로 작성했다.
'-1'로 초기화된 배열의 (0, 0)에서 시작하여, for문을 통해 direction(left) 방향으로 계속 이동한다(row와 col을 1씩 더하거나 빼가며). 그러다 다음 이동할 칸이 -1이 아닐 경우(out of range로 인한 undefined이거나 이미 양수로 할당된 칸), direction을 변경한다.
달팽이 배열에서 필요로 하는 return값의 형태는/** * @param n = 3 일 때, * 0 1 2 * 7 8 3 * 6 5 4 * 의 달팽이 배열이 나오고, * @return 값은 [{ row: 0, col: 0 }, { row: 0, col: 1 }, { row: 0, col: 2 }, { row: 1, col: 2 }, ... , { row: 2, col: 2 }] */ export function generateSnailPositionArray(n: number): { row: number; col: number }[] { const snailPositionArray = Array(n ** 2).fill(null); const snailIndexArray = Array(n ** 2).fill(-1); let direction: 'left' | 'right' | 'up' | 'bottom' = 'right'; let row = 0; let col = 0; for (let count = 0; count < snailIndexArray.length; count++) { snailPositionArray[count] = { row, col }; snailIndexArray[row * n + col] = count; switch (direction) { case 'right': if (col + 1 < n && snailIndexArray[row * n + col + 1] === -1) { col += 1; } else { direction = 'bottom'; row += 1; } break; case 'bottom': if (row + 1 < n && snailIndexArray[(row + 1) * n + col] === -1) { row += 1; } else { direction = 'left'; col -= 1; } break; case 'left': if (col - 1 >= 0 && snailIndexArray[row * n + col - 1] === -1) { col -= 1; } else { direction = 'up'; row -= 1; } break; case 'up': if (row - 1 >= 0 && snailIndexArray[(row - 1) * n + col] === -1) { row -= 1; } else { direction = 'right'; col += 1; } break; } } return snailPositionArray; }
arr = [{ row: 0, col: 0 }, { row: 0, col: 1 }, { row: 0, col: 2 }, { row: 1, col: 2 }, ... , { row: 2, col: 2 }]
와 같은 형태이다.
n번째 포스트잇을 arr[n]의 { row, col } 값 만큼 translate 하면 달팽이 모양대로 포스트잇을 나열할 수 있다. - 먼저 함수를 이용하여 달팽이 배열을 생성한다.
이 때, 새로 생긴 피드백이 달팽이 배열의 중심에 있어야 자연스럽기때문에 완성된 배열을 .reverse() 해준다.export const SNAIL_SIDE_LENGTH = 11; export const CARD_WIDTH = 200; export const BOARD_WIDTH = CARD_WIDTH * SNAIL_SIDE_LENGTH; export const snailPositionArray = generateSnailPositionArray(SNAIL_SIDE_LENGTH).reverse();
- 그 후 Card(포스트잇) 컴포넌트를 작성한다.
핵심 로직은 간단하게 구현할 수 있다.... import { CARD_WIDTH, FEEDBACK_COLOR_SET, SNAIL_SIDE_LENGTH, snailPositionArray, } from '.../variables'; interface Props { feedbackList: Feedback[]; } const FeedbackList = ({ feedbackList }: Props): React.ReactElement => { return ( <> {feedbackList.map((feedback, index) => { const isOutOfRange = index >= SNAIL_SIDE_LENGTH ** 2; return ( <Item style={{ zIndex: index === 1 ? Z_INDEX.BASE : 0, transform: isOutOfRange ? `translate(${-1 * CARD_WIDTH}px, 0)` : `translate( ${snailPositionArray[index].col * CARD_WIDTH}px, ${snailPositionArray[index].row * CARD_WIDTH}px )`, }} ... > <div className="card"> <p className="text">{feedback.content}</p> ... </div> </Item> ); })} </> ); }; export default React.memo(FeedbackList); export const Item = styled.div` position: absolute; top: 0; left: 0; width: ${CARD_WIDTH}px; height: ${CARD_WIDTH}px; padding: 12px; transition: transform 1s ease; .card { position: relative; width: 100%; height: 100%; padding: 16px 16px 32px; ... } ... `;
index번째 포스트잇은 (snailPositionArray[index].col * CARD_WIDTH, snailPositionArray[index].row * CARD_WIDTH) 만큼 translate 해준다.const isOutOfRange = index >= SNAIL_SIDE_LENGTH ** 2; return ( <Item style={{ zIndex: index === 1 ? Z_INDEX.BASE : 0, transform: isOutOfRange ? `translate(${-1 * CARD_WIDTH}px, 0)` : `translate( ${snailPositionArray[index].col * CARD_WIDTH}px, ${snailPositionArray[index].row * CARD_WIDTH}px )`, }} ...
새로운 피드백이 생겨 배열이 길어지면(> n^2) 달팽이 배열을 벗어나는(isOutOfRange) index가 발생한다. 이 index들은 'translate(${-1 * CARD_WIDTH}px, 0)'와 'overflow: hidden' 을 통해 사용자에게 보이지 않도록 숨겨준다. - 달팽이 배열 모양으로 움직이는 애니메이션을 쉽게 구현하였다!
- 먼저 한 변의 길이가 n인 달팽이 배열을 만드는 함수를 작성한다.
'프론트엔드 > React' 카테고리의 다른 글
선언형 Portal, Modal 컴포넌트 구현하기 (0) | 2022.02.15 |
---|---|
Toast 컴포넌트 구현하기 (1) | 2022.01.26 |
SVGR id collision issue 해결하기 (0) | 2021.11.30 |
Firebase auth 정보를 redux로 관리하기 (0) | 2021.11.18 |
useSetRecoilState로 렌더링 최적화하기 (0) | 2021.11.15 |