사이드 프로젝트 개발 과정 - (탭뷰 컴포넌트 구현, react-slick)

knh6269·2024년 6월 17일
1

ootd.zip

목록 보기
14/16
post-thumbnail

도입

아래와 같이 TabBar의 인덱스에 따라 해당 인덱스에 해당 하는 내용을 아래 렌더링 해 보여주는것을 탭뷰 라고 부른다.
일반적인 네비게이션 바와 다른점은, 슬라이드를 하면 오른쪽 인덱스로 넘어간다.
react-native에서 라이브러리를 사용해 구현해본 적이 있는데, webview에서 직접 구현해보았던 과정을 작성해보겠다.

[네이버 웹툰]


구현

슬라이드 기능

우선 슬라이드 기능 구현을 위해 쓸만한 라이브러리를 찾아보던 중 커스텀이 쉽고, 가장 대중적으로 사용된다는 react-slick을 사용하기로 했다.

원래 이미지 리스트를 보여주기 위한 라이브러리이지만 슬라이드 시 한 인덱스만 넘기고, 이미지 대신 컴포넌트를 넣으면 탭뷰 아닌가? 라는 생각으로 사용하게 되었다.


컴포넌트 설계

내가 설계한 TabView컴포넌트를 ootdzip메인 페이지를 기준으로 설명하겠다.

TabBar

react-slickTabBar를 지원하지 않기 때문에, 직접 만들어야한다.
TabBar를 클릭 하면 해당 인덱스의 Tab을 보여준다.

Tabs

각 탭들을 묶어주는 react-slick를 활용하는 컴포넌트이다. TabBar의 인덱스에 따라 해당 인덱스의 Tab을 보여준다.
Tabs을 슬라이드해서 인덱스 변경을 하게 되면 해당 TabBar의 선택된 인덱스를 변경한다.


탭뷰 공용 인덱스 구현

TabbarTabView는 서로 인덱스 변화에 따라 변한다. 이에 나는 공용 인덱스를 만들어 두가지 컴포넌트 모두 사용하면 좋을 것 같아 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;
};

TabView 구현

공용 index 사용을 위해 context 를 적용시킨다. 또한 컴포넌트에서의 사용 편의성을 위해 TabBar, Tabs, Tab을 합성 컴포넌트로 만들어준다.

export default function TabView({ children }: TabViewProps) {
  return (
    <TabViewProvider>
      {children}
    </TabViewProvider>
  );
}

TabView.TabBar = TabBar;
TabView.Tab = Tab;
TabView.Tabs = Tabs;

TabBar 구현

props

  • tab: 탭에 들어갈 내용을 담은 문자열 배열
  • className: css 커스텀을 위한 className
  • onChangeState: 탭이 변경된 뒤 실행될 함수
  • 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>  
  );
}

Tabs 구현

props

  • 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에 좋아요를 남기는 등의 활동이 이루어지고 나서야 의미가 있는 탭이다. 그래서 서비스 초반에는 탐색 탭을 기본 컨텐츠로 설정하기로 했다.

필터 모달 컴포넌트를 Open하는 버튼을 기본 인덱스로

옷장의 경우 내 옷을 필터링 할 수 있는 조건이 세가지 있다. 카테고리, 색상, 브랜드인데 기존의 경우에는 어떤 버튼을 눌러도 맨 앞의 탭인 카테고리가 선택되어 필터 모달이 열렸다. 하지만 색상 버튼 클릭 시 색상이 열리는 등 해당 버튼을 클릭하면 해당 인덱스가 활성화 된 상태로 필터 모달이 열리는게 맞다고 생각해 리팩토링을 진행하게 되었다.

구현

최초 인덱스 변경을 위해 context의 초기 index 상태를 props로 전달해주기로 했다.

Tabview.tsx

export default function TabView({ children, initialIndex }: TabViewProps) {
  return (
    <TabViewProvider initialIndex={initialIndex}>
      <>{children}</>
    </TabViewProvider>
  );
}

1차 구현

이렇게 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가 작동해 그런듯 했다.

2차 구현

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 에서 확인할 수 있다!

0개의 댓글