본문 바로가기
React

탭 컴포넌트 그리고 useTab 훅을 만들어보다

by LucetTin5 2023. 9. 20.

프로젝트의 요구사항에 따르면 브라우저 탭과 유사하게 동작하는 탭 컴포넌트가 필요했다.

 

요구되었던 내용

  • 상단 nav 영역에서 메뉴를 선택 시 신규 탭으로 열려야 한다
  • 탭은 최대 10 ~ 12개로 제한되어야 한다 -> 추가로 열려야 하는 경우 알림을 띄울 필요는 없다
  • 탭 영역의 홈 아이콘은 하나의 탭으로써, 메인 화면을 보여주는 형태여야 한다
  • 동일한 메뉴를 선택하더라도 서로 다른 탭으로 동작해야 한다
  • 탭의 이동 시 탭에서 작성 중이던 form의 내용은 유지되어야 한다
  • 최종 산출물 기준으로 새로고침과 뒤로가기 등의 액션은 일어나지 않는 것을 전제로 한다

기본적으로 요구되었던 내용은 위와 같았고 논의를 거듭하며 상세 동작을 정하기로 했다.

 

결과적으로 정해진 내용

  • 기존에 사용하던 react-router-dom의 router 컴포넌트를 대체하는 방향으로 간다
  • 모든 페이지를 렌더링하되 현재 작업 중인 탭만 display하도록 한다
  • 모든 페이지를 렌더링하여 display값만을 조절하므로 따로 중앙 상태관리를 통한 작업 정보를 보관하지 않는다
  • 탭을 닫을 때는 크롬과 동일하게 우측 탭을 우선 선택하도록 한다
  • 탭 이동 시 url을 해당 탭에 맞게 변화시키도록 한다

정해진 내용에 따라 기존의 Tab 컴포넌트(Tab 영역을 담당하는)와 Navgiation 컴포넌트, 컨텐츠 영역을 담당하는 컴포넌트에 들어갈 기능을 개발하게 되었다.

 

1. useTab 훅

열려있는 탭(tabs)의 정보와 tab의 생성, 이동, 종료 기능을 담당할 훅을 제작하기로 했다. App 혹은 layout에서 tab의 정보를 가지고 전달하는 방식을 생각하기도 했으나 탭 관련 로직을 몰아두는 게 좋겠다고 판단했다.

- tabs: tab의 아이디와 이름, url을 위한 대분류와 소분류, 선택되었는지 아닌지, 일부 queryString을 가진 탭의 경우 searchParams까지 담도록 tab type을 지정하고 tabs state가 이들을 array로 가지고 있도록 하였다.

- 새 탭 생성: 상단 Nav에서 메뉴를 선택 시, 컨텐츠 영역에서 다른 탭을 호출할 시 새 탭을 여는 기능으로 제작하였다. 새로 탭이 생성될 시 이전 탭이 존재할 경우 해당 탭의 queryString을 해당 탭의 정보에 담고, 새로운 탭을 선택된 탭으로 생성하고 선택값을 true로 바꾸는 형식으로 동작하게 했다.

- 탭 닫기: 탭의 X 아이콘을 누를 시 탭을 닫으며, 남은 탭이 없을 경우(홈 탭만 남는 경우)에는 주소를 /로 바꾸도록 하였다. 닫히고 난 다음 선택된 탭이 있다면 해당 탭의 대분류, 소분류, searchParams를 이용하여 url을 변경하도록 하였다.

- 탭 변경: tabID를 prop으로 받아 다음 탭을 선택하는 기능으로 우선 이전 탭의 queryString 정보를 저장한 뒤 다음 탭으로 선택값을 변경하는 방식으로 동작하게 하였다.

useTab은 위 4가지 필수적인 기능 외에 closingLastTab이라는 state를 추가로 가지게 했다. 해당 상태는 tabs.length가 1이 될 경우(홈 탭만 남을 경우) 마지막 탭을 닫고 있다는 state이다. 이 값은 새로고침 시 url 기준으로 하나의 탭을 새로 생성하는 기능을 위해서 추가하게 되었다. 요구사항 기준으로 새로고침을 고려할 필요는 없었지만 개발 단계에서 필요하다고 판단되어 url 기준의 작업과 url 기준의 새 탭 생성을 넣게 되었다.

const Example = () => {

    useEffect(() => {
        // tab이 홈 탭 하나뿐이면서,
        // 현재 경로가 /가 아니면서,
        // 마지막 탭을 닫는 상태가 아닐 경우
        // 그러면서 동시에 등록되어있는 정상 url이라고 판정된 경우
        if (tabControl.tabs.length === 1 && path !== '/' && !tabControl.closingLastTab) {
            if (validUrl) {
                openTab( ... );
                setClosingLastTab(false);
        	}
        }
    }, [ ... ]);

	return (...)
};

 

2. TabRenderer

  탭렌더러라 이름붙인 이 컴포넌트는 기존의 Routes 컴포넌트를 대신하기 위해 제작되었다. 어려운 기능은 아니었고 단순하게 등록된 page들을 pageMap으로 묶어 가지고 있고, 대분류, 소분류를 prop으로 받아 둘 다 empty string (path === "/")인 경우를 제외하고는 (validUrl인 경우) pageMap[대분류][소분류]를 return하는 형식으로 제작하게 되었다. 이 컴포넌트는 layout 컴포넌트에서 컨텐츠 영역에 들어가는 페이지 컴포넌트를 모두 맵으로 가지고 react-router의 BrowserRouter처럼 해당 컴포넌트를 return하는 것을 의도하여 제작하게 되었다.

 

3. 레이아웃

  Layout을 담당하고 있는 컴포넌트에서는 useTab 훅을 불러 TabContext를 제작하였다. 처음에는 useTab을 각 페이지에서 불러서 사용하면 되지 않을까 싶었지만 useTab의 인스턴스가 공유되어야 정상적으로, 원하는대로 작동한다는 것을 곧 알게 되었다. 따라서 레이아웃 컴포넌트에서 컨텍스트를 제공하는 방식으로 Navigation과 하위 Tab Content가 해당 내용을 사용할 수 있게 하였다. 

 

선택된 탭에 따라 내용을 다르게 렌더링 하는 형태의 일반적인 탭이 아니다보니 방향성을 잡는데 조금 오래 걸렸던 작업이었다. 그렇지만 기획의도에 맞는 결과물을 만들어내는 과정을 오롯이 제대로 경험한 것 같기도 하다. 이 과정에서 팀에 합류하기 이전에 작업되어있던 프로젝트를 보다 면밀히 살필 수 있게 되어 좀 더 팀의 개발에 보탬이 될 수 있겠다는 생각도 든다.