프론트엔드 작업을 하다보면 폴더 구조를 어떻게 잡을지 고민할 수 밖에 없게 됩니다.
하지만 대부분의 경우 페이지 단위로 코드를 짜는 경우가 많습니다. 아래처럼요.
그리고 hook과 context들이 같이 모여 있는데, 파일 타입에 따라서 구분 했다고 볼 수 있습니다.
그리고 view를 담당하는 컴포넌트들은 pages 하위에 위치 시키거나 components 하위에 위치시켰습니다.
사실 이런 방식의 아키텍처가 가장 흔합니다. 하지만 체계적으로 어떤 컴포넌트는 어디에 위치시킨다는 명확한 룰이 있는 것 같지도 않습니다. 시간이 지나고 프로젝트가 커지면서 지저분해질 수 밖에 없습니다.
가장 큰 문제점은 "Todo"라는 엔티티에 대한 코드가 여러 군데 퍼져 있다는 것입니다. 따라서 "Todo"라는 엔티티에 관해서 코드를 수정할 일이 있다면 흔히들 말하는 산탄총 수술이 될 가능성이 높습니다.
이를 보완하는 방법으로 Feature 단위로 폴더 구조를 나누는 것이 있습니다. 보통 Screaming Architecture, 혹은 Simple Modular Architecure 정도로 불리는 것 같습니다. 예시를 한번 보겠습니다.
이런식으로 피처 단위로 깔끔하게 정리된 모습을 보면, '리액트 어플리케이션'이 아니라 '프로젝트 관리 어플리케이션'이라는 게 확 느껴집니다. 그래서 'Screaming' 이라는 별칭이 붙게 되었구요.
피처 단위로 깔끔하게 프로젝트가 정리 되었습니다. 그런데 한 가지 아쉬운 점은, 하나의 피처 내에서 계층구조가 없다는 것입니다.
"Todo"와 관련된 모든 view 컴포넌트, hook, context를 하나의 폴더에 때려 넣었는데, 뭔가 체계적인 느낌이 들지는 않습니다.
더불어, 여러가지 엔티티를 포괄하는 피처가 있다면 그건 또 어디다 위치 시킬지 고민이 됩니다..
이런 고민을 해결하기 위해 탄생 한 것이 Feature Sliced Design입니다.
기존에는 todo, projects, users 처럼 평등한 'feature'들만 존재 했다면, FSD에서는 수직으로 레이어를 한 번 더 나눕니다.
즉, 가장 아래의 shared부터 app이라는 프로젝트 전체의 엔트리 포인트까지 6개의 Layer가 있는 것입니다.
각각의 Layer는 독립적인 모듈이라고 할 수 있는 Slice로 구성되고, Slice는 필요에 따라 Segment로 나뉩니다.
* Shared: 전체 프로젝트의 비지니스 도메인과 상관 없는 재사용성 높은 코드. 주로 api, ui 컴포넌트로 구성됩니다.
* entities: 비지니스 엔티티(Todo)
* feature: 유저 인터랙션과 같이 비지니스 엔티티를 사용하는 프로젝트의 핵심적인 "기능"(Create-Todo, Remove-Todo...)
* Widget: page 단위에서 조합하기 이전에 Entity와 Feature를 의미있는 하나의 단위로 조합함
* Pages: 우리가 아는 그 페이지 맞습니다
* App: app 전체 셋팅, queryClient, normalize Css등이 위치
Slice는 FSD에서 가장 중요한 단위로, 하나의 모듈을 구성합니다. 따라서 레이어와 같이 통일된 이름이 없고, 비지니스 도메인에 다라서 프로젝트 마다 다르게 설정됩니다.
App과 Shared는 Slice를 가지고 있지 않습니다. App과 Shared는 비지니스 로직과 상관 없기 때문입니다.
Segment는 단순히 코드를 정리하기 위한 용도로, Slice 하부에 위치합니다.
주로 ui
, model
, lib
등이 쓰입니다.
FSD는 굉장히 엄격한 룰을 가지고 있고, 이에 대한 설명과 풍부한 예시를 제공하고 있습니다. 세부 디테일을 여기서 다 설명할 수는 없고, 중요한 점들만 짚고 넘어가겠습니다.
features/auth
와 features/filters
는 서로 참조할 수 없습니다.features/auth
는 shared/ui/button
을 참조할 수 있지만, 반대로는 안됩니다.Activity는 api, lib, ui 세그먼트로 이루어져 있습니다.
index.ts에서는 외부에서 참조 가능한 것들만 내보내고 있습니다. 이 외의 코드를 참조하거나, index.ts를 우회해서는 안됩니다.
import { Activity } from "@entities/activity/api/activity-api"; (x)
import { Activity } from "@entities/activity"; (o)
해당 룰을 방지 하기 위해서 lint rule을 추가하는 것이 좋습니다.