[Project] Etsy 프로젝트 컴포넌트 feedback 수정
Project
개인 프로젝트
React 개인 프로젝트 (Typescript)
Main 페이지 PR 피드백 수정
Header, Footer
tsx 파일, ts style 파일, index 파일 3개를 하나의 폴더로 묶어서 개발하는 습관!
styled-compontent는 그 의도에 맞게 nesting을 많이 쓰지 않고,
각각의 컴폰너트가 어떤 스타일인지, 어떤 기능인지를 바로 알 수 있도록 세세하게 component로 선언해준다.
Tab Menu Interaction
이런 식으로 hover가 있는 tab menu 를 만드려고 한다.
먼저, css로만 못할 것 같다고 생각했다.
hover interaction은 css로 가능하지만, 각 목록 간에 넘어다닐 때는 left나 transform 값을 동적으로 변경시켜야할 것 같아서 JS가 있어야 될 것 같다고 생각했다.
-
hover interaction
hover 시 가운데부터 퍼지는 효과
&::after { content: ""; position: absolute; bottom: -2px; left: 50%; transform: translateX(-50%); width: 0; border-bottom: 2px solid gray; transition: all 0.2s ease-in; } &:hover::after { width: 100%; }
absolute 로 position 맞추고, bottom은 상위 컨테이너 border 보고 맞추면 된다.
hover 전에는 width가 0, hover 시 width 100%로 둔다.
transition을 적용하는데
left: 50%; transform: translateX(-50%);
이렇게 가운데로 끌고와줘야 가운데서 펼쳐진다. (아니면 왼쪽에서 오른쪽으로 커짐)
-
목록마다 line 이동
어떻게 구현할지 감이 안왔다.
근데 생각해보니 transform, 혹은 left를 이용해서 각 목록 인덱스마다 위치를 정해주면 될 것 같았다.(width를 이용해서 계산)
export const IndicatorLine = styled.div<{ alreadyIn: boolean; listWidth: number; linePosition: number; }>` transition: all 0.2s ease-in-out; ${({ alreadyIn, listWidth, linePosition }) => css` width: ${alreadyIn ? listWidth : 0}px; margin: 0 36px; position: absolute; bottom: -2px; left: ${linePosition + listWidth / 2}px; transform: translateX(-50%); transition: all 0.2s ease-in-out; border-bottom: 2px solid black; }`} `;
alreadyIn 이것은 목록에 hover되었을 시 토글되는 state이다.
이것을 이용해서 width가 0이 되었다가 특정 값이 되었다가 하면서 transition이 적용될 것이다.
listWidth는 hover된 요소의 width값을 나타낸다. 각 목록마다 width가 같으면 좋겠지만 그렇지 않기 때문에 그 요소의 width값을 전달해주어야 한다.
linePosition은 막대가 시작될 위치를 말한다. n번째 목록에 hover가 되었다면 0번부터 n-1번까지의 총 길이를 계산해서 left 속성에 값으로 넣어주어 hover된 요소 바로 아래에 line이 생기도록 하는 것이다.
<MenuContainer onMouseEnter={() => setAlreadyIn(true)} onMouseLeave={() => setAlreadyIn(false)} > <MenuList> {menu.map((tab, index) => ( <Menu key={tab} onMouseEnter={e => indicatePositionLine(e.currentTarget, index)} > {tab} </Menu> ))} </MenuList> <IndicatorLine alreadyIn={alreadyIn} listWidth={listWidth} linePosition={linePosition} /> </MenuContainer> <BorderLine />
hover 시 interaction 주는 곳이 MenuContainer(상위 컴포넌트)인 이유는 처음에 들어가고 나올 떄만 가운데에서 라인이 펼쳐지는 효과가 있기 때문이다.
목록간 이동할 때에는 left를 기준으로 transition이 생기지 저런 hover 는 딱 한번만 발생해야하기 때문이다.
이제 진짜 목록에 hover가 되면 그 hover된 e.target을 함수에 전달하고, 몇번째인지도 알려준다.
그 함수는 다음과 같이 실행된다.
const [alreadyIn, setAlreadyIn] = useState(false); const [listWidth, setListWidth] = useState(0); const [linePosition, setLinePosition] = useState(0); const indicatePositionLine = ( target: EventTarget & HTMLLIElement, index: number ) => { if (!target.parentNode?.childNodes) return; const startPosition = Array.from( target.parentNode?.childNodes as NodeListOf<HTMLLIElement> ) .slice(0, index) .reduce( (acc: number, tab: HTMLLIElement): number => (acc += tab.clientWidth), 0 ); setLinePosition(startPosition); setListWidth(target.clientWidth); };
함수 결과적으로는 hover된 요소의 width와 요소가 어디부터 시작해야하는 지에 대한 위치(number 값)가 설정된다.
먼저 요소의 width는 e.target의 clientWidth로 구하면 끝이다.
어디서부터 시작할지가 문제인데,
0번째 부터 n-1번째의 요소들의 width값을 다 더해주면 된다.
이 때 NodeList가 필요하기 때문에 target의 parentNode에서 모든 자식 요소를 일단 가지고 온다.
그 자식 (li)들을 가지고 배열로 만들어 주고 (노드 리스트는 유사배열),
몇번까지만 돌릴 것인지 slice 해준다. (이 때 index가 필요!)
그대로 clientWidth들을 축적해서 계산해주면 css에서 left로 들어갈 시작 위치를 알 수 있다.
left: ${linePosition + listWidth / 2}px; transform: translateX(-50%);
그래서 left에 linePosition(startPosition)을 기본 값에다가 listWidth(hover된 요소의 길이)의 반을 주고, 다시 그 요소의 반을 왼쪽으로 가져와야 하기 때문에 transform -50%을 해준다. (가운데서 라인 펼쳐지는 효과)
코드 컨벤션
컴포넌트 폴더 구조
컴포넌트 폴더 구조는 깔끔하게
폴더 안에 tsx 파일이 들어가고, tsx 파일의 style을 지정할 style.ts 파일, 밖에서 해당 컴포넌트를 사용하게 될 때 접근하도록 해주는 index.ts 파일해서 총 3개의 파일로 만드는 습관을 들이는 것이 좋을 것 같다.
styled-components
styled components를 사용할 때에는 깊게 nesting 하지 말고,
요소(컴포넌트) 하나마다 목적을 알려주는, 잘게 쪼개는 것이 좋다.
가독성도 훨씬 좋고, 유지보수할 때 더 좋다.
div로 감싸기만 할 때 네이밍할 것 없으면 보통
AboutContainer -> AbountWrapper -> AboutBox
이 순으로 많이 작성한다.
map
{
[1, 2, 3, 4, 5].map((el) => (
<StarIcon key={el} width={12} height={12} color="black" />
));
}
의미없는 반복을 하려고 할 때 배열을 생성해서 바로 map을 돌리는 것보다 아래와 같이 쓰는 것이 더 좋다.
{
Array.from([1, 2, 3, 4, 5], (el) => (
<StarIcon key={el} width={12} height={12} color="black" />
));
}
// or
Array.from({length:5}, n => ...)
[1, 2, 3, 4, 5] 이 배열을 직접 map을 돌리는 것이 아니라 Array.from으로 shallow copy하여 그 안에서 로직을 처리해주는 것이다.
기타 할일
피드백 대로 코드 수정 o
Main 페이지 UI layout 완성하기 (grid, logo 및 이미지, 배경)
Main 페이지 중복되는 컴포넌트 템플릿화 (근데 이렇게 하는 게 맞는건가)
댓글남기기