ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프론트엔드 폴더 스트럭처 구성 종류 (폴더링 패턴)
    Front-end 2025. 9. 11. 17:33
    반응형

    프론트엔드 폴더스트럭처

    어떻게 해야 이 프로젝트에 맞는 폴더 구조를 갖출 수 있을까?

    어떻게 해야 협업 하는데 좋은 방향으로 만들 수 있을까?

    자칫 쓸데없이 폴더 구조가 무거워지거나 헷갈리진 않을까?

    괜히 심플하게 했다가 확장성 없이 구현 되진 않을까?

    내가 알고 있는 방법 외에 또 다른 방법이 없을까?

    ...

    아마 프론트엔드 개발자라면 사용하는 폴더 구조에 대해서 고민을 해본 적 있을 것이다.

    나 또한 협업의 관점과 프로젝트의 관점에서 이것저것 섞어서 사용을 해본 경험이 있는데, 폴더 구조를 만들어내는 것은 정말 어려운 것 같다.

    차라리 Java나 NestJS와 같은 Framework라면 정해진 틀이라고 있을텐데, 특히 React의 경우에는 src/, assets/, public/ 등 외에는 짜여진 틀이 없다보니 src/ 내부에서 여러가지 폴더와 파일들이 소용돌이치게 된다.

    그래서 어떤 종류의 패턴이 있는지 몇 가지 살펴보고, 맞는 방법을 고민해 볼 수 있는 계기가 되면 좋겠다.


    1. Layered structure

    src/
     ├── components/
     ├── pages/
     ├── services/
     ├── hooks/
     ├── utils/
     └── assets/

    정의

    우선 전통적인 방식이라고 할 수 있는 Layered structure 이다.

    UI, services, hooks, utils 등 기술별로 폴더를 나누는 방법을 말한다.

    장점

    단순하고 빨라서 MVP를 만들 때 주로 사용된다

    사용에 대한 진입장벽이 낮은 편이다

    단점

    기능 관련 코드가 흩어져있어서 가독성이 저하 된다

    공통 폴더가 쓰레기통화 된다

    변경 범위가 넓어져서 리팩토링 비용이 증가하게 된다

    이런 단점을 극복하기 위해서 몇 가지 규칙을 정해놓을 수 있는데 components/는 UI만, services/는 비즈니스 로직으로만 분리를 하고, ESLint를 이용해서 강제로 services에서 components로 직접 의존 금지 규칙을 설정하고, 정기적인 리팩토링을 해주는 것이 좋다.

    2. Feature-based structure

    src/
     ├── features/
     │    ├── auth/
     │    │    ├── components/
     │    │    ├── hooks/
     │    │    ├── services/
     │    │    └── index.ts
     │    └── todo/
     │         ├── components/
     │         ├── hooks/
     │         ├── services/
     │         └── index.ts
     ├── shared/
     │    ├── components/
     │    ├── hooks/
     │    └── utils/
     └── app/
          ├── store.ts
          └── router.tsx

    정의

    이는 "기능 단위"의 구조를 말한다.

    장점

    관련 코드가 한 곳에 모여있어서 이해 및 추적이 용이하다

    유지보수가 쉬워진다.

    소유권이나 테스트를 명확하게 할 수 있다

    단점

    작은 프로젝트에서는 다소 과한 설정이 될 수 있다

    utils/ 등의 폴더에서 중복 코드가 발생할 수 있다

    팀 내 스타일이나 API 불일치가 나타날 수 있다

    이런 단점을 극복하기 위해서 feature는 index.ts를 이용해서 공개 API를 제한하며, 내부 구현은 외부에서 직접 참조를 금지하도록 내부적으로 규칙을 정할 수 있다.

    또한 중복 코드가 나타나지 않도록 2번 이상 사용되는 코드의 경우 shared/ 폴더로 추출하는 규칙을 만들 수 있으며, ESLint의 "no-restricted-imports"를 이용해서 feature 간 직접 참조를 차단하는 방법을 사용할 수 있다.

    Domain-driven structure

    src/
     ├── domains/
     │    ├── user/
     │    │    ├── model/
     │    │    ├── ui/
     │    │    └── api/
     │    └── product/
     │         ├── model/
     │         ├── ui/
     │         └── api/
     ├── shared/
     │    ├── ui/
     │    ├── lib/
     │    └── api/
     └── app/
          ├── providers/
          ├── router.tsx
          └── store.ts

    정의

    비즈니스 도메인 단위로 설계하는 방식이며, Model, UI, API 등을 세분화 시킨다.

    장점

    대규모 서비스 프로젝트에 적합하다

    팀 단위로 도메인이 분리되어서 작업할 때 편리하다

    큰 조직에서 사용하게 되면 책임 경계가 비교적 명확하다

    도메인 전략(비즈니스 규칙) 반영이 쉬운 편이다.

    단점

    구조 설계가 복잡해서 설계 비용이 높다.

    초반 경계 결정이 어려워, 초기 진입 장벽이 있는 편이다.

    경계가 잘못 설정되면 여러 도메인에서 중복되는 현상이 나타날 수 있다

    이런 단점을 완화시키기 위해서 규칙을 잘 정의해야할 것이다.

    도메인 인터페이스를 문서화하고, 버전 관리를 잘 해야한다.

    각 도메인마다 책임자를 지정하고 관리할 수 있도록 하며, cross-domain API의 경우 adapter 등의 방식을 사용해서 처리할 수 있다.

    Atomic Design Pattern

    src/
     ├── components/
     │    ├── atoms/       (Button, Input)
     │    ├── molecules/   (SearchBar, Card)
     │    ├── organisms/   (Header, Form)
     │    ├── templates/   (Page layout)
     │    └── pages/       (Final screens)
     └── utils/

    정의

    디자인 시스템으로 atoms → molecules → organisms → templates → pages 방식으로 확장시켜나가는 방식

    장점

    디자인 일관성 확보

    UI 재사용성 극대화

    유지보수나 수정이 쉬움 (ex. atoms/Buttons 하나만 변경하면 전체 적용)

    디자인 시스템 구축시 유리

    단점

    초기 학습 필요

    비즈니스 로직 관리 어려움

    팀원마다 "무엇이 atoms의 경계인가" 해석 차이 발생

    과도한 분해로 prop-drilling 발생 가능

    추적 비용 증가

    UI만 보고 로직을 섞을 위험

    단점을 보완하기 위해서 atoms는 순수한 표현 형태만 유지하고, 상태나 비즈니스 로직 사용은 금지한다.

    팀에서 규칙을 잘 정하고 진행하며, 리더는 PR 했을 때 혹은 따로 폴더를 정리하며 팀원들에게 공유한다.

    Feature-Sliced Design (FSD)

    src/
     ├─ app/        # 엔트리포인트, providers, 글로벌 설정
     ├─ processes/  # 유저 시나리오(회원가입 플로우 등)
     ├─ pages/      # 라우트 단위, 각 페이지의 조합
     ├─ features/   # 독립된 기능 단위 (로그인, 검색 등)
     ├─ entities/   # 핵심 도메인 모델 (User, Product)
     ├─ shared/     # 공용 리소스 (UI, hooks, libs, config)

    정의

    흔히 FSD라고 부르는 방법이다.

    "Layer(계층) + Slice(슬라이스)" 구조로 feature(기능) 중심의 계층별 책임 분리가 강조되는 방법이다.

    장점

    UI → 비즈니스 도메인 → 인프라 순으로 계층을 분리

    팀 규모가 커져도 구조가 무너지지 않고, 모듈 간의 의존성을 관리하기 쉬운 편이다

    각 레이어가 import 할 수 있는 방향을 규칙으로 제한 가능하다

    단점

    초반 러닝커브가 있는데, 특히 app, process, pages, features, entities, shared 개념 구분에 대한 어려움이 있을 수 있다

    작은 프로젝트에서는 오버 엔지니어링이 될 수 있다

    이런 단점을 완화 시키기 위해서 도입 전에 어느 기능을 feature로 둘지, 혹은 entity로 둘지 팀 내 합의를 하면 좋다.

    ESLint를 이용하여 import/order + custom rule로 계층 import 규칙을 강제로 설정하거나, shared 쓰레기통화 방지를 위해서 재사용 2회 이상 규칙과 같은 것이 필요할 수 있다.

    또한 README.md 파일을 이용해서 각 레이어 책임 정의를 작성하는 것도 팀 간의 공유에 도움이 될 수 있다.

    Modular Monolith

    src/modules/
     ├─ user/
     │   ├─ api/
     │   ├─ components/
     │   └─ store/
     ├─ product/
     │   ├─ api/
     │   ├─ components/
     │   └─ store/
     └─ shared/

    정의

    백엔드 Modular Monolith 개념을 프론트엔드에 적용한 것으로 각 도메인을 모듈 단위로 완전히 독립적인 패키지처럼 다룬다는 특징이 있다.

    장점

    DDD와 유사하게 도메인의 경계가 강한 편이다.

    각 모듈은 독립 배포/테스트가 가능하다

    단점

    경계를 나누기 위한 고민을 해야하며, 작은 프로젝트에서는 불필요하게 복잡성이 높아질 수 있다.

    모듈 간 의존성 관리가 어려우면 spaghetti import 현상이 나타날 수 있다

    이를 완화시키기 위해 모듈 간 타입 경계 관리가 필요하며, Barrel index 사용은 최소화 시켜서 실제 의존 관계 추적에 용이하도록 하는 편이 복잡성을 떨어트릴 수 있는 방법이 될 수 있다.

    Clean Arichitecture

    src/
     ├─ entities/   # 순수 도메인 로직, 타입
     ├─ usecases/   # 도메인 규칙을 실행하는 서비스
     ├─ ui/         # 리액트 컴포넌트
     └─ infra/      # 외부 API, storage, lib

    정의

    Clean Archiecture를 프론트엔드 진영에서 사용하기 좋게 변형 및 단순화 시킨 방법으로 핵심 도메인(Entities), Use Cases, UI, Infra 등의 레이어로 나눠서 사용

    장점

    비즈니스 로직을 UI와 분리할 수 있다.

    테스트 친화적이다.

    도메인 규칙이 명확해져서 유지보수에 용이하다.

    단점

    빠른 프로토타이핑에 불리

    프론트엔드 특성상 UI가 핵심이다보니, domain-first 접근은 과한 경우가 많음.

    작은 프로젝트에서는 오버 엔지니어링이 될 수 있음

    단점을 극복하기 위해 Domain 코드와 UI 코드 import 방향을 철저히 지키는 것이 좋다.


    사실 정확한 답이 있을까 싶다.

    Atomic Design Pattern은 이렇게 사용하는거니까 무조건 이렇게 사용해야지! 라고 하는 순간 갇히게 되는 것 같다.

    그냥 프로젝트에 맞는 방법을 찾고 팀에서 합의하여 진행 할 수 있다면 정의 된 방법이 아니라도 그 프로젝트에 맞는 더 좋은 방법이 있을 수 있다고 생각한다.

    반응형
Designed by Tistory.