FE 프로젝트 아키텍처 & 설계?

architecture

최근 육아휴직을 하게 되었고 중간중간 빈 시간을 활용하여 무엇을 공부해볼지 고민하다가, 현재 연차의 실무 역량 향상을 위해 공부할 것을 AI에게 추천받았다. 리액트의 최신 패러다임부터 타입스크립트 심화까지 다양한 추천을 받았는데, 그중에서도 "프론트엔드 아키텍처 & 설계 역량"이라는 항목이 특히 눈에 들어왔다.


프론트엔드 아키텍처에는 여러 가지 접근 방식이 있지만, 이 글에서는 Feature Sliced Design, Clean Architecture, Atomic Design을 중심으로 간단히 살펴보고자 한다. 그리고 현재 개발을 담당하고 있는 프로젝트 구조와 비교해 보면서, 프론트엔드 아키텍처와 설계 관점에서 어떤 인사이트를 얻을 수 있는지 정리해 보는 것을 목표로 한다. 세 가지 설계 방식은 이미 많이 사용되고 있고 잘 정리된 글들도 많기 때문에 여기서는 핵심 위주로 가볍게 정리해 보려 한다.



Feature Sliced Design

FSD(Feature Sliced Design)는 프로젝트의 복잡성을 제어하고 확장성을 보장하기 위해 고안된 아키텍처 방법론이라고 한다. 실제 설계는 도메인 주도 설계 혹은 모듈형 아키텍처와 개념적으로는 유사하지만, 리액트 컴포넌트 특성에 중점을 두고 복잡성을 효과적으로 분리할 수 있도록 설계되었다.


가장 중요한 핵심 요소로는 Layer / Slice / Segment 이며 FSD는 세 가지를 사용하여 3단계 계층 구조를 따르도록 한다.


FE-Architecture-1

FSD는 Layer / Slice / Segment 구조로 비즈니스적 가치를 수직 분할하여 모든 의존성이 단방향으로 흐르도록 설계가 된 아키텍처이다.


Layer는 공통 UI를 포함하여 비즈니스 핵심 로직과 페이지 등을 가지는 최상위 폴더이다. Layer에서는 app, pages, widgets, features, entities, shared 순서로 상하위가 존재하며 하위 Layer에서는 상위 Layer를 참조할 수 없도록 한다.

Slices는 Layer 내부에서 비즈니스 도메인별로 분리된 구조이며 같은 Layer내 다른 Slices는 참조할 수 없도록 한다.

Segments는 Slices 내부에서 코드의 역할에 따라 분리된 구조이다. Layer중 app, shared는 Slices 구조 없이 바로 Segments로 구성되어 진다.


FSD의 수직적 분할 구조는 코드 간 결합도를 낮추고 응집도를 높여, 특정 기능을 수정하더라도 시스템 전체에 미치는 사이드 이펙트를 최소화하는 데 도움을 준다. 이러한 특성 덕분에 단순한 라이브러리보다는 애플리케이션 개발에 더 적합하며, 유지보수 측면에서도 유리하고 동일한 프로젝트를 진행하는 팀원과 구조에 대한 공통 이해를 맞추기 쉽다.

다만 구조에 대한 일정 수준의 학습이 필요하며, 각 페이지가 서로 강하게 분리된 독립적인 특성을 가지는 경우에는 오히려 과도한 구조로 인해 복잡도가 증가할 수 있다는 점은 고려해야 한다.



Clean Architecture

클린 아키텍처는 시스템 복잡성을 관리하고 유지보수성을 높이기 위해 로버트 C. 마틴이 제안한 설계 방법이다. 아키텍처의 핵심은 관심사 분리로 주요 비즈니스 로직을 UI, 데이터베이스, 프레임워크와 같은 기술 환경으로부터 독립시켜 시스템이 특정 도구에 종속되지 않도록 설계하는 것이라고 한다.


주요 핵심은 Entities / Use Cases / Interface Adapters / Frameworks & Drivers 네 개의 동심원 계층으로 나누어진다.


FE-Architecture-2

클린 아키텍처의 동심원 계층은 안쪽으로 갈수록 시스템의 고수준 정책으로 쉽게 변하지 않는 규칙들이며 바깥쪽으로 갈수록 저수준의 변하기 쉬운 규칙들이 위치한다.


Entities는 비즈니스의 핵심 데이터 모델과 규칙으로 구성되며, 외부의 변화에 전혀 영향받지 않는 시스템의 가장 본질적인 요소이다.

Use Cases는 시스템이 수행해야 할 비즈니스 고유 규칙과 사용자 행위를 정의하는 요소이다. 데이터가 어디에 저장되는지, 화면에 어떻게 표시되는지와 같은 세부 사항은 전혀 알지 못한 채 오직 비즈니스 시나리오를 완결성 있게 실행하는 것을 목표로 한다.

Interface Adapters는 Use Cases와 외부 환경을 잇는 가교 역할을 수행한다. 내부의 비즈니스 언어를 외부 시스템이 이해할 수 있는 데이터 형식으로 변환하거나, 내부에서 정의한 인터페이스를 실제로 구현하여 시스템의 구체적인 동작을 실행 가능하게 만든다.

Frameworks & Drivers는 시스템의 가장 바깥쪽 계층으로, 데이터베이스나 프레임워크와 같이 비즈니스 핵심 로직과는 무관한, 언제든 교체 가능한 기술적 도구들이 위치한다.


클린 아키텍처의 동심원 계층 구조는 소스 코드의 의존성을 항상 안쪽으로만 향하게 설계하여, 외부 변경으로 인한 사이드 이펙트를 최소화한다. 또한, 의존성 역전 원칙(DIP)을 통해 구체적인 구현체가 아닌 추상화된 인터페이스를 참조함으로써, 제어의 흐름과 의존성 방향을 분리하고 고수준 정책의 독립성을 확고히 보장한다.

그리고 클린 아키텍처 또한 일정 수준의 학습 곡선이 존재하며, 도입 시 많은 양의 보일러플레이트(Boilerplate) 코드가 생성되어 프로젝트의 구조적 복잡도가 높아질 수 있다는 점을 충분히 고려해야 한다.



Atomic Design

아토믹 디자인은 화학에서의 원자 설계 방법론에서 힌트를 얻어, 인터페이스 디자인 시스템을 보다 체계적이고 계층적인 방식으로 구축하기 위해 서로 연관된 다섯 단계로 구성된 방법론이라고 한다.


연관된 다섯 단계는 Atoms / Molecules / organisms / templates / pages 순서로 단계를 거쳐 사용되도록 한다.


FE-Architecture-3

각 단계는 이전 단계의 요소를 조합하여 상위 단계의 요소를 구성하며, 낮은 단계일수록 재사용성이 높은 요소들이 각자의 역할에 맞게 배치된다.


Atoms는 버튼(Button)이나 입력창(Input)처럼 더 이상 쪼갤 수 없는 가장 작은 단위의 요소이다. 개별적으로는 복잡한 기능을 수행하기 어렵지만, 모든 UI를 구성하는 핵심 기초가 된다.

Molecules는 Atoms를 결합하여 검색창(Input + Button)처럼 특정 기능을 수행하는 단위이다. 재사용이 용이하도록 설계된 작은 규모의 구조체 역할을 한다.

Organisms는 Atoms와 Molecules를 조합하여 구성된 독립적인 인터페이스 구역이다. 실제 화면의 특정 영역을 담당하는 핵심적인 컴포넌트 단위이다.

Templates는 실제 데이터가 주입되기 전, 컴포넌트들을 배치하여 레이아웃과 골격을 잡는 단계이다. 주로 Organisms들의 위치를 설계하고 전체적인 구조를 정의한다.

Pages는 Templates에 실제 콘텐츠와 데이터를 주입하여 완성한 단계로, 사용자가 최종적으로 마주하게 되는 화면이다.


아토믹 디자인은 단방향 의존성을 기반으로 작은 단위의 요소를 결합하여 상위 단계를 구축하고, 최종적으로 실제 화면을 구성하는 것을 목표로 한다. 체계적인 구조로 컴포넌트를 세분화할 수 있으며, 재사용성이 높은 최소 단위들을 조합함으로써 코드 중복을 효과적으로 방지한다. 이러한 장점 덕분에 일관성 있는 디자인 시스템을 구축할 때 매우 유용한 아키텍처로 활용될 수 있다.

하지만 중간 계층 분류의 모호함으로 인해 컴포넌트를 나누는 과정에서 혼란이 발생할 수 있으며, 엄격하게 계층을 나눌 경우 프로젝트의 구조가 복잡해지거나 디렉토리의 깊이가 깊어지는 현상이 나타나기도 한다.

또한, 아토믹 디자인은 UI 컴포넌트의 계층화에 집중하기 때문에 도메인이나 비즈니스 기능 측면에서의 별도 규칙이 필요하다. 따라서 최근에는 FSD(Feature-Sliced Design) 구조 내의 UI 세그먼트(Segments)에 아토믹 디자인을 적용하거나, 클린 아키텍처를 통해 주요 비즈니스 로직을 분리하고 UI 영역에만 아토믹 디자인을 혼합하여 사용하는 방식이 많이 사용되고 있다.



우리의 프로젝트는?

현재 담당한 프로젝트는 Route 중심으로 설계된 아키텍처라고 설명하고 싶다. 처음 설계한 구조는 feature 단위로 비즈니스 로직을 관리하고 components에 UI를 배치하여 비즈니스 로직과 UI를 분산시키는 것을 목표로 하였다.

feature는 서버 API의 분류 기준으로 나누었고, 만약 여러 feature에서 사용되는 공통 로직 혹은 컴포넌트가 존재한다면 feature와 동일 계층에 존재하는 commons라는 공통 폴더에서 관리되도록 규칙을 정하였다. 그리고 feature 하위에서는 components / hooks / models / stores / utils로 나누어 관리하고 있다.

components에서는 feature 내부에서 Containers / Presentations로 나누어 비즈니스 로직을 포함한 컴포넌트와 그렇지 않은 컴포넌트를 구분 지어 관리하였다.


최상위 계층에서는 feature와 동일한 레벨에서 commons / routes / pages / layers / error 디렉터리를 두어, 공통 모듈, 라우팅 정의, 페이지 컴포넌트, 모달 컴포넌트, 에러 처리를 각각 분리해 관리하고 있다.

docker
src/ ├── commons/ # 공통 모듈 ├── routes/ # 라우팅 관리 ├── pages/ # 페이지 컴포넌트 ├── layers/ # 모달 컴포넌트 ├── error/ # 에러 처리 ├── ... # feature 단위 나열 ├── home/ # 홈 feature ├── setting/ # 설정 feature ├── components ├── hooks ├── models ├── stores ├── utils

Route 중심으로 설계된 아키텍처라고 프로젝트를 설명했던 이유는 처음 시작을 feature 단위로 나누어 관리하였지만, 페이지별 특성이 뚜렷한 프로젝트이다 보니 서버 API 또한 어떤 페이지에서 호출할 API인지 명확한 형태로 나누어져 Route 중심으로 설계되었다고 보는 것이 더 맞지 않나 생각한다. 실제로 feature로 나누어진 영역은 pages 하위에 페이지 컴포넌트로서 존재하고 있다.



어떤 부분을 개선하면 좋을까?

첫 번째로 생각되는 부분은 Route 중심으로 비즈니스 로직과 컴포넌트가 나뉘다 보니 여러 feature에서 필요한 경우 commons(공통 모듈)로 들어가는 로직들이 많아졌다. 특정 하나의 feature에서 사용되는 API나 비즈니스 로직이 다른 곳에서 사용이 필요해진다면 공통 모듈로 옮겨야 한다. 이는 하나의 도메인 혹은 기능적 중요 로직이 단순하게 공통 모듈이라는 창고에 쌓이는 모습이 될 것으로 생각된다. 그리고 공통 모듈 내에서 같은 계층의 다른 모듈을 참조해야 하는 상황이 발생하는 경우 순환 참조가 발생할 가능성이 높은 구조가 되어버린 것 같다.

만약 FSD의 Slices처럼 비즈니스 도메인을 분리하는 형태나 클린 아키텍처와 같이 주요 비즈니스 로직을 명확하게 나누었다면 좋지 않을까 생각이 든다.


두 번째는 components에서의 Containers / Presentations로 비즈니스 로직을 포함하는 Containers와 그렇지 않은 Presentations로 나누는 것으로 규칙을 정하였지만, 규칙이 지켜지지 않는 컴포넌트들이 존재하는 것을 확인하였다. 초기에는 규칙을 지키며 작업하였지만, 그 경계가 하나씩 무너졌던 것 같고 지금은 큰 의미 없는 구분처럼 보인다.

이 문제를 해결하기 위해 재사용성이 높은 컴포넌트들은 commons 영역에서 아토믹 디자인을 적용해 Atoms / Molecules / Organisms로 구분하고, feature 내부의 components에서는 templates / pages 단위로 나누어 하나의 페이지를 구성하는 방식은 어떨까 생각해보았다. 다만 이러한 구조를 도입한 이후, 규칙을 어떻게 일관되게 유지해 나갈 것인지는 추후 다시 고민해봐야 할 과제라고 생각한다.



FE 프로젝트 아키텍처 & 설계?

FSD(Feature Sliced Design), Clean Architecture, Atomic Design은 모두 계층 분리를 통해 의존성을 명확하게 관리하려는 공통된 목적을 가지고 있다고 볼 수 있다. 쉽게 변하지 않거나 재사용성이 높은 핵심 영역과 상대적으로 변경 가능성이 높은 영역을 구분하고, 각 계층이 어떤 방향으로 의존해야 하는지를 명확히 정의한다는 점에서 방향성이 유사하다.

이러한 설계 방식들은 단순히 구조를 나누는 데서 그치는 것이 아니라, 계층별 책임을 분리하고 의존성 규칙을 기반으로 점진적으로 결과물을 완성해 나가는 과정 자체를 중요하게 다룬다. 이를 통해 변경에 강한 구조를 만들고, 유지보수성과 확장성을 확보하는 데 목적이 있다.

실제 개발을 하다 보면 "이 프로젝트에는 어떤 아키텍처와 설계가 가장 적합할까?"라는 고민에 자주 빠지게 된다. 단순하게 위에서 알아보았던 아키텍처, 디자인 패턴 혹은 다른 설계 방식을 프로젝트에 적용할 수도 있지만, 공통된 핵심 원칙, 계층 분리, 의존성 관리, 책임의 명확화를 인지한 상태에서 프로젝트의 성격에 맞게 규칙을 조정하고 만들어 나간다면, 결국 해당 프로젝트에 가장 잘 맞는 우리만의 아키텍처와 설계 규칙이 자연스럽게 만들어지진 않을까도 생각해 본다.

FE-Architecture-4

"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs -