컴포넌트를 바라보는 시각과 미래지향적인 프론트엔드 아키텍처

Architecture | 2024년 01월 01일

원티드 인사이트팀이 컴포넌트를 어떻게 바라보는 시각과 더불어 프로젝트에 어떠한 아키텍쳐를 적용했는지 소개합니다.

원티드 인사이트팀은 애자일 방법론을 사용하며, 팀 내부에서는 빠르게 마켓 핏을 확인하는 것을 최우선 과제로 삼고 있습니다. 이에 따라 프로젝트의 빠른 변화에 맞춰 아키텍처를 설계할 필요성을 느꼈고, 다음과 같은 과정과 고민을 거쳐 아키텍처를 설계했습니다

컴포넌트에 대한 생각과 모놀리식 아키텍처

컴포넌트 기반 프레임워크로 개발할 때 많은 사람들이 추구하는 핵심 키워드는 "재사용 가능한 독립된 모듈"입니다. 모두가 이 키워드를 지켜 개발해야 한다는 것을 알고 있지만, 실제로 서비스를 만들 때는 잘 지켜지지 않는 경우가 많습니다.

개발을 할 때 다음과 같은 방향으로 접근할 수 있습니다.

스프린트 A에서 우리는 서비스에 들어갈 사이드 네비게이션을 만들어야합니다.

Typescript
const navItems = [
    {label: 'Wanted', to: '/wanted'},
    {label: 'Wanted Gigs', to: '/wanted-gigs},
    {label: 'Wanted Insight', to: '/wanted-insight},
    {label: 'spotlight', to:'/spotlight'},
    ...
]
...

return (
    <SideNavigation navItems={navItems} />
)

코드를 보면 상당히 간단하며 직관적인 코드라고 느낄 수 있습니다.

우리는 컴포넌트를 쉽고 재사용이 가능하도록 만들었습니다. 개발자는 렌더링 하려는 항목을 전달하기만 하면, 네비게이션을 사용할 수 있으며 내부에서 모든 비즈니스 로직을 처리합니다.

개발자는 <SideNavigation /> 컴포넌트가 어떤 일들을 하는지 깊게 신경쓸 필요가 없습니다!

하지만, 익숙한 시나리오로 흘러갑니다, 스프린트 B에서 다음과 같은 기능이 추가되었습니다.

  • 페이지를 새창으로 열 수 있게 해주세요.
  • 아이템을 그룹화 할 수 있게 해주세요.
  • 검색 기능을 넣어주세요.

기능을 만들기 전에 우리는 컴포넌트가 올바른 추상화인지 고민하고, 컴포넌트를 확장할지 다시 구현할지 결정해야 합니다.

처음부터 SideNavigation을 다시 설계하고 구현합니다. 새로운 Props를 전달받을 수 있도록 수정하고, 해당 속성을 확인하는 간단한 조건 뒤에 새 기능을 추가합니다. 어떤 선택을 하시겠습니까? 어떤 선택이든 좋습니다.

그러나 마감 시간이 촉박하거나 빠르게 개발해야 할 때, 우리는 1번을 선택하지 못할 가능성이 큽니다. 2번을 선택하면 우리는 앞서 언급한 컴포넌트의 핵심 키워드인 "재사용 가능한 독립된 모듈"에서 한 발짝 멀어지게 됩니다.

어떤 과정을 거쳐서 "재사용 가능한 독립된 모듈"에서 점점 멀어지는지 함께 살펴보겠습니다.

앞서 들어왔던 요청을 바탕으로 SideNavigation을 수정해보겠습니다.

페이지를 새창으로 열 수 있게 해주세요.

아이템을 그룹화 할 수 있게 해주세요.

검색 기능을 넣어주세요.

SideNavigation 컴포넌트를 구현할 때 labelto 2가지 속성만 받고 있었기 때문에 새로운 기능을 구현하려면 다양한 상태를 구별하기 위해 객체에 몇 가지 속성을 추가해주어야합니다.

SideNavigation-properties
{ id, to, label, size, type, separator, isSelected }

그런 다음 SideNavigation 내부에서 속성을 확인하고 그에 따라 렌더링 하는 로직을 추가해야 합니다.

이런 작은 변화에서부터 SideNavigation 컴포넌트는 수많은 조건에 따라서 렌더링을 해야 하며 속성이 기하급수적으로 늘어버렸습니다.

SideNavigation
const naviItems = [
    {
        label: 'Wanted',
        to: '/wanted',
        icon: 'https://wanted.icon/wanted',
        type: 'group',
        categories: [
            {
                label: 'recruitment',
                to: '/wanted/recruitment',
                type: '_self',
            }
        ]
        ...
    }
    ...
];

...

return (
    <SideNavigation>
        {naviItems.map((item, index) => (
            <NaviItemWrapper>
                {item.type !== 'group'
                    ? <NavItem label={item.label} icon={item.icon} target={item.target} />
                    : (
                        <NavItem label={item.label} icon={item.icon}>
                            <NestedGroup>
                                {item.categories.map((category) => (
                                    <NsetedItem isSelected={router.asPath === category.to} target={category.target}>
                                        <span>{category.label}</span>
                                    </NsetedItem>
                                ))}
                            </NestedGroup>
                        </NavItem>
                    )
                }
            </NaviItemWrapper>
        ))}
    </SideNavigation>
)

현재 코드를 짧게 줄였음에도 불구하고 SideNavtion 내부에서는 또다시 추상화된 NaviItems 와 여러 조건에 따라 렌더링을 하게 됩니다.

시간이 흘러 SideNavigation 에 새로운 기능 요청이 들어왔습니다.

관리자가 drag an drop을 통해서 Navigation에 목차를 수정할 수 있는 기능이 추가됩니다.

해당 기능을 구현하기 위해서 우리는 또다시 새로운 속성을 추가하고 그에 따라 렌더링 하는 로직을 추가해야 합니다.

우리는 위 사례로 성급한 추상화가 빠르게 변화하는 서비스를 만들어갈 때 어떤 결과를 낳는지 총 2번의 시나리오로 알아볼 수 있었습니다.

원래 설계했던 컴포넌트와 다르게 너무 많은 일을 하고, 크기가 비대해지는 현상을 이 글에서는 잘못된 모놀리식 컴포넌트라고 부르겠습니다.

아래에서 잘못된 모놀리식 컴포넌트가 어떻게 만들어지는지 간단하게 짚고 넘어가겠습니다.

잘못된 모놀리식 컴포넌트의 성장

잘못된 모놀리식 컴포넌트는 처음에는 단순하고 재사용 가능한 컴포넌트로 시작하지만, 시간이 지나면서 여러 기능과 요구사항이 추가되면서 점점 더 복잡해집니다. 이는 주로 다음과 같은 이유로 발생합니다:

  1. 빠른 개발 요구: 프로젝트를 진행할 때 우리는 기능을 빠르게 구현해야 한다는 압박이 큽니다. 이로 인해 기존 컴포넌트를 재설계하기보다는 새로운 요구사항을 기존 컴포넌트에 덧붙이는 방식으로 대응하게 됩니다.

  2. 마감 시간의 압박: 마감 시간이 촉박할 경우, 처음부터 다시 설계하고 구현하는 대신 기존 컴포넌트에 새로운 기능을 추가하는 방식이 선호됩니다. 이는 단기적으로는 시간을 절약할 수 있지만, 장기적으로는 컴포넌트의 복잡성을 증가시킵니다.

  3. 점진적인 기능 추가: 처음에는 간단한 기능을 제공하던 컴포넌트가 시간이 지나면서 여러 기능이 추가됩니다. 예를 들어, 사이드 네비게이션 컴포넌트에 페이지를 새 창으로 여는 기능, 아이템을 그룹화하는 기능, 검색 기능 등이 추가되면서 컴포넌트는 점점 복잡해집니다.

잘못된 모놀리식 컴포넌트의 문제점

  1. 복잡성 증가: 컴포넌트가 복잡해지면서 코드의 가독성과 이해도가 떨어집니다. 이는 버그 발생 가능성을 높이고, 새로운 기능 추가나 수정 작업을 어렵게 만듭니다.

  2. 재사용성 감소: 원래 재사용을 염두에 두고 설계된 컴포넌트가 특정 요구사항에 맞춰 점점 더 커스터마이즈되면서, 다른 프로젝트나 상황에서 재사용하기 어려워집니다.

  3. 유지보수 비용 증가: 컴포넌트의 복잡성이 증가할수록 유지보수에 필요한 시간과 비용도 증가합니다. 특히, 새로운 개발자가 프로젝트에 참여할 때, 기존 코드를 이해하고 수정하는 데 많은 시간이 소요됩니다.

  4. 성능 저하: 많은 기능을 포함한 컴포넌트는 불필요한 로직과 조건문을 포함하게 되어 성능 저하를 초래할 수 있습니다.

인사이트팀의 새로운 룰

빠른 추상화와 코드 재사용 금지

개발을 할 때, 우리는 종종 중복 코드를 제거하고 DRY 원칙(반복되는 코드 금지)을 지키기 위해 과도하게 추상화하려는 경향이 있습니다. 중복되는 코드를 하나의 컴포넌트로 추상화하는 것이 좋은 컴포넌트를 만드는 일이라고 생각하기 쉽지만, 성급한 추상화는 오히려 문제를 야기할 수 있습니다.

기획이 변경될 수 있다는 점을 인지하면서도 성급하게 추상화를 진행하면, 잘못된 추상화로 인해 컴포넌트가 복잡해지고 재사용성이 떨어질 수 있습니다. 특히 SideNavigation과 같은 모놀리식 컴포넌트에서는 drag and drop 기능이나 검색 기능을 추가하는 것이 더욱 어려워질 수 있습니다.

모든 일에는 트레이드오프가 있습니다. 우리는 컴포넌트 코드를 리팩토링하거나 추상화하여 사이드이펙트를 발생시키는 위험을 감수하는 대신, 반복되는 코드가 있더라도 새롭게 작성하는 것이 더 낫다고 생각합니다. 잘못된 추상화보다는 추상화 없이 코드를 리팩토링하는 것이 빠르게 변화하는 서비스 환경에 더 유리할 수 있습니다.

이 원칙을 통해 우리는 빠르게 변화하는 서비스 환경에 더 적합한 방식으로 개발을 진행하고자 합니다.

우리가 선택한 Bottom-Up 방식

잘못된 모놀리식 컴포넌트가 만들어지는 문제의 근본 원인은 성급한 추상화입니다.

모놀리식 컴포넌트를 만들지 않는 가장 간단한 방법은 개발 초기 단계에서 추상화를 피하는 것입니다. 우리는 이를 Bottom-Up 방식이라고 부릅니다.

그렇다면 Bottom-Up 방식은 무엇이 다른가요?

SideNavigation을 예로 들어보겠습니다.

초기 개발 접근 방식은 리스트 배열을 받아 반복하고 UI를 형성하여 재사용 가능하고 직관적인 컴포넌트를 만드는 것이었습니다.

Bottom-Up 방식을 사용하면 다음과 같은 순서로 컴포넌트를 만들 수 있습니다:

작은 컴포넌트부터 시작합니다. 예를 들어, 버튼이나 아이콘 같은 단순한 UI 요소를 구현합니다.

Small-Feature
    <section>
        <NaviItem to="/wanted">wanted</NaviItem>
        <LinkItem to="/event">event</LinkItem>
        <Separator />
    </section>

작은 컴포넌트를 사용하여 더 큰 중간 수준의 컴포넌트를 만듭니다. 예를 들어, 리스트 표시 컴포넌트를 만들 때 버튼과 아이콘 컴포넌트를 사용하여 리스트 항목 컴포넌트를 구성합니다.

Middle-Feature
    <NestedGroup>
        <NestedSection title="Wanted">
            <NavItem to="/wanted">Watend</NavItem>
            <NavItem to="/event">Event</NavItem>
            <NavItem to="/community">Community</NavItem>
            <Separator />
            <LinkItem to="/ai-interview" targe="_blank" />
        </NestedSection>
    </NestedGroup>

중간 수준의 컴포넌트를 사용하여 더 크고 상위 수준의 컴포넌트를 만듭니다. 예를 들어, 다양한 컴포넌트를 결합하여 기능의 완전한 레이아웃과 UI를 구성합니다.

HighLevel-Feature
    <SideNavigation>
        <NestedGroup>
            <NestedSection title="Wanted">
                <NavItem to="/wanted>Watend</NavItem>
                <NavItem to="/event>Event</NavItem>
                <NavItem to="/community>Community</NavItem>
                <Separator />
                <LinkItem to="/ai-interview" targe="_blank" />
            </NestedSection>
        </NestedGroup>
        <NaviSection>
            <NaviItem to="/wanted-gigs">
                <WantedGigsIcon />
                <span>Wanted Gigs</span>
            </NaviItem>
            <NaviItem to="/wanted-insight">
                <WantedInsightIcon />
                <span>Wanted Insight</span>
            </NaviItem>
            <LinkItem to="/spotlight" target="_blank">
                <SpotlightIcon />
                <span>Spotlight</span>
            </LinkItem>
        </NaviSection>
    </SideNavigation>

처음 작성된 예제 코드와 비교하면 추상화가 전혀 되어있지않고 반복되는 코드가 있어 지저분해 보일 수 있습니다.

그러나 작은 컴포넌트와 HTML 태그로 구성되어 있어 구조가 명확하고, 보다 유연하기 때문에 문제에 더 빠르게 대응할 수 있습니다.

초기 개발 속도는 느릴 수 있지만, 데이터 의존성이 적기 때문에 Bottom-Up 접근 방식은 Top-Down 방식보다 유연성을 제공하며, 장기적으로는 비용이 적게 들고 컴포넌트가 성장함에 따라 추상화를 고려할 시간을 더 많이 가질 수 있습니다.

이러한 이유로, 인사이트팀은 성급한 추상화를 지양하고 Bottom-Up 방식으로의 개발을 선택하여 적용중에 있습니다.

맺으며

모든걸 제 경험에 빗대어 말할 수 없지만 제 생각에 많은 프로젝트가 이 포스팅에서 설명된 것과 비슷한 구조로 개발을 하고 서비스가 발전한다고 생각해요.

하지만 시간 압박으로 인해 개발자는 코드와 구조를 정리할 기회가 적어지고 프로젝트는 여러 접근 방식이 뒤섞인 구조가 될 수 있습니다, 이 포스팅을 통해서 꼭 추상화많이 답이 아니라는것을 전달하고싶었습니다.

이 글이 여러분의 프로젝트에도 도움이 되기를 바랍니다. 감사합니다.

함께 읽으면 좋은글