FSD 아키텍처 적용하기

Zero·2024년 5월 21일
2
post-thumbnail

들어가기에 앞서

복잡하고 거대한 프로젝트를 할 수록 빛을 보는 FSD 아키텍처는 팀원 모두가 제대로 알고있고 컨벤션과 같은 룰을 지켜야 제대로 사용할 수 있습니다.

이 글을 통해 FSD 아키텍처의 기본 개념과 이를 효과적으로 사용하는 방법을 학습할 수 있습니다.

✨ FSD 아키텍처에 대한 간단한 설명

FSD 아키텍처

FSD 아키텍처를 공부하거나 조사를 해보면 위 글을 질리도록 봤을 것입니다.

FSD 아키텍처는 다음 2가지만 지키면 됩니다.

  1. 계층구조

    Layer 위에서 아래로 참고 (ex: import) 가 가능한 것이다. 또한 하나의 레이어는 여러개의 Slices로 쪼개질 수 있고 또 Segments로 쪼개질 수 있습니다.

    해당 폴더구조는 혼자서 진행한 프로젝트에서 NextJS를 사용하기에 사용하는 폴더구조이다. 다소 특이한 점이 있다면 pages가 없고 views가 있다는 것이다. 왜냐하면 apppagesNextJS 고유의 파일 컨벤션으로 지정을 해두었기에 두개의 폴더를 같이 사용할 경우 원치 않는 결과를 초래할 수 있다.

  2. 공개 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를 나누는 방법

아마 가장 많이 고민하는 부분이 FSD 아키텍처의 Layer를 나누는 방법일 것입니다. 저도 혼자서 진행을 하면서 몇번을 폴더구조를 수정한지 모르겠습니다.

변경된 내역

아마 혼자서 하는 프로젝트가 아닌 여럿이서 협업하는 프로젝트였다면 욕하지 않았을까..?

각 파일은 역할에 맞는 폴더에 들어가야 합니다.

  • app: 애플리케이션의 최상위 로직이 초기화, 정의되는 곳을 말합니다. provider. router, global styles, global types 선언등이 여기에서 정의됩니다.
  • process: 여러 단계 플로우(예: 회원가입 절차)를 관리하는 복수의 페이지로 구성되어 있습니다.
  • pages (views): 사용자에게 제공되는 전체 뷰를 나타내는 실제 페이지들을 정의합니다
  • widgets: 페이지 내 사용되는 독립적인 컴포넌트입니다. 기능(features)과 연결되거나 데이터 객체(entities)를 다룹니다.
  • features: 애플리케이션의 핵심 비즈니스 로직을 담당합니다. ex: 로그인, 로그아웃, 버튼을 눌렀을 때의 동작
  • entities: 애플리케이션의 비즈니스 객체나 도메인 모델(사용자, 리뷰 등)을 정의합니다.
  • shared: 전반적으로 재사용되는 컴포넌트, 유틸리티, UI 컴포넌트 등을 포함합니다.

해당 역할 중 가장 헷갈렸던 부분이 widgets, features, entities 가 아닐까 싶습니다. 어느 정도로 분리를 해야하고 어느 기준에서 쪼개야 하는걸까?

제가 설명하고자 하는 것은 지극히 제 개인적인 생각이자 컨벤션이며 맞다, 틀리다를 얘기를 할 수 없습니다. 각자 프로젝트의 성격이나 팀의 컨벤션에 맞춰 따라가면 될 것 같습니다.

🎇 Widget과 Entities 분리하기

변경전 상품 리스트

위의 컴포넌트는 예시로 공개된 사이트에서 캡쳐한 컴포넌트로 어느 폴더에 저장되어 있는지 시각적으로 잘 보여주고 있습니다.

해당 컴포넌트에서는 상품들을 보여주는 컴포넌트 리스트가 있다면 해당 리스트를 widget으로 각각의 productCard들을 entities로 관리를 하고 있습니다.

저렇게 될 경우 BaseProductList에서 데이터를 불러오고 각각의 ProductCardProps로 넘겨주는 것을 생각할 수 있습니다.

// /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가 되는 것입니다.

🎇 Widget과 Features 분리하기

변경된 상품리스트

아까전에 Entitie를 분리하고 위에 컴포넌트들을 본다면 feature도 똑같이 보일겁니다.

데이터를 보내거나 기능을 가진 함수가 필요한 컴포넌트

-> /features/[도메인]/ui,

사용하는 모델(타입) 또는 Entitie의 타입을 가져다가 써도 됩니다.

-> /features/[도메인]/model

데이터를 보내거나 기능을 가진 함수(훅)

-> /features/[도메인]/lib

즉 데이터를 보내는(작성하는) 모델들과 함수, 컴포넌트가 저장이 되어있는 곳이 features가 되는 것입니다.

함수만 가져와서 컴포넌트에 Props로 전달해주는 코드들도 많이 봤는데 이왕 하는거면 entities와 동일하게 ui도 분리가 되면 좋지 않을까 싶어 해당 함수, 기능또한 widget위로 올라가는 것을 방지하였습니다.

👏 FSD 후기

FSD에 대한 자료를 찾아보면 외국 자료들이 많고 번역을 해오는데 있어서 사람들마다 해석이 다르고 설명이 달라 해석을하고 적용하는데에 있어서 어려운데에 있었습니다.

하지만 컴포넌트들간 역할이 분명하고 또 위와 같이 분리해서 작성할 경우 features, entities에 있는 ui 하나만으로도 api 통신이나 기능 작동이 가능하기 때문에 테스트 코드 작성에도 유용합니다.

아키텍처를 팀내 프로젝트에 적용을 시키려면 모든 인원이 사용법을 인지합니다. 또한 모두 똑같이 인지하고 있어야합니다. 때문에 적용을 하려면 같이 오랫동안 공부하는 팀원들과 사용해보는 것을 추천해드립니다.

다양한 의견을 통한 피드백은 환영입니다!

profile
0에서 시작해, 나만의 길을 만들어가는 개발자.

0개의 댓글