본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 24. 14:05

【아키텍처】 대량의 유사한 Feature를 가진 Enterprise 프론트엔드 설계

요약

React 프론트엔드 개발 시 유사한 화면이 대량으로 늘어날 때 발생하는 코드 중복 문제를 해결하기 위한 'Feature Registry 패턴'을 소개합니다. 기존의 FSD, Container/Presentation, Hooks 패턴의 한계를 분석하고 효율적인 설계 방식을 제안합니다.

핵심 포인트

  • 유사한 피처가 반복될 때 발생하는 코드 중복 문제 지적
  • FSD, Container/Presentation, Hooks 패턴의 특징과 한계 분석
  • 대량의 유사 피처 관리를 위한 Feature Registry 패턴 도입 필요성

서론

React 프론트엔드 개발에서 유사한 화면이 늘어나면 반드시 부딪히는 문제가 있습니다. "거의 동일한 코드가 여러 곳에 존재한다", "새로운 화면을 추가할 때마다 같은 작업이 반복된다" — 이 문제에 대해 공통 컴포넌트나 기존의 디자인 패턴만으로는 완전히 해결할 수 없는 경우가 있습니다.

이 기사에서는 그 문제를 해결하는 『Feature Registry 패턴』을 소개합니다.

구체적인 구현 사례를 곁들여 이 패턴의 사고방식과 장점, 그리고 AI 시대의 프론트엔드 설계와의 관계까지 깊이 있게 다룹니다.

현대 프론트엔드 설계의 주류

React를 사용한 프론트엔드 개발에서는 몇 가지 설계 패턴이 널리 사용되고 있습니다.

일본어로 정리된 리소스로서 프론트엔드 디자인 패턴이 있습니다. 이 기사에는 대표적인 패턴이 망라되어 있어 참고하고 계신 분들도 많을 것입니다.

실무에서 특히 자주 사용되는 것은 다음의 3가지입니다.

Feature-Sliced Design

기능 단위로 코드를 닫는 사고방식입니다. features/ 하위에 도메인별 디렉토리를 만들고, 해당 피처(Feature)에 관한 모든 것을 그곳에 수용합니다.

features/
User/
Order/
...

피처를 가로지르는 의존성을 줄이고, 변경의 영향 범위를 국소화할 수 있습니다.

Container / Presentation 패턴

로직과 UI를 분리하는 패턴입니다. container.tsx가 데이터 취득 및 상태 관리(State Management)를 담당하고, presentation.tsx가 이를 받아 렌더링합니다.

Feature/
container.tsx # 로직
presentation.tsx # UI

책임이 명확해지는 반면, Hooks의 등장으로 인해 그 역할이 점차 옅어지고 있습니다.

Hooks 패턴

container.tsx가 담당하던 로직을 커스텀 Hooks로 분리하는 패턴입니다. 현대의 React 개발에서 가장 주류인 사고방식입니다.

Feature/
index.tsx # hooks를 호출하여 컴포넌트에 전달
hooks/
...

컴포넌트는 Props를 받아 표시하기만 하면 되므로, 로직의 재사용성이 높아집니다.

이러한 패턴들은 모두 "중복을 줄인다", **"책임을 분리한다"**는 문제를 해결하려고 합니다. 하지만 유사한 화면이 대량으로 늘어나면, 이러한 패턴들만으로는 대처하기 어려운 문제가 발생합니다.

유사한 화면이 늘어나면 어떤 일이 발생하는가

실제 개발에서 흔히 있는 케이스를 생각해 보겠습니다. EC 사이트의 관리자 화면에서 마스터 관리 기능을 구축하는 구현을 예로 듭니다. 이번에는 이해를 돕기 위해 다음의 **5가지 집약(Aggregation)**을 대상으로 합니다.

  • 카테고리
  • 브랜드
  • 태그
  • 시즌
  • 소재

화면은 하나이며 **탭(Tab)**을 통해 마스터를 전환합니다. 각각에 **목록 화면·상세 화면·폼(Form)**이 있으며, URL도 다릅니다. 정직하게 구현하면 다음과 같습니다.

features/
Master/
Category/
...

파일 수는 순식간에 불어납니다. 그리고 각 피처의 내용을 살펴보면, 거의 동일한 코드가 5곳에 존재한다는 사실을 깨닫게 됩니다. (50곳이라면 지옥이겠지요...)

// Category/container.tsx
const { data, isLoading } = useCategoryListRows()
return <MasterPresentation data={data} isLoading={isLoading} columns={categoryColumns} />
...

공통 컴포넌트로 해결을 시도함

가장 먼저 떠오르는 것은 공통 레이아웃이나 컴포넌트로 분리하는 것입니다.

features/
Master/
components/
...

이렇게 하면 UI의 중복은 어느 정도 줄어듭니다. 하지만 container.tsx는 남습니다.

// Category/container.tsx
const { data, isLoading } = useCategoryListRows()
return (
...

UI의 모습은 공통화할 수 있었습니다. 하지만 문제는 2가지 남아 있습니다.

1. 로직 구조의 중복

「Hooks를 호출하여 Props를 전달한다」는 구조가 5곳 남아 있습니다. 각 집약(Aggregation)마다 서로 다른 데이터 취득 Hooks와 라우팅 정보는 공통 컴포넌트에서 흡수할 수 없기 때문입니다. 집약이 늘어날 때마다 동일한 구조의 파일이 증식하며, 사양 변경이 있을 때마다 5곳을 수정해야 합니다. (if문으로 대응할 수는 있지만, 마스터(Master)가 거대해지면 이 또한 힘들어집니다.)

2. 집약마다 미세하게 다른 UI의 중복

폼의 우측 패널, 추가 섹션, 커스텀 셀의 렌더링 등, 집약마다 미세하게 다른 UI 파츠가 존재한다고 가정해 봅시다. 이것들은 공통 컴포넌트에 Props로 전달할 수 있지만, 패턴이 늘어날수록 Props가 비대해지고 컴포넌트가 복잡해집니다.

// 공통 컴포넌트의 props가 비대해짐
<MasterForm
renderBody={...}
...

공통 컴포넌트는 어느 정도의 중복은 줄일 수 있지만, 로직과 미세하게 다른 UI의 중복을 근본적으로 해결할 수는 없습니다.

그렇다면, 어떻게 해결할까요?

Feature Registry 패턴

앞서 언급한 문제를 정리하면, 본질은 **「집약별 차이점을 어디에 작성할 것인가」**입니다. Feature Registry 패턴에서는 그 차이점을 정의 객체(Definition Object)로 분리하여 Registry에 등록함으로써 해결합니다.

먼저 디렉토리 전체 구조를 살펴보겠습니다.

features/
Master/
Tag/ # 전용 내부 구조를 가짐
...

각 집약의 container.tsx가 사라진 것을 알 수 있습니다. 그 역할은 registry와 Master/hooks/에 집중되어 있습니다.

정의 객체로 차이점을 표현하기

각 집약은 자신의 차이점을 정의 객체로 가집니다. 라우팅 정보, 데이터 취득 Hooks, 열(Column) 정의, UI 파츠의 렌더링을 모두 하나의 객체로 묶습니다.

// Category/index.tsx
export const categoryMasterItem: MasterItemDefinition<CategoryRow, CategoryValues> = {
key: 'category',
...

Registry에 등록하기

각 집약의 정의를 하나의 registry.ts에 등록합니다.

// registry.ts
export const masterRegistry = {
category: categoryMasterItem,
...

Entity 키로부터 Registry를 해결(Resolve)하기

Master/hooks/는 Entity 키를 받아 Registry에서 해당 정의를 추출하고, Container에 전달할 준비를 합니다. Container가 비대해지지 않도록 이 부분을 분리했습니다.

// Master/hooks/useMasterList.ts (마찬가지로 상세 화면도 해결하는 훅으로 분리되어 있습니다)
export const useMasterList = <Entity extends MasterEntityKey>(entity: Entity) => {
const masterItem = masterRegistry[entity] as MasterItemDefinition<MasterRowByEntity[Entity]>;
...

공통 Presentation이 정의를 실행하기

Container는 Hooks로부터 받은 masterItem을 그대로 Presentation에 전달할 뿐입니다.

// MasterList/container.tsx
export const MasterListContainer: FC<Props> = ({ entity }) => {
const { masterItem, rows, setRows, isLoading, errorMessage } = useMasterList(entity);
...

상세·폼 화면 또한 마찬가지이며, Presentation은 전달받은 정의를 실행할 뿐입니다. 집약이 무엇인지 알지 못해도 동작합니다.

export const MasterFormContainer: FC<Props> = ({
entity,
formType,
...

각 애그리거트(Aggregate)의 Hooks는 공통화하지 않는다

useCategoryDetailQuery, useBrandDetailQuery와 같은 각 애그리거트의 Hooks는 의도적으로 공통화하지 않았습니다.

각 애그리거트는 API 엔드포인트(Endpoint)도 다르고 응답(Response) 타입도 다릅니다. 이것들을 공통화하려고 하면 타입이 복잡해져 오히려 유지보수성이 떨어집니다. 또한, Hooks를 공통화하지 않더라도 "어떤 Hooks를 사용할지"를 정의 객체(Definition Object)에 주입하는 구조이므로 중복은 발생하지 않습니다.

새로운 애그리거트 추가하기

정의 객체, Hooks, 관련 컴포넌트를 작성하고 Registry에 한 줄 추가합니다.

export const masterRegistry = {
...
color: colorMasterItem, // 추가
...

container.tsx를 새로 만들 필요는 없습니다. 공통 Presentation이 그대로 동작합니다.

장점

변경 사항이 한 곳으로 모인다

애그리거트의 URL을 바꾸고 싶을 때, 해당 애그리거트의 index.tsx만 변경하면 됩니다. Pages, Container, Presentation을 가로질러 수정할 필요가 없습니다.

사양 변경의 영향 범위가 "해당 애그리거트의 정의 파일 하나"로 수렴하는 것은 팀 개발에 있어 큰 장점입니다.

애그리거트의 전체상을 파일 하나로 파악할 수 있다

"이 마스터(Master)는 어떤 URL을 사용하고, 어떤 Hooks를 쓰며, 어떤 UI를 가지는가"를 index.tsx를 보는 것만으로 알 수 있습니다.

일반적인 Container / Presentation 패턴에서는 이 정보가 Pages, Container, Presentation, Hooks에 분산되어 있습니다. 새로운 멤버가 "이 마스터 화면은 어떻게 동작하는가"를 파악하기 위해 파일을 가로질러 읽어야 하지만, 이 패턴에서는 그럴 필요가 없습니다.

Pages 측이 단순해진다

// pages/admin/master/[entity].tsx
const { masterItem, rows } = useMasterList(entity)

Entity 키를 전달하는 것만으로 동작합니다. Pages 측은 라우팅(Routing)의 상세 내용이나 어떤 Hooks를 사용하는지 알 필요가 없어집니다.

애그리거트의 추가·삭제가 registry의 한 줄로 해결된다

라우팅을 포함하여 모든 차이점(Diff)이 정의 객체에 모여 있기 때문에, 애그리거트를 추가할 때는 정의를 작성하고 Registry에 한 줄 추가하면 되고, 삭제할 때도 마찬가지로 한 줄을 지우는 것만으로 관련 코드가 다른 곳에 남지 않습니다.

export const masterRegistry = {
category: categoryMasterItem,
brand: brandMasterItem,
...

복잡성의 위치를 바꾸고 있다

언뜻 보면 기존 구현을 "복잡하게 만들고 있을 뿐"으로 보일 수 있지만, 그렇지 않습니다. 복잡성의 위치를 바꾸고 있는 것입니다.

일반적인 Container / Presentation 패턴에서는 복잡성이 각 애그리거트의 Container에 분산됩니다. 이 패턴에서는 각 애그리거트의 index.tsxregistry.ts에 복잡성을 집중시킴으로써 Container와 Presentation을 단순하게 유지합니다.

애그리거트가 늘어나면 늘어날수록 이 차이는 더욱 커집니다.

장벽과 AI

Feature Registry 패턴은 유사한 화면이 많은 Enterprise 환경에서의 설계로는 훌륭하지만, 실무에 채택하기에는 장벽이 있습니다.

신규 참여자의 비용이 높다

이 패턴의 장벽은 신규 참여자의 학습 비용입니다.

엄격한 정의 주도(Definition-driven) 방식으로 구축하면 MasterItemDefinition에 무엇을 정의해야 하는지, registry.ts에 어떻게 등록해야 하는지, 프로세스가 어떻게 흐르는지를 코드를 직접 읽어보지 않으면 알 수 없습니다.

일반적인 Container / Presentation의 심플한 패턴이라면 React 경험이 있는 사람이라면 구조를 보는 것만으로 이해할 수 있습니다. 이 패턴은 그러한 직관이 통하지 않기 때문에 기존 코드를 가로질러 읽어야 합니다.

하지만 AI의 등장으로 이 장벽은 크게 낮아지고 있습니다.

정의를 작성하게 하는 데 적합하다

MasterItemDefinition의 타입 정의와 기존 애그리거트의 index.tsx

가 하나 있다면, AI는 새로운 애그리거트 (Aggregate)의 정의를 거의 생성할 수 있습니다.

"Color라는 애그리거트를 추가해 주세요.

기존 Category의 정의를 참고하여,

다음의 컬럼 정의로 작성해 주세요."

하드코딩된 중복 코드를 AI에게 쓰게 하는 것보다, 정의를 쓰게 하는 것이 정확도가 높고 리뷰 비용도 낮습니다. 하드코딩의 경우 AI가 생성한 코드가 올바른지 모든 행을 확인해야 하지만, 정의라면 타입이 맞는지 체크하는 것만으로 충분합니다.

코드를 읽는 속도가 달라진다

신규 참여자가 이 패턴을 이해하는 비용이 높다는 문제도, AI가 있다면 달라집니다.

"이 코드는 어떤 구조로 되어 있나요?"라고 AI에게 묻는 것만으로, Registry의 메커니즘부터 Presentation으로의 흐름까지 설명을 들을 수 있습니다. 코드를 가로질러 읽는 시간이 대폭 단축됩니다.

Spec과의 궁합이 좋다

MasterItemDefinition의 타입 정의는 그대로 Spec으로서 기능합니다. "이 타입을 만족하는 정의 객체를 작성한다"라는 태스크는 AI가 가장 잘하는 작업 중 하나입니다.

타입 정의가 충실할수록 AI의 생성 정확도가 올라가기 때문에, 타입을 제대로 작성하는 것이 곧 AI를 위한 지시서가 되는 구조가 생겨납니다.

차세대 프론트엔드

최근 이루어지고 있는 Generative UI는 AI가 UI를 동적으로 생성하는 개념입니다. 카탈로그를 정의하고, 사용자의 요구에 따라 AI가 UI를 결정합니다. AI가 판단할 때, UI/UX로서 문제가 없는 것과 같은 **사전 정의 (Pre-definition)**를 엔지니어가 설계해야 합니다.

또한, Server Driven UI는 서버 측에서 UI를 지정하는 수법입니다. Shopify와 같은 해외 메가 테크 기업들이 도입하고 있는 모던한 수법입니다. 이 방식은 API 사양으로서 컴포넌트를 정의하고, 프론트엔드의 컴포넌트에 매핑합니다.

Feature Registry 패턴은 이러한 모던한 사상과 마찬가지로 "구현을 직접 쓰는 것이 아니라, 정의를 실행한다"라는 동일한 구조를 취합니다.

프론트엔드는 개별 화면을 코드로 직접 기술하는 세계에서, "차이(Difference)를 정의하고, 공통 기반이 이를 실행한다"는 방향으로 나아가고 있습니다. Server Driven UI, Metadata Driven UI, Generative UI는 모두 그 흐름 속에 있습니다. React 자체 또한 "UI를 직접 쓰는 라이브러리"에서 "정의를 실행하는 Runtime"으로서의 역할이 강해지고 있습니다.

이번에 소개한 Feature Registry 패턴 또한 "Feature의 차이를 정의로서 분리하여, 공통 실행 기반에 위임한다"는 의미에서 그 연장선상에 있는 설계입니다.

요약

이번에는 "Feature 단위로 유사한 대량의 화면"을 어떻게 구현할 것인가에 대해 써보았습니다. 솔직하게 if문으로 구현하는 것이 더 나은 경우도 있겠지만, Feature 단위의 정의 주도형 (Definition-driven) 설계가 필요한 상황도 있을 것이라 생각합니다.

또한, Bun이 Rust로 하루 만에 다시 작성된 사례처럼, 구현 작업 그 자체는 AI가 담당하는 시대가 올 것입니다. 그렇기에 무엇을 만들 것인가, 어떻게 설계할 것인가에 대한 판단이 더욱 중요해질 것이라고 생각됩니다. "코드를 쓰는 능력"에서 "설계하는 능력"으로... Feature Registry 패턴이나 정의 주도형 설계는 그 하나의 해답이 될지도 모릅니다!

참고

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0