안녕하세요. 여러분의 네비게이션 바는 좋은 구조를 가지고 계신가요? 아니면, '좋은 컴포넌트 구조란 무엇일까? 이게 최선일까?' 를 고민해본 적이 있으신가요. 이런 고민을 해보신 적 있으신 분이시라면, 잘 오셨습니다. 이 글은 여러분을 위한 글입니다.
네비게이션바 같은 경우에는 정말 다양한 페이지에서 사용되는 컴포넌트입니다. (다양한 곳에서 다양한 모습으로 사용되는 공용 컴포넌트) 이런 컴포넌트의 경우, 다양한 곳에서 사용되는 만큼 중요한 컴포넌트이며 그런만큼 좋은 설계가 필요합니다. 언제나 "이게 최선일까?" 고민하게 마련입니다.
오늘은 제 마음을 시원하게 만들어 주었던 네비게이션바 리팩토링 경험을 공유하려고 합니다. 제가 소개해드릴 개선의 결과물은 당연히 정답일 수 없으며, 그저 현시점 저의 실력과 환경의 수준에서 최선이라고 생각되는 내용을 적용해나갔을 뿐입니다. 이 과정 속에서 제가 문제를 인식한 경험과 어떤 기준을 가지고 개선해나갔는지, 그리고 그 결과물은 무엇이고 어떤 효과를 얻을 수 있었는지에 대한 내용을 공유하려고 합니다.
글을 읽고 나면, 여러분들이 얻게 될 것은 다음과 같습니다.
이 글을 효과적으로 읽어나가기 위해 필요한 최소한의 수준입니다.
이 정도면 누구나 이 글을 읽고 유익을 누리거나, 반박을 할 정도의 이해도를 갖추었다고 생각합니다. 아마 컴포넌트를 작성하면서, 도대체 어떻게 작성해야 잘 작성하는거지? 를 고민했지만, 뽀죡한 수를 찾지 못했던 분들이 계시다면, 그 고민이 깊었던 만큼 이 글이 재미있을 것이라고 생각합니다. (그랬으면 좋겠어요,,🫠)
먼저, 기존에 회사에서 사용하던 네비게이션바에는 어떤 문제점이 있었는지 살펴보겠습니다. 사실상 네비게이션 바는 모든 페이지에서 사용되는 컴포넌트입니다. 그렇기 때문에, 가장 최상단의 부모 컴포컴포넌트에 감싸는 방식으로 구현되곤 합니다.
// root page
...
<Navigation>
{children}
</Navigation>
현재 제가 다니고 있는 회사에서도 다음과 같은 방식으로 구현되어있었습니다. 모든 화면에 들어가는 컴포넌트이기 때문에 이렇게 구현될 수 있다고 생각합니다. 그런데 이것이 왜 문제가 된다는 것일까요? 모든 화면에 들어가기는 하지만, 약간씩 다른 모양으로 표현된다는 것이 문제입니다. 예를 들어보겠습니다.
아래의 사진을 보시면, 매 화면마다 네비게이션의 구성이 달라진다는 것을 확인할 수 있습니다.
각각의 이미지는 모두 다른 페이지에서 가져왔습니다. 현재는 4가지 예시만 보여주고 있지만, 사실상 훨씬 다양한 종류의 페이지에서 다양한 모습을 보여주고 있는 것이 Navigation 컴포넌트였습니다.
이런 컴포넌트를 단 하나의 공통된 컴포넌트로 사용하면서, 매번 다른 페이지마다 다른 모습을 보여주기 위해서는 어떻게해야할까요? 여러가지 방법이 있겠지만, 가장 쉽게 떠올릴 수 있는 방식은 바로 if문을 추가하는 것입니다.
if(메인페이지이면) 카테고리 탭을 보여준다.
if(메인페이지이면) AppBar의 왼쪽에 로고 이미지를 보여준다.
if(상세페이지이면) 뒤로가기 버튼을 보여준다.
if(브라우저 히스토리가 없는 상세페이지이면) 뒤로가기 버튼을 메인 페이지로 가도록 동작시킨다.
이런 형식의 if문이 네비게이션 컴포넌트 안에 가득해질 것입니다. 실제로 이 네비게이션에서 표현해주어야 할 영역은 대략 6가지 영역 (상단 왼쪽 영역, 상단 오른쪽 영역, 하단 카테고리 탭 리스트 그리고 사진에는 보여지지 않았지만 하단 네비게이션, 그리고 공유하기 버튼을 눌렀을때 나오는 BottomSheet 컴포넌트, 검색하기 버튼을 눌렀을 때 나와야하는 검색 모달 컴포넌트까지) 으로 표현해줘야하는 부분이 상당히 다양했고, 그 영역안에서도 다양한 요소들로 화면에 표현해주어야했습니다. Navigation 컴포넌트가 굉장히 비대한 상황이죠.
이 모든 것을 적절한 상황에 적절하게 보여주기 위해서 Navigation 컴포넌트에는 대략 50개 정도의 if문이 들어가 있었습니다.
좋습니다. if문이 50개 정도나 들어가있다니요. 이게 뭐가 문제라는 걸까요.
우선 읽기가 어렵습니다. 예를 들어 아래와 같은 함수들이 여러개 있다고 해보겠습니다.
const isSearchPath = (location: Location): boolean => {
if (isNil(location) || isNil(location.pathname)) {
return false;
}
// 다음 페이지들이 아닌 곳들에 검색 아이콘이 뜸
return !(
location.pathname.includes('picks') ||
location.search.includes('search') ||
location.pathname.includes('points') ||
location.pathname.includes('reviews') ||
location.pathname.includes('menu') ||
location.pathname.includes('product') ||
location.pathname.includes('bills') ||
location.pathname.includes('profiles') ||
location.pathname.includes('refunds')
);
};
그리고 이런 함수들이 또 중첩되어서, 특정 아이콘을 보여줄 것인지 아닌지를 결정합니다. 명백히 읽기가 어렵습니다.
또한 이렇게 if문이 중첩되는 코드의 심각한 문제점은 코드를 읽기 위해서 개발자가 한번에 인지해야하는 양이 많아진다는 것입니다. 한번에 인지해야 할 양이 많다는 것은 어딘가 놓칠 확률이 존재한다는 것입니다. 이렇듯 if문이 무수히 중첩되는 코드는 개발자로 하여금 실수를 장려하는 환경입니다.
뿐만 아니라, 특정 페이지에서 다른 조건으로 아이콘을 보여줘야한다던지, 특정 페이지에서 다른 디자인을 넣어주고 싶을 때면, 해당 페이지만을 위한 if문을 추가해주어야합니다. 이 if문을 넣을 위치를 찾는 것 또한 여간 어려운 일이 아니며, 설사 그 위치를 찾아서 넣었다고 할지라도 불안합니다. 이 if문이 다른 페이지에 보여줄 요소에 영향을 미치지는 않을까가 두렵기 때문입니다. (실제로 이런 영향으로 다른 페이지에 문제가 생겨 다시 디버깅을 해야하는 경우도 종종 있었습니다.)
또한 문제가 발생했을 때, if문을 하나씩 추적해가며 문제가 된 영역을 좁혀나가야하는데, 이것이 유지보수를 정말 어렵게 만들었습니다. 우선 이 네비게이션 영역에서 어떤 문제가 발생했다는 이야기를 듣기만 하면, 그때부터 한숨이 나옵니다. 이는 기존에 만들어진 컴포넌트의 구조가 개발자의 자신감을 떨어뜨리는 구조라는 것이며, 코드를 손댈 때 두려움부터 느끼게 만듦으로 인해서, 생산성을 격하게 떨어뜨리는 구조입니다.
이 정도가 되면, 정말 문제입니다. Navigation 컴포넌트는 새단장이 필요합니다.
이제 문제는 파악했고, 그러면 어떤 기준을 가지고 개선해나가면 좋을까요? 제가 가졌던 기준은 다음과 같습니다.
1) 하나의 컴포넌트에서는 하나의 역할만 감당해야 한다.
2) 어떤 요소를 보여줄 것인지는 사용할 쪽에서 결정한다.(if문을 제거한다.)
사실 이 2가지 기준은 객체지향에서 중요하게 생각하는 원칙 중 2가지 원칙을 따른 것입니다. 단일 책임 원칙(Single Responsibility Principle)과 의존성 주입(Dependency Injection)입니다.
제가 제시한 기준을 간단하게 살펴보겠습니다.
이 첫 번째 기준은 단일 책임 원칙에서 왔습니다. 사실 이 단일 책임 원칙은 "하나의 모듈은 하나의 역할만 감당해야한다"라고도 표현할 수 있지만, "하나의 모듈에서 변경이 발생하는 이유는 단 하나이어야한다." 라고도 말할 수 있습니다. (하나의 컴포넌트에서 변경이 발생하는 이유는 단 하나이어야 한다)
이런 기준에서 봤을 때, 기존의 Navigation 컴포넌트는 변경의 대한 다양한 이유를 가지고 있었습니다. 위에서 말씀드렸듯이, 6가지의 영역을 가지고 있었고, 6가지 영역에서 변경이 발생할 때마다 계속 이 컴포넌트를 변경시켜줘야합니다. (상단 왼쪽 영역에 변경이 있어도, Navigation 컴포넌트를 수정해야하고, 상단 오른쪽, 바텀네비게이션에 변경이 생겨도 Navigation 컴포넌트를 수정해야합니다) 명백히 SRP를 어기고 있었습니다.
각각의 영역을 구분하는 범주가 분명히 필요한 상황이며, 그 범주에 따라서 모듈
(컴포넌트)을 분리해주어야 합니다. 그리고 그 결과 각 영역의 변경이 있으면, 그 변경은 해당 모듈 안에서만 발생하도록 해야합니다. 이것이 제가 개선해나갈 첫 번째 기준이었습니다.
의존성 주입은 모듈의 내부에서 의존성을 제거하고, 해당 모듈을 사용하는 쪽에서 의존성을 입력하는 방식으로 이루어집니다. 아직 의존성이라는 단어가 익숙하지 않은 분들을 위해 부연설명을 하자면, 의존성이란 해당 모듈이 사용되기 위해서 '필요한 것'입니다.
예를 들어보겠습니다.
import Logo from "./Logo" // 의존성.
const Navigation => () => {
return
(
<Container>
<Logo>
</Container>
)}
현재 이 Navigation 컴포넌트는 Logo 컴포넌트에 대한 의존성이 있습니다. Navigation 컴포넌트가 동작하기 위해서 Logo 컴포넌트가 필요하다고 명시하고 있기 때문입니다.
그러면 다시, DI라는 것을 구현한 방식, 즉 의존성을 사용하는 쪽에서 입력하는 방식은 어떤 모습일까요?
// Navigation.jsx
interface Props {
children : React.ReactNode;
}
const Navigation => ({ children } : Props) => {
return (
<Container>
{children}
</Container>
)}
// app.jsx
import Navigation from "./Navigation"
import Logo from "./Logo"
const App = () => {
return (
<Navigation>
<Logo>
</Navigation>
)}
무엇이 달라졌나요? Navigation 컴포넌트 안쪽에서 Logo 컴포넌트에 대해 구체적으로 의존하고 있던 의존성이 사라졌습니다. 이제 무엇이 입력될지는 모르겠지만, children이라는 어떤 타입의 컴포넌트가 들어오겠구나 라는 것만 알 수 있습니다.
그리고 Navigation 컴포넌트를 사용하는 쪽에서 Logo 컴포넌트를 넣어주고 있습니다. 사용하는 쪽에서 의존성을 입력하는 방식이지요.
이쯤에서 객체지향을 이해하는데에 도움이 되는 용어 몇 가지만 소개하고 넘어가겠습니다. 위쪽에서 봤던 방식, 그러니까 모듈 내부에서 의존성을 가지고 있는 방식을 우리는 컴파일 타임 의존성 이 있다고 합니다. 코드가 작성된 시점에 어떤 의존성을 가지고 있는지 명시적으로 알려져 있다는 의미입니다.
그리고 DI가 적용된, 사용하는 쪽에서 의존성을 주입하는 방식을 우리는 런타임 의존성 이 있다고 부릅니다. Navigation 컴포넌트 입장에서 코드가 작성된 시점에는 어떤 의존성이 있는지 모릅니다. 오로지 코드가 실행이 되어서, children에 입력된 무언가를 받게 된 그 시점에 무엇이 들어오는지 알 수 있는 것이죠. 런타임에 의존성이 무엇인지 확인할 수 있기 때문에 이것을 런타임 의존성이라고 부릅니다.
이렇게 제가 생각한 2가지 기준과, 그 기준에 대한 객체지향적 배경을 간략히 설명했습니다. 그럼 이제 본격적으로 이 컴포넌트를 어떻게 개선했는지를 살펴보겠습니다.
지금부터 설명드릴 내용은 코드에 대한 설명이 들어가는 부분입니다. 때문에 약간의 집중력이 요구됩니다.
이 첫 번째 기준을 따라서, 저는 먼저 Navigation 컴포넌트 안에서 하나의 역할이라고 말할 수 있는 영역들을 구분했습니다. 그렇게 구분한 영역 및 컴포넌트는 총 6가지였습니다.
1.TopNavigation
2.TopBar
3.NavLinkList
4.leftContent
5.centerContent
6.rightContent
우선 가장 최상단에 해당하는 TopNavigation 영역입니다.
// TopNavigation.tsx
interface Props {
TopBar: React.ReactNode;
NavLinkList?: React.ReactNode;
}
const TopNavigation = ({ TopBar, NavLinkList }: Props) => {
return (
<Wrapper>
{TopBar}
{NavLinkList}
</Wrapper>
);
};
TopNavigation.TopBar = TopBar;
TopNavigation.NavLinkList = NavLinkList;
TopNavigation.Extentions = Extentions; // 모달같이 기존의 네비게이션 기능에서 확장된 용도로 사용되는 컴포넌트들
TopNavigation은 Navigation의 범주에선 가장 부모에 해당합니다. 사진에서 보실 수 있듯이, 이 TopNavigation에서는 2가지 영역을 포함하고 있습니다. TopBar와, NavLinkList입니다. TopNavigation의 역할은 상단 네비게이션에서 보여줄 2가지 영역을 표시하는 것입니다. 해당 영역에 사용할 컴포넌트를 사용하는쪽에서 받을 수 있도록 props로 받아오게 하고 있습니다.
그리고 쉽게 가져다 사용할 수 있도록
NewTopNavigation.TopBar = TopBar;
NewTopNavigation.NavLinkList = NavLinkList;
이렇게 compound component 패턴을 사용하고 있습니다.
다음은 TopBar입니다. 상단 네비게이션에서 항상 보여지는 영역입니다. 이 친구는 또 3가지 영역을 가지고 있습니다. 왼쪽, 중앙, 오른쪽에 보여줄 컴포넌트들입니다. 위에서 소개한 4,5,6번의 영역들입니다. 이 TopBar에서 감당하는 역할은 이 3가지 영역을 화면에 표시하는 것입니다.
interface Props {
leftContent?: React.ReactNode;
centerContent?: React.ReactNode;
rightContent?: React.ReactNode;
}
const TopBar = ({ leftContent, centerContent, rightContent }: Props) => {
return (
<div className="flex justify-between px-5 py-3 gap-4 w-full">
{leftContent}
{centerContent}
<div className="flex gap-4 justify-end">{rightContent}</div>
</div>
);
};
TopBar.BackArrow = BackArrow;
TopBar.CloseButton = CloseButton;
TopBar.Logo = Logo;
TopBar.SearchButton = SearchButton;
...
...
...
여기 TopBar에서도 마찬가지로 사용할 아이콘들을 미리 지정해줍니다.
TopBar.BackArrow = BackArrow;
TopBar.CloseButton = CloseButton;
이렇게하면 매우 쉽게 가져다가 사용할 수 있습니다.
const NavLinkList = () => {
return(
<div>
{linkList.map(link => {
<Link href={link.href}>{link.text}</Link>
})
<div>
)
}
NavLinkList는 특별할 것이 없습니다. 저의 경우 이곳에서의 컴포넌트들은 페이지에 따른 변경사항이 없었습니다. 그래서 고정으로 linklist가 들어가 있습니다.
이제 이렇게 그 각각의 영역들에 대해서 입력을 받을 수 있도록 만들었고, 그 영역에 채워질 컴포넌트를 각각 작성했습니다. 결국 여기서 한 일은 TopNavigation이라는 거대한 컴포넌트안에서 최대한 나눌 수 있는 영역과 역할을 구분해낸 것입니다. 이것이 어떤 효과를 발휘하게 될 것인지는 차차 살펴볼 것입니다.
처음의 문제는 단 하나의 컴포넌트로 거의 20개 이상의 페이지에서 다양한 모습으로 사용되고 있는 것이었습니다. 그러다보니 if문이 한없이 늘어나고, 코드가 지저분해지는 문제점이 있었습니다.
이제는 위에서 적용했듯이 하나의 역할만을 감당하는 여러개의 컴포넌트로 변경되었습니다. 이것을 활용해서 if문이 제거된 컴포넌트를 살펴보겠습니다.
이제 이 네비게이션 컴포넌트를 메인 페이지에 보여준다고 해보겠습니다. 메인 페이지에 보여줘야 할 요소들이 총 4가지가 있는데, 기존에는 if문을 통해서 복잡한 절차를 거쳐서 4가지 요소를 보여줘야했습니다.
하지만 이제 변경된 컴포넌트는 어떤 요소를 보여줄지 메인 페이지에서 결정합니다.
// mainpage/components/NavigationLayout.tsx
import React, { ReactNode, useState } from 'react';
import Navigation from 'components/layout/Navigation';
interface Props {
children?: ReactNode;
}
const NavigationLayout = ({ children }: Props) => {
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
return (
<>
<Navigation.TopNavigation
TopBar={
<Navigation.TopNavigation.TopBar
leftContent={<p className="text-h10">로고 이미지</p>}
rightContent={
<>
<Navigation.TopNavigation.TopBar.SearchButton onClick={() => setIsSearchModalOpen(true)} />
<Navigation.TopNavigation.TopBar.AlarmButton />
</>
}
/>
}
NavLinkList={<Navigation.TopNavigation.NavLinkList />}
/>
<Navigation.TopNavigation.Extentions.SearchModal
isOpen={isSearchModalOpen}
onClose={() => setIsSearchModalOpen(false)}
/>
{children}
<Navigation.BottomNavigation />
</>
);
};
export default NavigationLayout;
이 컴포넌트는 오로지 메인 페이지에서만 사용됩니다. 메인 페이지에서 사용할 컴포넌트를 이렇게 선언을 하고, 메인 페이지에 감싸줍니다.
<NavigationLayout>
{mainpage}
</NavigationLayout>
위에서 선언한 컴포넌트를 간략히 살펴보겠습니다.
TopBar={
<Navigation.TopNavigation.TopBar
leftContent={<p className="text-h10">로고 이미지</p>}
rightContent={
<>
<Navigation.TopNavigation.TopBar.SearchButton onClick={() => setIsSearchModalOpen(true)} />
<Navigation.TopNavigation.TopBar.AlarmButton />
</>
}
/>
}
이곳의 코드를 잘 살펴보시면, TopBar에 들어갈 내용이 MainPage에서 사용할 컴포넌트를 정의하는 곳에서 결정되고 있습니다.
이런 구조에서는 left, right, center 등의 위치에 넣어주고 싶으면 넣고, 안넣고 싶으면 안넣으면 됩니다. 어떤 요소를 넣을지도 사용하는 쪽에서 결정합니다. 지금 TopBar는 left와 right에 보여줘야할 컨텐츠가 있기 때문에 이 2가지 prop만 사용했습니다.
TopBar 컴포넌트 안쪽에서는 무엇이 들어올지 전혀 모르며, 따라서 모든 if문이 TopBar에서 제거된 상태입니다.
<Navigation.TopNavigation
...
NavLinkList={<Navigation.TopNavigation.NavLinkList />}
/>
여기서 사용중인 NavLinkList도 마찬가지입니다. TopNavigation 컴포넌트 안쪽에서는 NavLinkList를 사용할지 사용하지 않을지 모릅니다. 오로지 TopNavigation를 사용하는 쪽에서만 알 수 있습니다.
다름으로는 Extentions입니다.
<Navigation.TopNavigation
TopBar={
<Navigation.TopNavigation.TopBar
...
rightContent={
<>
<Navigation.TopNavigation.TopBar.SearchButton onClick={() => setIsSearchModalOpen(true)} />
...
</>
}
/>
}
/>
<Navigation.TopNavigation.Extentions.SearchModal
isOpen={isSearchModalOpen}
onClose={() => setIsSearchModalOpen(false)}
/>
{children}
<Navigation.BottomNavigation />
</>
TopBar의 오른쪽 영역에 있는 SearchButton을 클릭하면, SearchModal 컴포넌트가 화면에 렌더링됩니다. 이렇게 네비게이션바의 요소와 소통을 해야하지만, 네비게이션과는 상관없는 컴포넌트를 렌더링해야하는 경우에는 Extentions라는 속성을 등록해두고 렌더링하도록 만들어두었습니다. 저희 회사의 경우 검색 모달, 바텀시트 모달과 같은 것들이 있었습니다.
지금까지는 main 페이지에서 사용하기 위해 작성한 Navation컴포넌트를 알아봤습니다. 그럼 또 다른 페이지에서는 어떻게 작성할까요?
// 상품페이지.tsx
import React, { ReactNode } from 'react';
import Navigation from 'components/layout/Navigation';
import { useRouter } from 'next/navigation';
interface IProps {
children?: ReactNode;
}
const NavigationLayout = ({ children }: IProps) => {
const router = useRouter();
return (
<>
<Navigation.TopNavigation
TopBar={
<Navigation.TopNavigation.TopBar
leftContent={<div />}
rightContent={
<>
<Navigation.TopNavigation.TopBar.AlarmButton />
<Navigation.TopNavigation.TopBar.CloseButton onClick={() => router.back()} />
</>
}
/>
}
/>
{children}
</>
);
};
export default NavigationLayout;
이 페이지에서는 화면의 상단 오른쪽에 알람 버튼과 닫기 버튼만 있으면 됩니다. 때문에 사용할 녀석만 이렇게 주입해주면 됩니다. 반복되는 말이지만, 이렇게 주입할 수 있는 방식이 아니었다면, TopNavigation 컴포넌트 안쪽에서
if(상품페이지이면) {
renderAlaram()
renderClose()
}
와 같은 방식으로 작성해주어야 했을 것입니다. 그러면 위에서 언급했던 문제들이 또 다시 발생하게 되겠죠.
다양하고 많은 효과가 있었지만, 가장 큰 효과는 두려움이 사라졌다는 것입니다. 기존에는 네비게이션바와 관련해서 어떤 문제가 발생했다고 하면 먼저 두려움부터 생겼습니다. 그 복잡한 네비게이션 컴포넌트 안쪽을 들여다봐야한다는 두려움, if문의 지옥을 헤쳐나가야하는 두려움, 문제를 고쳤다할지라도 또 어떤 변경의 여파를 다른 곳에서 맞이하게 될지 모른다는 두려움.
하지만, 제가 제시한 방법대로 컴포넌트를 수정한 이후에는 "상품 페이지 네비게이션 동작이 이상해요"라는 이슈제보를 받으면, 무서울게 없습니다. 먼저, 가장 공통된 네비게이션 컴포넌트는 쳐다볼 생각을 하지 않습니다. 오로지 상품 페이지에서 정의한 네비게이션 컴포넌트만 쳐다보면 됩니다. 여기서 어떤 코드의 변경을 했다고 할지라도, 다른 페이지에 미칠 영향 따위는 고민하지 않아도 됩니다. 오직 상품 페이지에서만 변경의 여파가 가두어졌기 때문입니다.
사실 1)번에서 이야기한 것과 비슷한 맥락이지만, 1)번은 감정적인 부분을 언급했다면, 2)번은 실질적인 효용성에 대한 이야기입니다.
기능을 추가하는 것이 쉬워졌습니다. 예를 들어 어떤 아이콘이나, 컴포넌트를 특정 페이지의 네비게이션바에 렌더링해야하는 일이 생겼다면, 필요한 컴포넌트를 만들고, 해당 페이지의 네비게이션바를 정의한 곳에서 수정을 해주면 됩니다. 이것으로 인해서 다른 페이지에 발생할 문제점을 고려할 필요가 없습니다. 제거하는 것도 마찬가지의 상황입니다.
또한 compound component 패턴을 통해서 컴포넌트들을 정의해놓았기 때문에, 네비게이션바를 정의할 때 intellicence가 작동되어 굉장히 쉽게 편하게 작성할 수 있었습니다. 그리고 Navigation 컴포넌트 하나만 import하면 언제든지 각 화면에 필요한 NavigationLayout을 작성할 수 있다는 이점도 있었지요. 때문에 어떤 페이지에서도 쉽게 쉽게 만들어서 사용할 수 있었습니다.
사실 기존의 네비게이션바는 이슈가 발생하기 쉬운 상황이기도 했고, 실제로 그랬습니다. 한 2-3주에 한번씩은 네비게이션과 관련해 이슈가 제보되었던 것 같습니다. 하지만, 이러한 방식으로 네비게이션을 변경한지가 4달이 넘어가는 지금은, 1번 정도 이슈가 제보된 이후로 단 한번도 이슈가 없었습니다. 심지어 그마저도 해결해야하는 범위가 매우 명확했기에, 편안한 마음으로 버그픽스를 할 수 있었습니다.
이렇듯 저는 네비게이션바 컴포넌트를 리팩토링함으로써, 더 이상 이 컴포넌트에 대한 어떠한 부채의식없이 다른 개발을 원활하게 진행할 수 있게 되었습니다. 하지만 여기에는 장점만 있었을까요? 단점은 없었을까요?
물론 문제도 있었습니다.
보셨다시피, 매 페이지를 작성할 때마다 저 네비게이션 컴포넌트를 다시 선언해주어야 합니다. 이 과정에서 불가피하게 반복이 발생합니다. 예를 들어, 저희 회사에는 x버튼만 보여주면 되는 화면이 무수히 많습니다. 이런 네비게이션을 정의하기 위해서 매 페이지마다 다시 작성해준다면, 이것또한 문제입니다.
하지만, 이것도 나름의 해결책이 있습니다. 만약 반복되는 패턴이 발견되었다면 그때는 그 패턴을 또 하나의 template화 된 컴포넌트로 만드는 것입니다. 앞서 말씀드렸듯이 저희 회사에는 x버튼만 보여주는 페이지가 많았는데, 이런 경우에는 아예 CloseRightNavigation이라는 템플릿화된 컴포넌트를 미리 만들어두었습니다. 이렇게요.
import React, { ReactNode } from 'react';
import Navigation from 'components/layout/Navigation';
import { useRouter } from 'next/navigation';
interface IProps {
children?: ReactNode;
}
const CloseRightNavigationLayout = ({ children }: IProps) => {
const router = useRouter();
return (
<>
<Navigation.TopNavigation
TopBar={
<Navigation.TopNavigation.TopBar
leftContent={<div/>}
rightContent={
<>
<Navigation.TopNavigation.TopBar.CloseButton onClick={() => router.back()} />
</>
}
/>
}
/>
{children}
</>
);
};
export default CloseRightNavigationLayout;
미리 정의된 컴포넌트가 있으니, 이렇게 반복되는 형태의 네비게이션을 다시 정의해줄 필요가 없어졌습니다.
오늘 저희는 이렇게 다양한 페이지에서 다양한 모습으로 사용되는 네비게이션 컴포넌트를 리팩토링하는 과정을 살펴보았습니다. 이렇게 다양한 곳과 다양한 모습으로 나타나야하는 컴포넌트들은 언제든지 존재할 수 있습니다. 이럴때 어떠한 모습으로 컴포넌트의 구조를 잡는 것이 좋을지 고민이 생길 수 있습니다. 그럴 때 저의 글이 도움이 될 수 있다면, 저로써는 기쁠 것 같습니다.
다시 한번 제가 적용한 리팩토링의 핵심 포인트를 짚어보자면, 1) 각 컴포넌트가 책임지고 있는 역할의 범주를 잘 정의한다 (SRP를 지킨다) 2) if문을 제거하고 의존성을 주입할 수 있게 만든다 (DI를 적용한다) 입니다. 사실 이러한 내용을 기억하고 있는 것은 많은 경우 컴포넌트의 구조를 설계할 때 도움이 될 내용이라고 생각합니다.
혹시 제가 제시한 방법 외에도 더 좋은 방법이 있다면, 언제든 말씀해주십시요. 저는 들을 준비가 되어있습니다. 왜냐하면 저는 제가 한 방식이 최선이라고 생각하지 않거든요! 이렇게 긴 글을 읽어주셔서 감사합니다! 모두 즐거운 개발하세요🕺🏻
좋은 글 항상 감사드립니다