본문 바로가기
개발

[Architecture] 프론트엔드 클린 아키텍처: 도메인 로직과 UI 컴포넌트 분리하기

by 돌미나리는야생미나리 2026. 4. 27.

프런트엔드 프로젝트 초기에는 모든 것이 순조롭습니다. 하지만 컴포넌트가 수십 개로 늘어나고 비즈니스 요구사항이 복잡해지면, 하나의 컴포넌트 파일이 500줄을 넘어가기 시작합니다. 그 안에는 API 호출, 데이터 가공, 상태 관리, 그리고 UI 렌더링 로직이 뒤섞여 있어 작은 수정 하나에도 어디가 고장 날지 모르는 '스파게티 코드'가 되어버리곤 합니다.

이를 해결하기 위해 백엔드에서 주로 쓰이던 '클린 아키텍처(Clean Architecture)' 개념을 프런트엔드에 도입해야 합니다. 핵심은 하나입니다. "UI(어떻게 보여줄 것인가)와 도메인 로직(무엇을 할 것인가)을 철저히 분리하는 것"입니다.


1. 왜 프론트엔드에도 아키텍처가 필요한가?

프런트엔드 기술 스택은 매우 빠르게 변합니다. 리액트 버전이 올라가고, 스타일 라이브러리를 Tailwind에서 다른 것으로 바꾸거나, 상태 관리 도구를 교체해야 할 상황이 올 수 있습니다.

만약 비즈니스 로직(예: 할인율 계산, 장바구니 로직 등)이 특정 UI 컴포넌트나 라이브러리에 강하게 결합되어 있다면, 기술 스택을 바꿀 때 비즈니스 로직까지 모두 새로 짜야합니다. 클린 아키텍처는 기술적인 세부 사항(UI, 라이브러리)이 비즈니스 핵심 로직을 오염시키지 않도록 방어벽을 세우는 작업입니다.


2. 프론트엔드 레이어링: 3단계 계층 구조

프런트엔드 환경에 맞게 레이어를 크게 세 가지로 나눌 수 있습니다.

2.1 도메인 레이어 (Domain Layer / Entities)

가장 핵심이 되는 비즈니스 규칙과 데이터 모델이 위치합니다. 이 레이어는 리액트(React)나 특정 프레임워크에 전혀 의존하지 않는 순수 자바스크립트/타입스크립트 코드여야 합니다.

  • 예: calculateDiscount(price, coupon), validateEmail(email) 등

2.2 서비스/애플리케이션 레이어 (Service / Use Cases)

도메인 로직을 조합하여 실제 사용자 시나리오를 구현합니다. API를 호출하고 데이터를 도메인 모델로 변환하는 등의 역할을 수행합니다. 주로 Custom Hooks가 이 역할을 담당하게 됩니다.

  • 예: useCart, useAuth 등

2.3 프레젠테이션 레이어 (Presentation Layer / UI)

사용자에게 화면을 보여주고 입력을 받는 역할만 합니다. 로직은 최대한 배제하고, 서비스 레이어에서 제공하는 데이터와 함수를 연결하기만 합니다.

  • 예: Button, CartList, UserProfile 등

3. 실전 적용: 컴포넌트에서 로직 추출하기

배송비 계산 로직이 들어있는 장바구니 컴포넌트를 예로 들어보겠습니다.

3.1 Bad: 로직과 UI가 뒤섞인 경우

JavaScript
 
// 모든 로직이 컴포넌트 안에 있어 재사용과 테스트가 어려움
function Cart({ items }) {
  const totalPrice = items.reduce((acc, item) => acc + item.price, 0);
  const deliveryFee = totalPrice > 50000 ? 0 : 3000;

  return (
    <div>
      <p>총 합계: {totalPrice + deliveryFee}원</p>
    </div>
  );
}

3.2 Good: 도메인 로직과 서비스 레이어 분리

JavaScript
 
// 1. 도메인 로직 (Pure JS)
// /domain/cart.ts
export const calculateDeliveryFee = (totalPrice) => totalPrice > 50000 ? 0 : 3000;

// 2. 서비스 레이어 (Custom Hook)
// /services/useCart.ts
export function useCart(items) {
  const totalPrice = items.reduce((acc, item) => acc + item.price, 0);
  const deliveryFee = calculateDeliveryFee(totalPrice);
  return { totalPrice, deliveryFee, finalPrice: totalPrice + deliveryFee };
}

// 3. 프레젠테이션 레이어 (UI)
function Cart({ items }) {
  const { finalPrice } = useCart(items);
  return <p>총 합계: {finalPrice}원</p>;
}

이렇게 분리하면 calculateDeliveryFee는 UI 없이도 단독으로 유닛 테스트를 수행할 수 있고, 나중에 리액트가 아닌 다른 환경에서도 그대로 사용할 수 있습니다.


4. 클린 아키텍처 도입의 이점

  1. 유닛 테스트의 용이성: 복잡한 UI 렌더링을 신경 쓰지 않고 순수 함수 형태의 비즈니스 로직만 따로 떼어 테스트할 수 있습니다.
  2. 높은 가독성: 컴포넌트 파일만 봐도 이 화면이 어떤 구조인지 한눈에 들어옵니다. 세부 로직은 훅이나 도메인 파일에 숨겨져 있기 때문입니다.
  3. 유지보수성 향상: "배송비 정책이 바뀌었다"면 도메인 파일만 수정하면 됩니다. UI를 건드릴 필요가 없습니다.
  4. 팀 협업: UI 개발자와 로직 개발자가 역할을 나누어 동시에 작업하기 수월해집니다.

5. 주의할 점: 과유불급(過猶不及)

모든 곳에 클린 아키텍처를 적용할 필요는 없습니다.

  • 단순히 값을 보여주기만 하는 페이지나, 일회성 이벤트 페이지에 이런 구조를 도입하는 것은 과도한 엔지니어링(Over-engineering) 일 수 있습니다.
  • 프로젝트의 비즈니스 복잡도가 높고, 장기적으로 유지보수해야 하는 핵심 서비스(예: 쇼핑몰 결제, 대시보드 등)부터 단계적으로 적용하는 것을 권장합니다.