아래와 같이 TabBar의 인덱스에 따라 해당 인덱스에 해당 하는 내용을 아래 렌더링 해 보여주는것을 탭뷰 라고 부른다.
일반적인 네비게이션 바와 다른점은, 슬라이드를 하면 오른쪽 인덱스로 넘어간다.
react-native
에서 라이브러리를 사용해 구현해본 적이 있는데, webview
에서 직접 구현해보았던 과정을 작성해보겠다.
[네이버 웹툰]
우선 슬라이드 기능 구현을 위해 쓸만한 라이브러리를 찾아보던 중 커스텀이 쉽고, 가장 대중적으로 사용된다는 react-slick
을 사용하기로 했다.
원래 이미지 리스트를 보여주기 위한 라이브러리이지만
슬라이드 시 한 인덱스만 넘기고, 이미지 대신 컴포넌트를 넣으면 탭뷰 아닌가?
라는 생각으로 사용하게 되었다.
내가 설계한 TabView
컴포넌트를 ootdzip
의 메인 페이지를 기준으로 설명하겠다.
react-slick
엔 TabBar
를 지원하지 않기 때문에, 직접 만들어야한다.
TabBar를 클릭 하면 해당 인덱스의 Tab을 보여준다.
각 탭들을 묶어주는 react-slick
를 활용하는 컴포넌트이다. TabBar
의 인덱스에 따라 해당 인덱스의 Tab
을 보여준다.
Tabs을 슬라이드해서 인덱스 변경을 하게 되면 해당 TabBar의 선택된 인덱스를 변경한다.
Tabbar
와 TabView
는 서로 인덱스 변화에 따라 변한다. 이에 나는 공용 인덱스를 만들어 두가지 컴포넌트 모두 사용하면 좋을 것 같아 context
상태 관리를 사용하기로 했다.
Tabview/context.tsx
const TabViewContext = createContext<TabViewContextType | undefined>(undefined);
export const TabViewProvider = ({ children }: ProviderProps) => {
const [index, setIndex] = useState<number>(1);
const contextValue = useMemo(() => ({ index, setIndex }), [index, setIndex]);
return (
<TabViewContext.Provider value={contextValue}>
{children}
</TabViewContext.Provider>
);
};
export const useTabViewContext = () => {
const context = useContext(TabViewContext);
if (context === undefined) {
throw new Error('useTabViewContext must be used within a TabViewProvider');
}
return context;
};
공용 index 사용을 위해 context
를 적용시킨다. 또한 컴포넌트에서의 사용 편의성을 위해 TabBar, Tabs, Tab
을 합성 컴포넌트로 만들어준다.
export default function TabView({ children }: TabViewProps) {
return (
<TabViewProvider>
{children}
</TabViewProvider>
);
}
TabView.TabBar = TabBar;
TabView.Tab = Tab;
TabView.Tabs = Tabs;
tab
: 탭에 들어갈 내용을 담은 문자열 배열className
: css 커스텀을 위한 classNameonChangeState
: 탭이 변경된 뒤 실행될 함수display
: 탭바의 형태는 탭바의 크기만큼 작아지는 형태, 페이지를 꽉 차게 나누는 형태 두가지로 나눠진다. (inline | block)export default function TabBar({
tab,
display,
className,
onChangeState,
}: TabBarProps) {
const { index, setIndex } = useTabViewContext();
//탭 클릭 시 인덱스 변경
const handleTabClick = (currentIndex: number) => {
setIndex(currentIndex);
};
// 탭이 변경된 뒤 함수 실행
useEffect(() => {
onChangeState?.();
}, [index]);
return (
<Layout className={className}>
{tab.map((item, idx) => (
<Tab
display={display}
key={idx}
focus={index - 1 === idx}
onClick={() => handleTabClick(idx + 1)}
>
<Body2 state="emphasis">{item}</Body2>
</Tab>
))}
</Layout>
);
}
children
: Tab 컴포넌트들이다.dots
: Carousel
아래에 표시되는 인덱스 점 렌더링 여부이다. (boolean)export default function Tabs({ children, dots }: TabsProps) {
const { index, setIndex } = useTabViewContext();
//인덱스가 변경될 시 해당 인덱스로 변경
const afterChangeHandler = (currentIndex: number) => {
setIndex(currentIndex + 1);
};
//슬라이더의 ref
const ref = useRef<Slider | null>(null);
//인덱스가 변경될 시 그 인덱스에 해당하는 슬라이드로 이동
useEffect(() => {
const slider = ref.current;
if (slider) {
slider.slickGoTo(index - 1);
}
}, [index]);
return (
<Carousel
slidesToShow={1}
infinite={false}
afterChange={afterChangeHandler}
ParentRef={ref}
dots={dots}
initialSlide={index - 1}
>
{children}
</Carousel>
);
}
<TabView>
<TabView.TabBar
display="inline"
tab={['큐레이팅', '탐색']}
className="tabBar"
onChangeState={onChangeTabBarIndex}
/>
<TabView.Tabs>
<TabView.Tab>
<S.Curation>
<LikeOOTD />
<SameCloth />
</S.Curation>
</TabView.Tab>
<TabView.Tab>
<S.Explore>
<Explore />
</S.Explore>
</TabView.Tab>
</TabView.Tabs>
</TabView>
기존의 탭뷰의 경우 맨 앞, 즉 0번째 인덱스가 기본적으로 선택되고있었다. 하지만 다른 인덱스가 기본으로 선택되어야하는 경우가 생겼다.
큐레이팅
의 경우 유저가 옷을 등록하거나, 다른 유저의 ootd에 좋아요를 남기는
등의 활동이 이루어지고 나서야 의미가 있는 탭이다. 그래서 서비스 초반에는 탐색
탭을 기본 컨텐츠로 설정하기로 했다.
옷장
의 경우 내 옷을 필터링 할 수 있는 조건이 세가지 있다. 카테고리, 색상, 브랜드
인데 기존의 경우에는 어떤 버튼을 눌러도 맨 앞의 탭인 카테고리가 선택되어 필터 모달이 열렸다. 하지만 색상 버튼 클릭 시 색상이 열리는
등 해당 버튼을 클릭하면 해당 인덱스가 활성화 된 상태로 필터 모달이 열리는게 맞다고 생각해 리팩토링을 진행하게 되었다.
최초 인덱스 변경을 위해 context
의 초기 index
상태를 props로 전달해주기로 했다.
Tabview.tsx
export default function TabView({ children, initialIndex }: TabViewProps) {
return (
<TabViewProvider initialIndex={initialIndex}>
<>{children}</>
</TabViewProvider>
);
}
이렇게 initialIndex
를 전달받았다면, 최초 렌더링 시 해당 인덱스로 업데이트 해주었다.
export const TabViewProvider = ({ children, initialIndex }: ProviderProps) => {
const [index, setIndex] = useState<number>(1);
const contextValue = useMemo(() => ({ index, setIndex }), [index, setIndex]);
useEffect(() => {
if (initialIndex) {
setIndex(initialIndex);
}
}, []);
return (
<TabViewContext.Provider value={contextValue}>
{children}
</TabViewContext.Provider>
);
};
⚠오류 발생
해당 페이지로 진입시마다 첫번째 인덱스에서
initialIndex
인덱스로 이동하는 애니메이션이 발생했다. 이미 컴포넌트가 렌더링 된 후useEffect
가 작동해 그런듯 했다.
Tabview/context.tsx
이렇게 initialIndex
를 전달받아, 초기 상태로 지정해주었다. 이렇게 작성하니 처음부터 원하는 인덱스로 설정되다 보니 애니메이션이 발생하지 않았다.
export const TabViewProvider = ({ children, initialIndex }: ProviderProps) => {
const [index, setIndex] = useState<number>(initialIndex ? initialIndex : 1);
const contextValue = useMemo(() => ({ index, setIndex }), [index, setIndex]);
return (
<TabViewContext.Provider value={contextValue}>
{children}
</TabViewContext.Provider>
);
};
오늘은 직접 탭뷰를 구현해보았다. 직접 만들어보니 context 상태관리
에 대해 공부할 수 있었고, 원하는대로 커스텀에 성공하니 뿌듯했다 :) 완성된 탭뷰는 https://apps.apple.com/kr/app/ootdzip/id6499494035 에서 확인할 수 있다!