복잡하고 거대한 프로젝트를 할 수록 빛을 보는 FSD 아키텍처는 팀원 모두가 제대로 알고있고 컨벤션과 같은 룰을 지켜야 제대로 사용할 수 있습니다.
이 글을 통해 FSD 아키텍처의 기본 개념과 이를 효과적으로 사용하는 방법을 학습할 수 있습니다.
FSD 아키텍처를 공부하거나 조사를 해보면 위 글을 질리도록 봤을 것입니다.
FSD 아키텍처는 다음 2가지만 지키면 됩니다.
계층구조
Layer
위에서 아래로 참고 (ex: import
) 가 가능한 것이다. 또한 하나의 레이어는 여러개의 Slices
로 쪼개질 수 있고 또 Segments
로 쪼개질 수 있습니다.
해당 폴더구조는 혼자서 진행한 프로젝트에서
NextJS
를 사용하기에 사용하는 폴더구조이다. 다소 특이한 점이 있다면pages
가 없고views
가 있다는 것이다. 왜냐하면app
과pages
를 NextJS 고유의 파일 컨벤션으로 지정을 해두었기에 두개의 폴더를 같이 사용할 경우 원치 않는 결과를 초래할 수 있다.
공개 API
각 슬라이스와 세그먼트에는 공개 API가 있습니다. 공개 API는 index.js
또는index.ts
파일이며, 이 파일을 통해 슬라이스 또는 세그먼트에서 필요한 기능만 외부로 추출하고 불필요한 기능은 격리할 수 있습니다.
예시로는 다음과 같은 파일 구조가 있습니다.
/widgets
index.ts
Outlet.tsx
Header.tsx
Footer.tsx
사용하는 레이아웃을 보면 다음과 같이 사용하고 있습니다.
// @/widgets/Outlet.tsx
export default function Outlet({ children }: Props) {
return (
<>
<Header />
{ children }
<Footer />
</>
)
}
// /app/layout.tsx
export default function Layout({ children }: Props) {
return (
<main className={styles.container}>
<Outlet>{ children }</Outlet>
</main>
)
}
<Outlet/>
컴포넌트는 외부에서 사용을 하지만 <Header/>
와 <Footer/>
의 경우 외부에서 사용을 하지 않습니다. 따라서 index.js
혹은 index.ts
를 다음과 같이 작성할 수 있습니다.
// @/widgets/index.ts
export { Outlet } from './Outlet'
/*
외부에서 사용하는 다른 컴포넌트들
*/
이를 각각의 슬라이스와 세그먼트에서 사용함으로서 각각의 필요한 기능만 외부로 추출하고 불필요한 기능은 격리할 수 있습니다.
아마 가장 많이 고민하는 부분이 FSD 아키텍처의 Layer를 나누는 방법일 것입니다. 저도 혼자서 진행을 하면서 몇번을 폴더구조를 수정한지 모르겠습니다.
아마 혼자서 하는 프로젝트가 아닌 여럿이서 협업하는 프로젝트였다면 욕하지 않았을까..?
각 파일은 역할에 맞는 폴더에 들어가야 합니다.
provider
. router
, global styles
, global types
선언등이 여기에서 정의됩니다.해당 역할 중 가장 헷갈렸던 부분이 widgets
, features
, entities
가 아닐까 싶습니다. 어느 정도로 분리를 해야하고 어느 기준에서 쪼개야 하는걸까?
제가 설명하고자 하는 것은 지극히 제 개인적인 생각이자 컨벤션이며 맞다, 틀리다를 얘기를 할 수 없습니다. 각자 프로젝트의 성격이나 팀의 컨벤션에 맞춰 따라가면 될 것 같습니다.
위의 컴포넌트는 예시로 공개된 사이트에서 캡쳐한 컴포넌트로 어느 폴더에 저장되어 있는지 시각적으로 잘 보여주고 있습니다.
해당 컴포넌트에서는 상품들을 보여주는 컴포넌트 리스트가 있다면 해당 리스트를 widget
으로 각각의 productCard들을 entities
로 관리를 하고 있습니다.
저렇게 될 경우 BaseProductList에서 데이터를 불러오고 각각의 ProductCard에 Props
로 넘겨주는 것을 생각할 수 있습니다.
// /widget/main/ProductPopular.tsx
import { BaseProductList } from "@/widget/main
export function ProductPopular() {
return (
<section>
<h2>Featured Products</h2>
<BaseProductList />
</section>
)
}
// /widget/main/BaseProductList.tsx
import { ProductCard } from "@/entites/product
export function BaseProductList(){
const [productCardList, setProductCardList] = useState([]);
useEffect(()=> {
/*
데이터를 불러오는 함수
*/
setProductCardList(불러온 데이터)
}, [])
return (
<ul>
{productCardList.map(productCard =>
<ProductCard key={productCard.id} productCard={productCard}/>)}
</ul>
)
}
// /entities/product/productCard.tsx
import { ProductType } from "@/entieties/model
type Props = {
productCard: ProductType
}
export funtion ProductCard({ productCard }:Props){
/*
상품의 정보 컴포넌트
*/
}
저는 평소에 widget
과 같은 파일에 데이터를 불러오는 hook
함수가 있기에 코드가 길어지는 것을 싫어하였습니다. 때문에 다음과 같이 분리를 하였습니다.
// /widget/main/ProductPopular.tsx
import { BaseProductList } from "@/entities/product
export function ProductPopular() {
return (
<section>
<h2>Featured Products</h2>
<BaseProductList />
</section>
)
}
// /entities/product/BaseProductList.tsx
import { ProductCard } from "@/shared/product
import { ProductType } from "@/entieties/model
export function BaseProductList(){
const [productCardList, setProductCardList] = useState<ProductType>([]);
useEffect(()=> {
/*
데이터를 불러오는 함수
*/
setProductCardList(불러온 데이터)
}, [])
return (
<ul>
{productCardList.map(productCard =>
<ProductCard key={productCard.id} productCard={productCard}/>)}
</ul>
)
}
// /shared/ui/productCard.tsx
import { ProductType } from "@/entieties/model
type Props = {
productCard: ProductType
}
export funtion ProductCard({ productCard }:Props){
/*
상품의 정보 컴포넌트
*/
}
ProductCard의 경우 재사용을 고려하여 Shared로 분리를 하였습니다.
위와 같이 분리를 할 경우 애매했던 폴더 구조를 좀 더 가독성있게 분리를 할 수가 있었습니다.
즉 다음과 같은 기준으로 분리를 하게 됩니다.
데이터를 가져오는 함수가 필요한 컴포넌트
-> /entities/[도메인]/ui,
사용하는 모델(타입)
-> /entities/[도메인]/model
데이터를 불러오는 함수(훅)
-> /entities/[도메인]/lib
그러면 widgets
폴더에 들어가는 컴포넌트들은 따로 데이터를 불러오는 컴포넌트가 들어가지 않습니다.
즉 데이터를 불러오는 모델들과 함수, 컴포넌트가 저장이 되어있는 곳이 entities
가 되는 것입니다.
아까전에 Entitie
를 분리하고 위에 컴포넌트들을 본다면 feature
도 똑같이 보일겁니다.
데이터를 보내거나 기능을 가진 함수가 필요한 컴포넌트
-> /features/[도메인]/ui,
사용하는 모델(타입) 또는 Entitie
의 타입을 가져다가 써도 됩니다.
-> /features/[도메인]/model
데이터를 보내거나 기능을 가진 함수(훅)
-> /features/[도메인]/lib
즉 데이터를 보내는(작성하는) 모델들과 함수, 컴포넌트가 저장이 되어있는 곳이 features
가 되는 것입니다.
함수만 가져와서 컴포넌트에 Props
로 전달해주는 코드들도 많이 봤는데 이왕 하는거면 entities
와 동일하게 ui
도 분리가 되면 좋지 않을까 싶어 해당 함수, 기능또한 widget
위로 올라가는 것을 방지하였습니다.
FSD에 대한 자료를 찾아보면 외국 자료들이 많고 번역을 해오는데 있어서 사람들마다 해석이 다르고 설명이 달라 해석을하고 적용하는데에 있어서 어려운데에 있었습니다.
하지만 컴포넌트들간 역할이 분명하고 또 위와 같이 분리해서 작성할 경우 features, entities에 있는 ui 하나만으로도 api 통신이나 기능 작동이 가능하기 때문에 테스트 코드 작성에도 유용합니다.
아키텍처를 팀내 프로젝트에 적용을 시키려면 모든 인원이 사용법을 인지합니다. 또한 모두 똑같이 인지하고 있어야합니다. 때문에 적용을 하려면 같이 오랫동안 공부하는 팀원들과 사용해보는 것을 추천해드립니다.
다양한 의견을 통한 피드백은 환영입니다!