-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FE] feat: NavigationTab 컴포넌트 구현 #1037
base: develop
Are you sure you want to change the base?
Changes from 7 commits
95ab269
eae4a64
b63b771
513334c
604298f
f55421a
9d4f0ea
85c67c2
5afd4ed
521eb6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
import { useLocation, useNavigate } from 'react-router'; | ||
|
||
import * as S from './styles'; | ||
|
||
interface TabInfoItem { | ||
name: string; | ||
path: string; | ||
param: string; | ||
} | ||
|
||
interface NavigationTabProps { | ||
tabInfoList: TabInfoItem[]; | ||
} | ||
|
||
const NavigationTab = ({ tabInfoList }: NavigationTabProps) => { | ||
const activeTab = sessionStorage.getItem('activeTab'); | ||
|
||
const [currentIndex, setCurrentIndex] = useState(Number(activeTab) || 0); | ||
const [currentTabWidth, setCurrentTabWidth] = useState(0); | ||
const [currentTabLeft, setCurrentTabLeft] = useState(0); | ||
const [isTransitionEnabled, setIsTransitionEnabled] = useState(false); | ||
|
||
const location = useLocation(); | ||
const navigate = useNavigate(); | ||
|
||
const currentItemRef = useRef<HTMLUListElement>(null); | ||
|
||
// URL의 쿼리 파라미터 값을 읽어, 탭 인덱스 업데이트 | ||
useEffect(() => { | ||
const queryParams = new URLSearchParams(location.search); | ||
const tabParam = queryParams.get('tab'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useSearchParamAndQuery 훅 이용할 수 있어요 |
||
const tabIndex = tabInfoList.findIndex((item) => item.param === tabParam); | ||
|
||
if (tabIndex >= 0) { | ||
setCurrentIndex(tabIndex); | ||
sessionStorage.setItem('activeTab', String(tabIndex)); | ||
} | ||
}, [location.search]); | ||
|
||
// 탭이 변경될 때마다 현재 탭의 크기와 위치 업데이트 | ||
useEffect(() => { | ||
if (currentItemRef.current) { | ||
const currentTab = currentItemRef.current.children[currentIndex]; | ||
const { width, left } = currentTab.getBoundingClientRect(); | ||
setCurrentTabWidth(width); | ||
setCurrentTabLeft(left); | ||
} | ||
}, [currentIndex]); | ||
|
||
const handleTabClick = (path: string, index: number) => { | ||
setCurrentIndex(index); | ||
setIsTransitionEnabled(true); | ||
navigate(`${path}?tab=${tabInfoList[index].param}`); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 활성화된 탭을 클릭할 경우 불필요한 상태 업데이트가 발생할 것 같아요. index와 currentIndex가 다른 경우에만 업데이트하는 것은 어떨까요? |
||
|
||
return ( | ||
<S.NavContainer> | ||
<S.NavList ref={currentItemRef}> | ||
{tabInfoList.map((item, index) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로필 탭 리뷰에서도 남겼지만, 합성 컴포넌트 방식을 사용해서 map 대신 태그를 통해 어떤 요소들이 렌더링되는지 바로 알 수 있도록 만드는 방법은 어떨까요?? 링크 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 각 탭의 정보를 배열로 담아서 map으로 돌리는 방식이 아닌 명시적으로 하나하나 넣어주는 방식인가요?🤔 return (
<NavContainer>
<NavList>
<NavItem
label="리뷰 링크 관리"
$isSelected={currentTabIndex === index}
onClick={() => navigate('/user/review-link-management')}
/>
<NavItem
label="작성한 리뷰 확인"
$isSelected={currentTabIndex === index}
onClick={() => navigate('/user/written-review-confirm')}
/>
</NavList>
</NavContainer>
); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 맞습니다!! 자식 요소 개수가 적고 정적인 경우에 사용하면 좋을 것 같아요. (옆동네에서 추천받은 방식입니다ㅋㅋ) 지금 리뷰미에서는 배열을 map으로 렌더하는 코드가 대다수지만요. |
||
<S.NavItem key={index} selected={currentIndex === index}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. key에 index를 사용하지 않는게 좋아서 다른 방법으로 key를 구별했으면 좋겠어요 |
||
<button onClick={() => handleTabClick(item.path, index)}>{item.name}</button> | ||
</S.NavItem> | ||
))} | ||
</S.NavList> | ||
<S.CurrentNavBar width={currentTabWidth} left={currentTabLeft} isTransitionEnabled={isTransitionEnabled} /> | ||
</S.NavContainer> | ||
); | ||
}; | ||
|
||
export default NavigationTab; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import styled from '@emotion/styled'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스타일 컴포넌트에 props들 에 모두 $ 표시 없네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가하겠습니다....😅 |
||
|
||
import media from '@/utils/media'; | ||
|
||
export const NavContainer = styled.nav` | ||
position: relative; | ||
display: flex; | ||
width: 100vw; | ||
border-bottom: 0.1rem solid ${({ theme }) => theme.colors.lightGray}; | ||
`; | ||
|
||
export const NavList = styled.ul` | ||
display: flex; | ||
gap: 3rem; | ||
padding: 0 2.5rem; | ||
list-style-type: none; | ||
|
||
${media.xSmall} { | ||
gap: 0; | ||
width: 100%; | ||
padding: 0; | ||
} | ||
`; | ||
|
||
export const NavItem = styled.li<{ selected: boolean }>` | ||
margin-bottom: 1rem; | ||
padding: 0.7rem 1rem; | ||
border-radius: 0.5rem; | ||
|
||
button { | ||
font-weight: ${({ theme }) => theme.fontWeight.semibold}; | ||
color: ${({ theme, selected }) => (selected ? theme.colors.black : theme.colors.disabled)}; | ||
|
||
&:hover { | ||
color: ${({ theme }) => theme.colors.black}; | ||
} | ||
} | ||
|
||
${media.xSmall} { | ||
display: flex; | ||
flex: 1; | ||
justify-content: center; | ||
padding: 0.7rem 0; | ||
} | ||
`; | ||
|
||
export const CurrentNavBar = styled.div<{ width: number; left: number; isTransitionEnabled: boolean }>` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스타일 컴포넌트 Props 가 두 개 이상일 경우 따로 타입으로 빼주세요 |
||
position: absolute; | ||
bottom: 0; | ||
left: ${({ left }) => `${left}px`}; | ||
|
||
width: ${({ width }) => `${width}px`}; | ||
height: 0.3rem; | ||
|
||
background-color: ${({ theme }) => theme.colors.primary}; | ||
border-radius: 0.1rem; | ||
|
||
transition: ${({ isTransitionEnabled }) => (isTransitionEnabled ? 'all 0.2s ease-in-out' : 'none')}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 목록, 모아보기 페이지 하단바 오차는 자식 요소의 left 값을 그대로 사용하고 있어서 생긴 오차같아요. 자식 요소의 left 값에서 부모 요소의 left 값을 빼면 오차없이 제대로 계산이 돼요. const currentTab = currentItemRef.current.children[currentIndex];
const parentLeft = currentItemRef.current.getBoundingClientRect().left;
const currentLeft = currentTab.getBoundingClientRect().left - parentLeft; 추가로! 하단바를 넣은 이유가 애니메이션을 넣고 싶어서..였기 때문에😅 애니메이션이 없어진다면 하단바도 없어지고 버튼의 border-bottom으로 할 것 같아요! |
||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
탭에서 선택 상태를 관리하고, 또 그 관리 방법이 useState인 이유가 있는지 궁금해요.
(아마 애니메이션 추가 + 페이지를 각자 다른 사람이 만들고, 탭 위치에 대해서 제대로 얘기한 적이 없어서 해석에 차이가 나는 것 같아요!)
현재 OptionSwtich의 구현을 보면 현재 선택된 옵션 상태가 컴포넌트 외부(훅)로 분리되어 있고, useState가 아닌 단순 js 변수를 사용해요. 그리고 선택된 요소를 url을 통해 판단하고 있어요.
그리고 이 상태를 이용해 선택 상태를 제어하기 때문에 새로고침이 일어나도 기존 페이지를 불러올 수 있어요.
뒤로가기 동작 또한 방문했던 url 기반이기 때문에 자연스럽게 동작합니다.
url 기반 상태관리와 useState 기반 관리 모두 각자 장단점이 있지만, 지금 리뷰미에서는 url을 적극적으로 이용하고 있고(reviewRequestCode를 url에서 가져와 사용하는 등) 이 방식을 사용해도 중요한 정보가 url에 노출되지 않기 때문에, 또 탭 자체도 url과 연관이 깊기 때문에 url 방식이 더 낫다는 의견입니다~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 디코에서 말을 잘못 전달한 것 같아요..😭 디코에서는 제가
링크 관리 페이지
에 네비게이션 탭을 두고,작성한 리뷰 확인
탭을 클릭하면 해당 페이지 안에서작성한 리뷰 확인
페이지를 불러오는 방식이라고 설명해서 상하 구조로 오해가 생긴 것 같아요. 사실 제가 생각했던 방식은링크 관리 페이지
와작성한 리뷰 확인
페이지를 포함하는 부모 페이지를 만들어서, 네비게이션 탭은 부모 페이지에만 두고 탭을 클릭하면 네비게이션 탭은 그대로 유지되면서, 탭 아래의 컨텐츠가 전환되는 방식을 생각했어요. 그래서 부모 페이지를 만들어 하나의 탭만 넣을지, 두 개의 페이지에 각각 탭을 넣을지? 고민했었어요🤔부모 페이지(/my-page)를 만들어서 리뷰 링크 관리 탭을 클릭하면
/my-page?tab=manage-links
로 이동하는 방식?제가 원했던 탭 방식이 각 탭을 클릭하면 하단바가 애니메이션을 통해 부드럽게 이동하는 것이였어요. 그래서 하단바가 선택된 탭 상태에 따라 width와 left를 바로 계산해서 동적으로 변경해 줘야 하는데 새로고침이나 탭 간 이동 시, 하단바가 깜빡이거나 늦게 나타나는 문제가 있어서 useState와 세션 스토리지를 결합해서 사용했어요..😅