ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Next.js 14 / 🔥 Tadak Tadak 🔥] Context API를 사용하여 Tab Navigation을 구현할 수 있을까? (문제 해결 과정 1)
    Next.js 2024. 1. 3. 23:59

     

     

    🍎 목차
    1.  ONE DAY HERO 프로젝트에서의 Tab Navigation 공통 컴포넌트
    2. Tadak Tadak 프로젝트에서의 Context API를 사용한 Tab Navigation
    3. Context API를 사용한 Tab Navigation 시도 
    4. 문제 상황 : Context API로 공유한 값이 공유가 안된다..?
    5. 문제 해결 : 코드 위치를 신경 쓰자! 
    6. 마무리

     

     

    예전에 ONE DAY HERO라는 프로젝트를 진행하면서 Tab Navigation 공통 컴포넌트를 구현한 적이 있었다. 
    그때 시간이 촉박해 여러 방법을 생각할 여유가 없어 자료 조사를 충분히 하지 못하고 구현하게 되어 아쉬움이 남았었다. 
    이번 프로젝트를 진행하면서도 Tab Navigation 이 필요하여 이번에는 좀 다양한 방법으로 구현해 보려 한다. 

     

     

    1. ONE DAY HERO 프로젝트에서의 Tab Navigation 

     

     

    Tabs 컴포넌트가 leftRoute와 rightRoute를 매개변수로 넘겨받고, 넘겨받은 Route 주소를 Link에 연결해 주어 탭을 클릭하게 되면 탭에 연결된 링크로 이동하는 구조였습니다. 

     

    "use client";
    
    import Link from "next/link";
    import { usePathname } from "next/navigation";
    
    type TabRoute = {
      name: string;
      path: string;
    };
    
    interface TabsProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
      leftRoute: TabRoute;
      rightRoute: TabRoute;
    }
    
    const Tabs = ({ leftRoute, rightRoute, className = "" }: TabsProps) => {
      const pathName = usePathname();
    
      const tabContainerDefaultStyle =
        "flex z-50 w-8/12 bg-white justify-center items-center text-base text-center h-12 rounded-full";
    
      const tabDefaultStyle =
        "mx-1 flex h-10 w-24 flex-auto items-center justify-center rounded-full";
    
      return (
        <>
          <div className={`${tabContainerDefaultStyle} ${className}`}>
            <Link
              className={`${tabDefaultStyle} ${
                pathName === leftRoute.path ? "bg-primary " : ""
              }`}
              href={leftRoute.path}
             >
              {leftRoute.name}
            </Link>
            <Link
              className={`${tabDefaultStyle} ${
                pathName === rightRoute.path ? "bg-primary" : ""
              }`}
              href={rightRoute.path}
             >
              {rightRoute.name}
            </Link>
          </div>
        </>
      );
    };
    
    export default Tabs;

     

     

    공통 컴포넌트로 구현했어야 했기에 우선 성능을 고려하지 않고 팀원들이 어떻게 하면 사용하기 편할까를 고민하고 코드를 짰던 기억이 있습니다. 그래서 URL pathname에 의존한 컴포넌트가 만들어지게 되었습니다. 

     

    이때 고민했던 부분이 URL pathname 에 의존하지 않으면서 렌더링 최적화도 할 수 있는 방법이었습니다. 그래서 URL(도메인 주소)은 바꾸지 않고 유지하되, 탭 아래의 컨텐츠만 변경할 수 있는 방법은 없을까? 생각했습니다. 

     

     

     

    2. Tadak Tadak 프로젝트에서의 Tab Navigation

    Tadak Tadak 프로젝트에서는 홈페이지 중앙 위치에 Post 목록이 자리 잡고 있는데요. 

    이 중앙 위치 상단에 Tab Navigation을 달아 추천 포스트, 내가 팔로우 한 사람의 포스트를 탭으로 나눠 보여 주려고 합니다. 

     

    Tab  컴포넌트는 클릭 이벤트가 들어가기 때문에 Client Component로 구현했습니다. 

     

     

    해결해야 하는 상황은 탭 컴포넌트의 Tab State가 Tab 아래의 Content List 컴포넌트에도 영향을 줘야 한다는 겁니다. 그러기 위해서는 Tab State를 크게는 전역적으로, 적어도 Tab 컴포넌트와 Content List 컴포넌트를 포함하는 바로 상위 레벨에서는 공유하고 있어야 합니다. 

     

    그래서 생각한 방법이 상태 관리 라이브러리(ex. Redux 등)의 Store(저장소)와 Context API인데 어째서인지 Next.js 와  Redux와의 조합으로 잘 사용한 사례가 없는 것 같아 Context API를 사용하기로 했습니다. 

     

     

     

    3. Context API를 사용한 Tab Navigation 시도

    export default function Home() {
      return (
        <main className={style.main}>
          <TabProvider>
            <Tab />
            <div style={{ marginTop: '110px' }}>게시글 입력 폼</div>
            <TempPostList />
          </TabProvider>
        </main>
      )
    }

     

     

    TabProvider를 생성하고 Tab 컴포넌트와 Content List 컴포넌트 포함하는 부분을 감싸주었습니다. ( Provider 부분의 최적화는 추후 진행하려고 합니다. )

     

    TabProvider를 통해 State(tab)와 Setter(setTab)를 공유하려고 합니다. 

     

     
    <TabContext.Provider value={{ tab, setTab }}>
    {children}
    </TabContext.Provider>

     

     

    Tab component는 아래와 같이 구현했습니다. 

     

    'use client'
    
    import { useState } from 'react'
    import style from './tab.module.css'
    
    export default function Tab() {
      const [tab, setTab] = useState('rec')
    
      const onClickRec = () => {
        setTab('rec')
      }
    
      const onClickFol = () => {
        setTab('fol')
      }
    
      return (
        <div className={style.homeFixed}>
          <div className={style.homeText}>홈</div>
          <div className={style.homeTab}>
            <div onClick={onClickRec}>
              추천
              <div className={style.tabIndicator} hidden={tab === 'fol'} />
            </div>
            <div onClick={onClickFol}>
              팔로우 중
              <div className={style.tabIndicator} hidden={tab === 'rec'} />
            </div>
          </div>
        </div>
      )
    }

     

     

     

    Tab Indicator(주황색 부분)를 useState의 tab state를 이용해 디자인 적용했습니다. 

     

     

     

     

    앞서 설명했던 것처럼 tab의 상태를  Content List 컴포넌트에서도 불러와야 하기 때문에 const {tab} = useContext(TabContext)를 이용해 불러왔습니다. 

     

     

     

    4. 문제 상황 : Context API로 공유한 값이 공유가 안된다..?

     

    제 생각은 아래의 코드와 같이 불러온 tab의 상태에 따라 보여주는 컨텐츠 목록을 달리 하려고 했습니다. 하지만 컨텐츠 목록이 "rec"으로 고정되어 바뀌지 않았습니다. 

     

    'use client'
    
    import { useContext } from 'react'
    import { TabContext } from './TabProvider'
    
    export default function TempPostList() {
      const { tab } = useContext(TabContext)
    
      console.log('tabState', tab) // "rec"
    
      return (
        <>
          {tab === 'rec' && (
            <>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
              <li>추천 포스트</li>
            </>
          )}
          {tab === 'fol' && (
            <>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
              <li>팔로우 포스트</li>
            </>
          )}
        </>
      )
    }

     

     

     

     

    5. 문제 해결 : 코드 위치를 신경 쓰자! 

    아마 setTab으로 바뀌는 tab 값이 컨텐츠 목록에도 공유되지 않아 기본값인 'rec' 값이 고정되어 있는 것 같았습니다.

     

    코드를 다시 살펴보니 Tab 컴포넌트 내부에서 const [tab, setTab] = useState('rec')를 선언하고 사용하기 때문에 충돌이 나는 것 같았습니다. 

     

    그래서 Tab 컴포넌트 내의 useState 코드를  ➡︎

    const { tab, setTab } = useContext(TabContext)

     

    로 교체하여 tab, setTab 값은 상위 TabProvider에서만 관리하게 했습니다. 

    'use client'
    
    import { createContext, ReactNode, useState } from 'react'
    
    export const TabContext = createContext({
      tab: 'rec',
      setTab: (value: 'rec' | 'fol') => {},
    })
    
    type Props = { children: ReactNode }
    
    export default function TabProvider({ children }: Props) {
      const [tab, setTab] = useState('rec')
    
      return (
        <TabContext.Provider value={{ tab, setTab }}>
          {children}
        </TabContext.Provider>
      )
    }

     

     

     

     

    6.  결과물

    Tab의 상태에 따라 아래의 포스트가 적절히 바뀌는 것을 확인할 수 있습니다! 

     

     

     

    6. 마무리

    다음에는 React-Query를 사용하여 리팩토링 해보려 한다!

    👏😆👏

    반응형

    댓글

Designed by Tistory.