안녕하세요, 이상혁입니다

프론트엔드 구조, 나만의 레이어드 레시피 🍰

프론트엔드 구조, 나만의 레이어드 레시피 🍰

2025년 4월 13일

혼자 프로젝트를 진행하게 되었을 때 가장 먼저 한 일은, 기존 프로젝트의 레포지터리를 클론하여 재사용 가능한 파일들을 선별해 남겨두는 것이었다. 하지만, 이전 프로젝트가 급하게 완성된 탓에 각 팀원의 스타일이 제각각 반영되어 있어, 관심사 분리가 명확하지 않았고 프로젝트 관리가 애매했다.

프론트엔드 폴더 구조를 어떻게 구성할지 고민하며 인터넷을 검색해본 결과,

  • 아토믹 디자인 패턴
  • DDD (Domain-Driven Design)
  • 클린 아키텍처
  • FSD (Feature-Sliced Design)

등 다양한 아키텍처 방식이 존재함을 알게 되었다. 이 중 일부는 직접 사용해보고 싶었지만, 프로젝트 규모가 작고 설계에 많은 시간을 할애하기 어려운 상황이라 가볍게 접근할 수 있는 방식을 원했다.

그 과정에서 백엔드 아키텍처 패턴을 프론트엔드 구조에 적용하는 사례들을 접하게 되었고, 그 중 특히 계층화 구조(Layered Architecture) 방식이 눈에 띄었다. 이에 해당 개념을 중심으로 구조를 탐구하고 적용해 보기로 했다.

레이어드 아키텍처는 소프트웨어 개발에서 가장 일반적으로 사용되는 아키텍처 패턴 중 하나로, 애플리케이션을 여러 수평적 계층으로 분리하는 방식입니다. 각 계층은 특정 책임을 가지며, 일반적으로 상위 계층은 하위 계층에 의존합니다.

레이어드 아키텍처의 주요 특징

  • 관심사의 분리: 각 계층은 특정 기능이나 관심사에 집중합니다.
  • 추상화: 각 계층은 하위 계층의 구현 세부사항을 추상화합니다.
  • 의존성 방향: 의존성은 일반적으로 위에서 아래로 흐릅니다.
  • 느슨한 결합: 각 계층은 인터페이스를 통해 통신하므로 결합도가 낮습니다.

레이어드 아키텍처의 4계층 구조

프레젠테이션 레이어(Presentation Layer)

  • 사용자와 직접 상호작용하는 계층
  • 사용자 인터페이스(UI) 구성 요소 포함

비즈니스 레이어(Business Layer)

  • 핵심 비즈니스 로직과 규칙을 구현하는 계층
  • 애플리케이션의 기능적 요구사항 처리

퍼시스턴스 레이어(Persistence Layer)

  • 데이터 접근 및 조작을 담당하는 계층
  • 데이터 저장소와의 상호작용을 추상화

데이터베이스 레이어(Database Layer)

  • 실제 데이터가 저장되는 계층

- ChatGPT

여러 블로그를 참고하고 ChatGPT에게도 질문해보며 아키텍처에 대한 개념을 익혔지만, 여전히 몇 가지 의문이 남았다.

  • 프론트엔드에는 데이터베이스가 없는데, 데이터베이스 계층은 무엇에 빗대어 계층을 분리할 수 있을까?
  • 비즈니스 계층에서 도메인별로 나눠야 할 때, 도메인을 어떤 기준으로 나눠야 할까?
  • 기존에 정의된 계층 사이에 하나를 더 추가하거나, 오히려 줄여도 괜찮을까?
  • 어떤 사람은 Entity, Application, Presentation, Infrastructure 같은 명칭으로 계층을 나누며 이를 Layered 아키텍처라고 설명하던데, 무엇이 정답일까?

첫 번째 질문과 관련해서는, 프론트엔드에서는 데이터의 출처가 데이터베이스가 아닌 외부 API나 **Web Storage(Local Storage, Session Storage, IndexedDB)**라는 점에 착안했다. 그래서 이러한 데이터 원천을 기반으로 Persistence 계층을 추상화하면 좋겠다는 결론에 도달했다.

두 번째 질문에서는 프로젝트의 성격에 따라 도메인을 나누는 기준이 달라질 수 있다는 점을 인지했다. 당시에는 전자투표 프로젝트를 진행 중이었기 때문에, ‘투표’를 구성하는 주요 요소를 중심으로 도메인을 정의했다. 예를 들어, 투표 정보, 안건, 후보, 투표자 정보(조합원) 등으로 나누었고, 이렇게 도메인을 잘 정리하니 상위 계층(특히 Persistence 계층)에서의 고민이 줄어드는 경험을 했다.

처음에는 전통적인 아키텍처는 대부분 구조가 일관될 것이라고 생각했지만, 실제 사례들을 보면 예상보다 다양하게 변형되어 사용되고 있었다. 그렇다고 해서 누군가의 방식이 ‘잘못됐다’고 느껴지지는 않았고, 오히려 서로 다른 방식들이 모두 일리 있는 설명을 제공하고 있었다.

공통적으로는 계층마다 책임을 분리하고, 흐름을 명확히 하며, 의존 방향을 일정하게 유지한다는 원칙을 지키려는 시도가 있었다. 이러한 특징들을 기반으로, 나도 직접 계층 간 역할과 흐름을 도식화해보며 내가 이해한 Layered 아키텍처를 정리해보았다.

layered recipient 3

프로젝트의 특성상 React에 의존하게 되면서, 컴포넌트와 상태 관리 부분이 아키텍처에 어느 정도 녹아들 수밖에 없었다. 비즈니스 로직을 React에서 어떻게 관리할 것인가에 대해 고민한 끝에, 코드 레벨에서는 Custom Hook 패턴을 활용하기로 했다. 또한 공통적인 오류 처리 외에도, 특정 도메인에서 발생할 수 있는 예외적인 상황들을 별도로 관리할 필요가 있었기 때문에, 비즈니스 계층 안에 “Fallback” 파트를 추가해 도메인별 오류 처리까지 아우를 수 있도록 설계했다.

layered recipient 4

이전 프로젝트 경험을 바탕으로, 이번 아키텍처에서는 **데이터 모델 전처리(Model Preprocessing)**의 중요성을 반영했다. 일반적으로 Model Preprocessing은 AI 분야에서 사용되는 용어로, 모델 학습 전에 데이터를 수집하고 정규화하여 학습에 적합한 형태로 준비하는 과정을 의미한다.

이와 유사하게, 본 프로젝트에서도 데이터를 단순히 받아들이는 것에서 그치지 않고, 각 View에서 필요한 형식으로 미리 포맷팅하거나 변형하는 작업을 사전에 수행하고자 했다.

이러한 전처리 과정은 비즈니스 계층에 위치하며, 이를 통해 이후 로직 처리 시 코드가 더욱 깔끔해지고, UI에서도 일관성 있는 데이터 표현이 가능해진다. 결과적으로, 전반적인 데이터 흐름과 유지보수 측면에서 큰 이점을 기대할 수 있다.

/** Project Common Resources **/ |-- src |---- assets // Images, Fonts 등의 정적 콘텐츠 관리 |---- utils // Formatting, Configuration 같은 공용 함수 관리 |---- layouts // Header, Footer 같은 레이아웃 컴포넌트 관리 |---- hooks // 해당 프로젝트에서 공용적으로 쓰이는 Hook(함수) 관리 |---- styles // 해당 프로젝트의 global style |---- types // 해당 프로젝트에서 이용하는 공용 Types 및 Interfaces |---- libs // 해당 프로젝트에서 이용하는 라이브러리 관련 config 및 함수 관리 |---- constants // 해당 프로젝트에서 이용하는 상수 관리 |---- components // 해당 프로젝트에서 공용적으로 쓰이는 컴포넌트 관리 | ... | ... |------ Error // 에러 관련 컴포넌트 폴더 | | /** Model Layer **/ | |---- models |------ [관심사명] | // 관심사에 대한 Model(Type) 정의 및 데이터 가공 로직 포함 | | /** Service Layer **/ | |---- services |------ [관심사명] |-------- index.ts // 해당 관심사의 API 함수 및 Request, Response Types 관리 |-------- mock.ts // 해당 관심사의 Mock API | | /** Application Layer **/ | |---- core | ... | ... |------ [Main Route(메인 라우트)명] |-------- components // 해당 경로에 관련된 컴포넌트 관리 |-------- hooks // 해당 경로에 관련된 Hooks(Queries, Mutations, Logics 등) 관리 |-------- utils // 해당 경로에 관련된 함수 관리 | | /** Presentation Layer **/ | |---- pages // 여러 View, Step 관리 |---- routes // Private, Public View Routing 관리 | | /** Settings(Others) **/ | ... | ... |---- vite.config.ts |---- index.html

설계 초기에는 아키텍처 구상대로 프로젝트를 진행하고자 했지만, 실제 적용 과정에서는 약 80% 정도만 원안대로 이행되었고, 나머지 일부는 예상과 다른 방향으로 프로젝트가 관리되면서 수정이 필요했다.

layered recipient 2

특히 비즈니스 계층의 구조에 있어, 처음에는 도메인별로 나누려 했지만, 실제로는 페이지 경로를 기준으로 관리하는 방식으로 전환하게 되었다. 예를 들어, 하나의 비즈니스 로직 함수가 A 도메인과 B 도메인 모두와 관련된 경우, 어느 도메인 폴더에 넣어야 할지 판단하기 어려운 상황이 자주 발생했다.

이러한 불필요한 고민을 줄이고, 라우팅과 도메인 간의 이중 관리 문제를 피하기 위해, 비즈니스 로직을 페이지 경로 기준으로 정리하는 방식을 택했다. 덕분에 구조에 대한 고민보다 로직 구현에 집중할 수 있는 환경을 빠르게 마련할 수 있었다.

layered recipient 5

View들을 컴포넌트 단위로 관리하면서, 페이지 단위 컴포넌트, 디자인 시스템 컴포넌트, 프로젝트 전역에서 사용하는 공통 컴포넌트 등으로 구분해 사용했습니다. 하지만 프로젝트에만 국한되며 특정 도메인이나 페이지에 의존하는 애매한 컴포넌트들도 존재했는데, 이러한 컴포넌트는 별도로 분리하지 않고 해당 페이지 내부에 직접 구현하려 했습니다. 그러나 이 방식은 페이지 코드의 역할이 명확하지 않아 가독성과 관리가 어려웠습니다.

처음에는 Presentation 계층에서 도메인별로 컴포넌트를 분리해 관리하려 했지만, 이 구조가 Application 계층과 역할이 겹치는 느낌이 강했고, 폴더 관리 측면에서도 비효율적일 것 같다는 판단이 들었습니다. 결국 FSD(Feature-Sliced Design) 방식을 참고하여, 각 라우트마다 컴포넌트, 유틸, 훅 등을 하나의 기능 단위로 묶어 관리하는 방식으로 구조를 재정비했고, 이로 인해 프로젝트 진행이 한결 수월해졌습니다.

설계를 완벽히 이행하지 못한 점은 아쉬움으로 남았지만, 상황에 맞게 유연하게 대응하며 프로젝트를 진행한 점에는 만족감을 느꼈다. 다만, 일부 결정에서 특별한 근거나 논리 없이 개인적인 ‘감(Feel)’에 의존한 부분이 있었기에, 앞으로는 이러한 판단에 보다 타당한 근거를 더해나갈 수 있는 방법을 고민해보고자 한다.