웹 프론트엔드 아키텍처 비교
FSD(Feature-Sliced Design), Layered Architecture, Atomic Design, DDD, MVC/MVVM 등 프론트엔드 코드 구조 설계 아키텍처를 심층 비교 분석합니다.
렌더링 방식이 아닌 코드를 어떻게 구조화할 것인가에 대한 설계 패턴을 다룹니다.
FSD, Layered Architecture, Atomic Design, DDD, MVC/MVVM을 비교합니다.
🗂️ 목차
- 아키텍처 선택이 중요한 이유
- FSD (Feature-Sliced Design)
- Layered Architecture (계층형 아키텍처)
- Atomic Design
- DDD (Domain-Driven Design)
- MVC / MVVM
- 종합 비교표
- 어떤 걸 선택해야 할까?
1. 아키텍처 선택이 중요한 이유
프론트엔드 프로젝트는 초기에 단순하게 시작하더라도 기능이 추가되면서 빠르게 복잡해진다. 명확한 코드 구조 설계 없이 진행하면 다음과 같은 문제가 반복된다.
- 의존성 지옥: 컴포넌트가 서로를 무분별하게 참조해 수정 시 연쇄 영향 발생
- 중복 코드: 재사용 원칙 없이 비슷한 로직이 여러 곳에 산재
- 낮은 응집도: 관련 없는 코드가 같은 파일에 모여 가독성 저하
- 높은 결합도: 특정 모듈을 교체하거나 삭제하기 어려운 구조
좋은 아키텍처는 이러한 문제를 예방하고, 팀원 모두가 동일한 규칙으로 코드를 작성할 수 있는 가이드라인을 제공한다.
2. FSD (Feature-Sliced Design)
개요
FSD는 2021년경 러시아 개발 커뮤니티에서 체계화된 프론트엔드 전용 아키텍처 방법론이다. 코드를 레이어(Layer) → 슬라이스(Slice) → 세그먼트(Segment) 의 3단계 계층으로 나누는 것이 핵심이다.
폴더 구조
src/
├── app/ # 앱 초기화, 전역 설정, 라우팅
├── pages/ # 라우트 단위 페이지 조합
├── widgets/ # 독립적인 복합 UI 블록
├── features/ # 사용자 시나리오/액션 단위
├── entities/ # 비즈니스 도메인 단위
└── shared/ # 공통 유틸, UI 킷, 타입 등
핵심 규칙: 단방향 의존성
FSD의 가장 중요한 원칙은 상위 레이어는 하위 레이어만 참조 가능하다는 단방향 의존성이다.
app → pages → widgets → features → entities → shared
예를 들어 features 레이어는 entities와 shared만 가져올 수 있고, 절대 pages나 widgets를 import해서는 안 된다.
코드 예시
// features/auth/ui/LoginForm.tsx
import { Button } from '@/shared/ui'; // ✅ shared 참조 가능
import { UserAvatar } from '@/entities/user'; // ✅ entities 참조 가능
// import { Header } from '@/widgets/header'; // ❌ 상위 레이어 참조 불가
export const LoginForm = () => {
return (
<form>
<input type="email" />
<input type="password" />
<Button>로그인</Button>
</form>
);
};
장점
- 비즈니스 기능 단위로 코드가 응집되어 유지보수 용이
- 단방향 의존성으로 순환 참조 원천 차단
- 팀 단위 병렬 개발 용이 (슬라이스 단위로 작업 분리)
- 규칙이 명확해 코드 리뷰 기준이 일관적
단점
- 초기 학습 비용이 높음 (레이어 기준 이해 필요)
- 소규모 프로젝트에서는 과도한 구조로 느껴질 수 있음
- 슬라이스 간 공유 로직 처리 기준이 모호한 경우 발생
적합한 상황
- 중대형 팀이 협업하는 장기 프로젝트
- 비즈니스 도메인이 명확히 구분되는 서비스
- 코드 품질과 유지보수성을 최우선으로 하는 프로젝트
3. Layered Architecture (계층형 아키텍처)
개요
백엔드에서 오랫동안 사용되어 온 계층형 구조를 프론트엔드에 적용한 방식이다. 코드를 기술적 역할에 따라 수평으로 나눈다. 가장 직관적이고 진입 장벽이 낮아 많은 프로젝트에서 자연스럽게 채택된다.
폴더 구조
src/
├── components/ # UI 컴포넌트
├── pages/ # 라우트 페이지
├── hooks/ # 커스텀 훅
├── services/ # API 호출 및 외부 통신
├── store/ # 전역 상태 관리
├── utils/ # 유틸리티 함수
└── types/ # TypeScript 타입 정의
코드 예시
// services/userService.ts
export const userService = {
getUser: async (id: string) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
},
};
// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query';
import { userService } from '@/services/userService';
export const useUser = (id: string) => {
return useQuery(['user', id], () => userService.getUser(id));
};
// pages/ProfilePage.tsx
import { useUser } from '@/hooks/useUser';
const ProfilePage = () => {
const { data: user } = useUser('123');
return <div>{user?.name}</div>;
};
장점
- 구조가 직관적이라 초보 개발자도 빠르게 적응 가능
- 같은 종류의 코드가 한 곳에 모여 검색·관리 편의성 높음
- 기존 프로젝트에 점진적으로 적용하기 쉬움
단점
- 프로젝트 규모가 커질수록 각 폴더 내 파일 수가 폭발적으로 증가
- 기능 단위 응집도가 낮아 특정 기능 수정 시 여러 폴더를 횡단해야 함
- 파일 간 의존 관계 규칙이 없어 순환 참조 발생 가능성 높음
적합한 상황
- 소~중형 프로젝트 또는 초기 스타트업
- 팀 내 아키텍처 경험이 부족한 경우
- 빠른 개발 속도가 필요한 프로토타입
4. Atomic Design
개요
Brad Frost가 2013년 제안한 UI 컴포넌트 설계 방법론이다. 자연계의 원소(Atom)에서 분자(Molecule), 유기체(Organism)로 구성되는 원리를 UI에 대입한다. UI 컴포넌트 계층화에 특화된 방법론으로, 코드 전체 구조보다는 컴포넌트 설계에 초점을 맞춘다.
5단계 계층
| 단계 | 설명 | 예시 |
|---|---|---|
| Atoms | 더 이상 분해할 수 없는 최소 UI 단위 | Button, Input, Label, Icon |
| Molecules | 2개 이상의 Atom 조합 | SearchInput (Input + Button) |
| Organisms | Molecules + Atoms 조합의 독립적 UI 섹션 | Header, LoginForm, ProductCard |
| Templates | 실제 데이터 없이 레이아웃만 정의한 페이지 뼈대 | MainLayout, DashboardTemplate |
| Pages | Template에 실제 데이터가 주입된 최종 페이지 | HomePage, ProductPage |
폴더 구조
src/
└── components/
├── atoms/
│ ├── Button/
│ ├── Input/
│ └── Icon/
├── molecules/
│ ├── SearchBar/
│ └── FormField/
├── organisms/
│ ├── Header/
│ └── ProductCard/
├── templates/
│ └── MainLayout/
└── pages/
└── HomePage/
장점
- 디자이너-개발자 간 소통에 유리한 공통 언어 제공
- 컴포넌트 재사용성 극대화
- Storybook과 결합 시 컴포넌트 카탈로그 구축 용이
- 디자인 시스템 구축에 최적화
단점
- Atom/Molecule/Organism 경계 기준이 팀마다 달라 일관성 유지 어려움
- 비즈니스 로직 배치 기준이 명확하지 않음
- Organism이 커질수록 단일 책임 원칙 위반 가능성 증가
- UI 설계에는 강하지만 상태 관리, API 레이어 설계 지침이 없음
적합한 상황
- 디자인 시스템을 구축하는 팀
- 컴포넌트 라이브러리 개발 프로젝트
- 디자이너와 긴밀히 협업해야 하는 환경
5. DDD (Domain-Driven Design, 도메인 주도 설계)
개요
Eric Evans가 2003년 제안한 소프트웨어 설계 철학으로, 원래는 백엔드에서 주로 사용되었으나 복잡한 프론트엔드 비즈니스 로직 처리에도 적용 가능하다. 핵심은 비즈니스 도메인의 언어와 개념을 코드에 그대로 반영하는 것이다.
핵심 개념
- Bounded Context: 특정 도메인 모델이 유효한 경계 (예: 주문, 결제, 사용자 각각의 독립 영역)
- Entity: 고유 식별자(ID)를 가진 객체 (예: User, Order)
- Value Object: 식별자 없이 값 자체로 동등성을 판단하는 객체 (예: Money, Address)
- Repository: 도메인 객체의 영속성 관리 추상화 계층
- Domain Service: 특정 Entity에 속하지 않는 비즈니스 로직
폴더 구조 (프론트엔드 적용 예)
src/
├── domain/
│ ├── user/
│ │ ├── User.ts # Entity
│ │ ├── UserRepository.ts # Repository 인터페이스
│ │ └── UserService.ts # Domain Service
│ ├── order/
│ │ ├── Order.ts
│ │ ├── OrderItem.ts
│ │ └── Money.ts # Value Object
│ └── payment/
├── infrastructure/ # API, LocalStorage 등 외부 시스템 연결
│ └── api/
│ └── UserApiRepository.ts
└── presentation/ # UI 레이어
└── pages/
코드 예시
// domain/order/Money.ts — Value Object
export class Money {
constructor(
private readonly amount: number,
private readonly currency: string
) {}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return `${this.amount.toLocaleString()} ${this.currency}`;
}
}
// domain/order/Order.ts — Entity
export class Order {
constructor(
public readonly id: string,
private items: OrderItem[],
private status: OrderStatus
) {}
getTotalPrice(): Money {
return this.items.reduce(
(total, item) => total.add(item.getPrice()),
new Money(0, 'KRW')
);
}
canCancel(): boolean {
return this.status === OrderStatus.PENDING;
}
}
장점
- 복잡한 비즈니스 로직을 도메인 모델로 명확히 표현
- 기획자/디자이너와 공통 언어(Ubiquitous Language) 공유 가능
- 비즈니스 규칙이 UI나 인프라 코드와 분리되어 테스트 용이
- 마이크로프론트엔드 구조와 자연스럽게 결합 가능
단점
- 학습 곡선이 매우 가파름
- 간단한 CRUD 중심의 앱에서는 과도한 복잡성 유발
- 객체지향 설계에 대한 깊은 이해 선행 필요
- 순수 함수형 패러다임(React의 기본 철학)과 이질감이 있을 수 있음
적합한 상황
- 복잡한 비즈니스 규칙이 많은 엔터프라이즈 애플리케이션
- 마이크로프론트엔드 아키텍처 도입 시
- 백엔드 팀과 도메인 모델을 공유해야 하는 프로젝트
6. MVC / MVVM
개요
MVC(Model-View-Controller)는 소프트웨어의 오랜 설계 패턴으로, 코드를 세 가지 역할로 분리한다. 프론트엔드에서는 React/Vue의 등장 이후 MVVM(Model-View-ViewModel) 형태로 진화했으며, 많은 프레임워크가 이 패턴을 암묵적으로 따른다.
MVC 구조
Model — 데이터와 비즈니스 로직 (상태, API 응답)
View — 사용자에게 보이는 UI
Controller — 사용자 입력을 받아 Model을 업데이트하고 View를 갱신
MVVM 구조 (프론트엔드 관점)
Model — 원시 데이터, API 응답 타입
View — JSX/Template (순수 렌더링)
ViewModel — 상태 관리 + 비즈니스 로직 (useXxx 훅, Pinia store, Vuex 등)
코드 예시 (React MVVM)
// ViewModel — useProductViewModel.ts
export const useProductViewModel = (productId: string) => {
const [product, setProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchProduct = async () => {
setIsLoading(true);
const data = await productService.getById(productId);
setProduct(data);
setIsLoading(false);
};
const formattedPrice = product
? `${product.price.toLocaleString()}원`
: '-';
return { product, isLoading, formattedPrice, fetchProduct };
};
// View — ProductCard.tsx
const ProductCard = ({ productId }: { productId: string }) => {
const { product, isLoading, formattedPrice } = useProductViewModel(productId);
if (isLoading) return <Spinner />;
return (
<div>
<h2>{product?.name}</h2>
<p>{formattedPrice}</p>
</div>
);
};
장점
- 역할 분리가 명확해 단위 테스트 작성 용이
- 오랜 역사로 레퍼런스와 커뮤니티 자료가 풍부
- 프레임워크와 자연스럽게 통합 (Vue는 MVVM 기반)
- ViewModel 분리로 View 로직을 가볍게 유지 가능
단점
- 대규모 앱에서 Controller/ViewModel이 비대해지는 "Massive ViewModel" 문제
- 계층 간 데이터 흐름이 복잡해질 수 있음
- MVC/MVVM만으로는 폴더 구조 전체를 정의하기 어려움 (다른 패턴과 병행 필요)
적합한 상황
- Vue.js 기반 프로젝트 (MVVM 기본 철학)
- 비즈니스 로직과 UI를 명확히 분리하고 싶은 경우
- 테스트 커버리지가 높은 프로젝트
7. 종합 비교표
| 기준 | FSD | Layered | Atomic Design | DDD | MVC/MVVM |
|---|---|---|---|---|---|
| 학습 난이도 | 높음 | 낮음 | 중간 | 매우 높음 | 중간 |
| 코드 응집도 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 확장성 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| UI 재사용성 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 테스트 용이성 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 소규모 적합 | ❌ | ✅ | ✅ | ❌ | ✅ |
| 대규모 적합 | ✅ | ⚠️ | ⚠️ | ✅ | ✅ |
| 대표 프레임워크 | React, Vue | 공통 | React, Vue | React, Vue | Vue, Angular |
8. 어떤 걸 선택해야 할까?
프로젝트 규모별 추천
소형 프로젝트 (혼자 or 2~3명)
→ Layered Architecture 로 빠르게 시작. 필요 시 일부 Atomic Design 도입.
중형 프로젝트 (4~10명 팀)
→ FSD 도입을 적극 검토. 기능 단위 병렬 개발이 가능하고, 코드 리뷰 기준이 명확해진다.
대형 프로젝트 / 엔터프라이즈
→ FSD + DDD 조합. FSD로 폴더 구조를 잡고, 핵심 비즈니스 도메인에 DDD를 적용.
디자인 시스템 전담 팀
→ Atomic Design 이 최선. Storybook과 함께 컴포넌트 카탈로그 운영.
혼합 전략
실무에서는 단일 아키텍처만 사용하는 경우가 드물다. 대표적인 조합은 다음과 같다:
- FSD + Atomic Design: FSD의
shared/ui레이어를 Atomic Design으로 설계 - Layered + MVVM: 기본 계층 구조에 ViewModel 패턴으로 비즈니스 로직 분리
- FSD + DDD:
entities레이어에 DDD 패턴 적용
결론
아키텍처는 목적지가 아닌 수단이다. 가장 중요한 것은 팀 전체가 동의하고 일관되게 지킬 수 있는 규칙을 정하는 것이다. 완벽한 아키텍처를 찾다가 시작을 못하는 것보다, 간단한 구조로 시작해 팀의 성장에 맞게 점진적으로 개선하는 것이 현실적으로 더 효과적이다.