FSD(Feature Sliced Design)는 정말 유용할까?

2025-05-26
  • architecture
  • FSD

💡 왜 새로운 아키텍처가 필요했을까?

세부적인 컨벤션의 부재

프론트는 주로 hook, component, page와 같은 기준으로 코드를 크게 역할별로 나눠 분리하고 있고, 내가 담당했던 회사 프로젝트도 이러한 구조를 따르고 있었다.

개인 프로젝트를 하거나 간단한 사이드 프로젝트를 진행했을 때는 이와 같은 구조에서 불편함을 느끼지 않았는데, 프로젝트의 규모가 커질수록 이 구조에 대한 불편함을 느끼기 시작했다.

따라서 이런 질문을 팀원들에게 한 적도 있다.

기존에 사용하던 구조에서는 특정 도메인에서만 여러 군데 중복적으로 사용되는 함수를 분리할 곳이 마땅치 않았다. 세부적인 분리에 대한 컨벤션을 팀끼리 정했다면 명확했겠지만 그러지 않았기에 애매했고, 팀원들도 이에 대해 공감했었다.

처음 프로젝트를 분석할 때면 이미 세팅된 구조를 훑어보며 이 폴더에는 이런 파일을 넣으면 되겠구나 라는 깨달음을 얻게 된다.

그러나 기준이 명확히 정해져 있지 않다면 각 팀원마다 도출한 정의가 다를 수 있고, 파악하기도 애매한 경우가 있을 수 있다. 따라서 가끔 이 폴더에 이 파일이 왜 있는 거지? 라는 의문에 휩싸이기도 한다.

또는 명확한 기준이 존재하지 않아 분리하지 못해 컴포넌트에 쌓여가고 있는 코드를 보게 되기도 한다. 기능을 추가할수록 이런 애매모호함 때문에 고민이 깊어졌기에 서로 다른 기준들을 어떻게 통일시킬 수 있을지 고민했다.

📂src/
└── 📂components/
    ├── 📂book-list/
    └── 📂book-detail/

특히 프로젝트 규모가 클수록 components가 매우 비대해지기에 회사 프로젝트는 페이지 기준으로 거기서 한 번 더 폴더를 분리하고 있었다.

책 목록 페이지는 book-list, 책 상세 페이지는book-detail와 같은 느낌이다. 그러면 book-detailbook-list에 공통으로 사용되는 UI는 어디에 넣어야 할까?란 고민이 커지기 시작했다.

외에도 기존에 컴포넌트 코드 분리 기준이 세부적으로 존재하지 않았기 때문에 작업자가 신경 쓰지 않는다면 하나의 컴포넌트 안에 상태 관리 로직, 렌더링 관련 로직, 비즈니스 로직들이 모두 담기게 되었다.

따라서 컴포넌트가 갈수록 비대해지고 많은 책임을 지게 되기 일쑤였다. 잘 분리되지 않은 컴포넌트는 800줄을 넘는 경우도 있었다.

그렇게 지금의 분리 기준은 문제가 있어 라는 결론을 내렸지만, 그러면 어떻게 분리해야 할까? 라는 질문에 대한 해답을 찾기는 어려웠다. 우리 팀에게는 새로운 아키텍처가 필요했다.

결합도와 응집도

낮은 응집도

응집도란 서로 연관성이 있는 코드들이 얼마나 응집되어 있는가를 말한다.

따라서 그래프를 보았을 때 cohesion(응집도)이 낮은 경우 서로 색상이 다른 동그라미끼리 뭉쳐져 있거나, 서로 완전히 분리되어 따로 노는 모습을 볼 수 있다.

기존의 폴더구조는 하나의 도메인, 혹은 기능과 관련된 코드가 api, utils, hooks 등과 같은 폴더에 파편화되어 흩어지게 하기에 응집도가 낮다. 따라서 기능 작업과 관련된 코드들을 한 곳에서 확인할 수 있는 구조가 아니었기 때문에 맥락 파악이 더 어려워지게 됐고, 기능 작업 시에도 번거로움이 추가되었다.

높은 결합도

결합도란 서로 다른 모듈이나 컴포넌트가 얼마나 강하게 의존하는지를 나타낸다.

그래프상에서 Coupling(결합도)이 높은 경우 서로 다른 색상의 동그라미가 서로 강하게 연결된 모습을 볼 수 있다.

이런 경우 기능 간의 결합도가 높기 때문에 상태들과 비즈니스 로직이 얽히기에 디버깅이 점차 어려워진다. 그리고 하나의 기능이 수정되면 다른 기능에도 영향을 미치고, 다른 기능에 의존하기 때문에 이를 재사용하기도 어려워진다.

💡 FSD(Feature Sliced Design)란?

프로젝트가 커질수록 기존의 폴더구조로서는 컴포넌트의 암묵적인 연결 및 모듈의 복잡성 때문에 유지보수가 어려워진다. 따라서 기능 분할 설계를 통해 결합을 느슨하게 하고 응집력을 높이기 위해 FSD가 등장했다.

FSD는 Feature Sliced Design의 약자로 기존의 프론트엔드 아키텍처의 단점을 극복하고 변화하는 비즈니스 요구사항에 맞춰 빠르게 대응하기 위해 프로젝트를 좀 더 이해하기 쉽고 체계적으로 만드는 것에 목적을 둔다.

FSD는 크게 Layer, Slice, Segment라는 세 가지 계층으로 이루어진다. 각각의 계층을 세부적으로 살펴보자.

Layer

Layer는 최상의 계층으로써 애플리케이션이 첫 번째로 분리가 되는 단계이다.

앞서 본 그림과 같이 app, proceess, pages, widgets, features, entities, shared로 나뉜다. process는 더 이상 사용되지 않기에 총 6개의 layer가 존재한다. 상위 layer로 갈수록 비즈니스 로직이 심화되고, 하위 layer로 갈수록 추상화가 심화되는 구조를 갖고 있다. 필요에 따라 layer는 생략될 수 있다.


  1. app

    • 애플리케이션의 전체적인 설정과 초기화를 담당
    • ex) routing, entrypoints, global styles, providers
  2. pages

    • 실제 페이지 단위 컴포넌트
    • ex) 홈페이지, 상품 목록 페이지, 마이페이지
  3. widgets

    • 여러 기능이나 UI 요소를 독립적인 단위로 조합한 컴포넌트
    • ex) 헤더 네비게이션, 상품 검색 필터, 댓글 위젯, 장바구니 미니 위젯, UserProfile, IssuesList
  4. features

    • CRUD가 아닌 특정 행위(사용자 액션)를 처리하기 위한 비즈니스 기능을 포함
    • 정책에 따른 비즈니스 로직, 기획에 따라 수정될 수 있는 로직 포함
    • ex) 상품 정렬 기능, 좋아요 버튼, 리뷰 작성 폼, AddToCart, SendComment
  5. entities

    • 프로젝트가 다루는 비즈니스 엔티티에 대한 UI 및 CRUD 코드
    • 부수 효과가 없는 순수 함수와 비즈니스 데이터 모델, 타입 정의 등
    • 기획이 변해도 변하지 않을 데이터 기반의 코드
    • ex) 상품(Product) 모델, 사용자(User) 모델, 주문(Order) 모델, 리뷰(Review) 모델
  6. shared

    • 다른 layer에서 공통으로 사용할 수 있는 유틸리티와 UI 컴포넌트
    • 비즈니스 로직을 포함하지 않음
    • ex) UI 버튼, 인풋 컴포넌트, 날짜 포맷팅 유틸, API 클라이언트, 타입 정의

FSD는 계층 구조로 되어있다. 각 계층은 상위 계층에서 하위 계층으로 의존성을 갖는다. 즉, app은 pages에 의존하고, pages는 widgets에 의존하는 식이다. 그러나 반대로 하위 계층에서 상위 계층으로 의존하는 것은 금지된다.

예를 들어, widgets는 pages에 의존할 수 있지만, pages는 widgets에 의존할 수 없다. 이러한 구조는 한 방향으로만 향하는 선형적 흐름을 유지함으로써 의존성을 명확히 하고 결합도를 낮추며, 변경이 일어날 때 영향을 최소화시킬 수 있다. 코드를 수정하더라도 직접적인 참조 관계에 있는 하위 계층에만 영향이 가게 된다.

Slice

애플리케이션 분해의 두 번째 단계로, Layer를 더 세부적으로 나누는 단계이며 도메인 단위로 기능을 분리하는 것을 의미한다. slice의 이름은 따로 규정되어 있지 않으며 user, post와 같이 그룹화된 도메인에 따라 달라진다.

App과 Shared는 Slice로 분리하지 않고 segment로만 분리된다. App은 애플리케이션의 설정과 초기화를 담당하기 때문에 Slice로 분리할 필요가 없고, Shared는 다른 Layer에서 공통으로 사용되는 유틸리티와 UI 컴포넌트이기 때문에 Slice로 분리하지 않는다.

밀접한 slice의 경우 디렉토리 내에 그룹 지을 수 있지만, 다른 slice와 같이 직접적으로 코드를 공유해서는 안 된다.

다른 slice를 참조하면 안 된다는 규칙은 높은 응집도와 낮은 결합도를 달성하기 위함이다.

slice는 도메인 단위로 그룹화되는데 다른 slice를 참조하지 않는다면, slice가 담당하는 도메인에 대해서만 단독으로 관리할 수 있기에 다른 도메인에 영향을 주지 않을 수 있다.

또한, slice 간의 의존도가 최소화되어 결합도가 낮아지기에 slice에 대해 변경이 일어나도 다른 slice에 영향을 주지 않게 된다.


Segment

Segment는 Slice를 더 세부적으로 나누는 단계로, 도메인을 기준으로 기능 단위로 분리하는 것을 의미하며 슬라이스 내의 코드를 나누는데 도움이 된다. 보통 기능 단위로 이름을 짓는데 segment의 이름을 규정되어 있지 않기 때문에 팀마다 다른 네이밍을 사용할 수 있다.

  • api: 백엔드와의 상호작용. (request 함수, 데이터 타입, mapper 등.)
  • ui: UI 관련된 모든 것 (UI 컴포넌트, 날짜 포맷터, 스타일 등)
  • model: 비즈니스 로직, 즉 상태와의 상호 작용. (스키마, 인터페이스, 스토어, 비즈니스 로직, actions 및 selectors)
  • lib: slice 안에 있는 다른 모듈이 필요로 하는 라이브러리 코드. util 함수의 성격.
  • config: 설정 파일과 기능 플래그. (자주 사용되지는 않음)
    • 기능 플래그: 코드 배포와 기능의 출시를 분리할 수 있는 ON/OFF 스위치
  • consts: 필요한 상숫값들.

하나의 slice 내부에 api, ui, model과 같은 segment를 응집도 있게 모아 관리하기 때문에 작업자가 도메인과 관련된 전체적인 맥락들을 파악하기 용이해진다. 이러한 구조를 통해 각 segment는 slice 내에서 독립적으로 관리되며, 비즈니스 로직이 분리되기에 낮은 결합도로 코드를 관리할 수 있다.

💡 도입 경험

너무 잘게 쪼개는 게 아닐까?

FSD는 항해 플러스 교육 과정에서 과제로 사용해본 적이 있고, 또 회사 프로젝트에도 도입해본 경험이 있다. 항해 플러스 과제에서 처음 FSD 구조로 리팩토링했을 때는 작은 애플리케이션이었기에, 불필요하게 코드를 많이 나눈다는 생각이 들었다.

6개의 layer로 분리해야 했기에 작은 코드 단위를 쪼개고 쪼개니 있으니 오히려 로직을 파악하기가 어렵다는 생각이 들었고, 불필요한 파일들과 컴포넌트가 많아지는 느낌이었다. 따라서 FSD의 필요성에 대해 확신하지 못했다.

entities, features, widgets의 경계는 모호해 보였기에 셋을 하나의 단계로 합쳐 관리하고 싶다는 생각이 들었으며, 결합도는 낮아진 것 같지만 응집도까지 오히려 낮아진다는 생각이 들었다.

FSD가 필요한 시점

회사 내에서 다루고 있는 프로젝트들의 경우 과제로 받았던 프로젝트보다 더 복잡하고 관리해야 할 코드가 많다. 또한 글의 초반부에 말한 것처럼 기존 구조에 대한 문제가 기능을 추가할 때마다 심화되고 있었기에 새로운 방법론이 필요하다고 생각하고 있던 와중이었다. 당시의 상황은 오히려 제대로 분리되지 않아서 문제였고, 분리를 위한 컨벤션이 정의되지 않아서 문제였다.

따라서 이에 대한 해결책으로 FSD를 떠올리게 됐다. FSD는 규모가 큰 프로젝트에 유용한 방법론이었고, 팀끼리 코드 일관성을 맞추기에 좋은 가이드가 되어줄 수 있다고 생각했다. 프로젝트의 규모가 커짐에 따라 역할을 기준으로 관련 파일들이 파편화되어 이를 추적하는 게 점점 어려워지던 상황이었으며, components, stores, utils 하위에 있는 폴더를 나누는 기준도 명확하지 않았다. 경우에 따라서는 하위 폴더를 페이지 단위로 나누는 방식 때문에 로직들을 관리하기가 더 곤란해지던 상황이었다.

도입할 때 겪은 우여곡절

도입할 때는 팀원들과 FSD에 대한 개념을 일치시켜야 했기에 그 과정에서 조금 어려움을 겪었다. 각 계층의 역할이 명확하게 정의되어 있지만, 팀원마다 이해하는 방식이 다를 수 있다. FSD 공식 문서에 있는 examples를 보더라도 각각의 프로젝트가 다른 분리 기준을 가지고 있음을 확인할 수 있다.

FSD의 규칙을 벗어나지 않는 전제하에 팀 모두가 합의할 수 있는 개념을 일치시키는 과정을 거친다면, FSD는 유용한 아키텍처가 될 수 있다. 컨벤션을 맞출 때 도움이 된 글features + auth + ui = ?와 같이 어떤 컴포넌트, 함수가 해당할지 예상해 보는 문제들이 있는데, 이를 통해 서로의 합의점을 맞춰나가는 과정이 조금 더 수월했다.

논의했던 내용을 바탕으로 가이드 및 컨벤션 문서를 작성해서 팀 문서에 올려두었고, 이를 기반으로 이후의 작업을 처리했다.

리팩토링 과정

프로젝트의 규모가 크기 때문에 한번에 FSD를 적용하기는 어렵다. 따라서 부분적으로 도입하기 시작해야 한다. 새로 추가하는 기능을 FSD로 작업한다거나, app, shared, pages 같이 옮기기 쉬운 layer부터 작업하면 좋다. 내 경우 따로 src 하위에 fsd라는 폴더를 만들어서 하나씩 옮기는 작업을 했다.

특히 경계가 모호하다고 여겨지는 entities, features, widgets의 경우에는 먼저 entities를 나누고 이 외의 경계가 필요하다고 여겨지면 features를 나누고, widgets를 나누는 방식으로 도입해 보는 게 좋다. 필요성에 대해 의심이 들고, 경계가 모호하게 느껴진다면 굳이 나누지 않아도 된다.

리팩토링하고 나서는 추후에 기능을 추가할 때 특정 도메인 하위에 있는 폴더만 수정되어 git diff에 걸리는데 그게 응집도 있고 깔끔해서 좋았다. 보통 작업 후에는 여러 폴더에 변경 사항이 생겨 좀 더 복잡하게 느껴졌기 때문이다.

맺으며

FSD는 언제 어느 프로젝트에나 정답이 되지는 않는다. 프로젝트의 특성에 맞게 도입하는 것이 중요하다. 단순 MVP 프로젝트나 랜딩 페이지에 FSD를 적용하게 되면 프로젝트의 유지보수성이 역으로 떨어질 수가 있다. 따라서 도입을 위해서는 프로젝트의 규모와 복잡성을 고려하여 신중히 도입하는 것이 필요할 것 같다.

Profile picture

박세리

Frontend Developer