FSD 관점으로 바라보는 코드 경계 찾기

teo.v·2024년 11월 19일
128

테오의 프론트엔드

목록 보기
48/48
post-thumbnail

프롤로그

이번 글의 주제도 FSD(Feature-Slided Design)입니다! 현재 제가 가장 관심있는 관심사 2가지 중 하나가 바로 이 FSD 네요. 지금 하고 있는 일이 레거시 코드를 최신 기술로 고도화하는 작업입니다. 그러다보니 새로운 개발보다는 리팩토링이 거의 대부분의 업무이고, 자연스레 코드를 잘 정리할 수 있도록 하는 것이 제일 중요한 고민거리가 됐습니다.

그리고 다른 하나는 당연히(?) AI입니다. AI 시대를 대비해 AI를 최대한 이용해서 코딩을 해보고자 Claude나 Copilot, Cursor등을 을 이용해서 개발을 시도해보고 있습니다. 일전에 AI가 대체하기 가장 어려운 직업 중에 하나가 청소부라고 했던 게 기억나는데요, 무엇이 쓸모있고 무엇이 쓸모없는지를 구분하는 건 대단히 어려운 기술이라고 하더라고요.

AI가 사람보다 뛰어난 능력을 가지고 있는데 참 아이러니하게도 이 코드 정리만큼은 AI가 잘 못하는 영역인 것 같아요. AI의 도움을 받아도 여전히 어려운 부분입니다. 코드 정리와 정돈은 아직까지 AI보다 개발자가 더 잘할 수 있는 영역이라 생각하며, 이러한 정리와 정돈의 체계적인 방법론이라는 점에서 FSD는 매우 매력적인 접근법이라고 생각합니다.

이러한 배경에서 FSD를 다양한 프로젝트에 적용해보게 되었습니다. 작은 프로젝트는 물론이고, 큰 프로젝트, 과제, 실험적인 프로젝트에 이르기까지 다양한 곳에서 FSD를 사용해보면서 얻은 실질적인 경험을 가이드로 공유하고자 이 글을 쓰게 되었습니다.

FSD를 처음 접하면 개념 자체이 상당히 멋지게 느껴지지만 막상 실제로 도입하려고 하면 "뭐가 Entity고, 뭐가 Feature일까? Widgets에는 뭘 넣어야 할까?"와 같은 모호한 경계 때문에 상당히 어려움을 겪게 됩니다. 저 뿐만이 아니라 FSD를 시도하시려는 사람들이 비슷한 고민을 하고 있다 생각합니다.

FSD 공식 사이트에 가봐도 딱 맞는 답을 주지 않습니다. 샘플을 보면 저마다의 해석이 다르고 구조가 제각각입니다. 이 글 역시 "이게 정답이다"가 아니라, 또 하나의 해석이자 "저는 FSD를 이렇게 적용해봤어요"라는 경험담을 바탕으로 이야기를 하지만 그래도 실질적인 가이드가 되어 줄 수 있을까 해서 작성해봅니다.


1부. FSD의 배경과 간단 설명

이 글을 통해 FSD를 처음 접하는 사람을 위해 간단하게 배경과 내용을 설명하려고 합니다. 더 자세한 내용은 제가 쓴 다른 글과 공식 홈페이지의 내용을 참고해보시면 좋아요.

프론트엔드 개발자 관점으로 바라보는 관심사의 분리와 좋은 폴더 구조 (feat. FSD)
https://velog.io/@teo/separation-of-concerns-of-frontend

코드를 '잘' 나누는 기술의 중요성

우리가 개발을 하다 보면 필연적으로 코드의 양이 커지는 순간이 옵니다. 처음에는 쉽게 작성되던 코드도, 양이 많아지면서 그 뒤에는 그 코드를 건드리지 않더라도 여러 문제가 발생하기 시작하죠.

경험적으로 대개 코드가 200~300라인을 넘어가면 변화가 몸으로 느껴지기 시작합니다. 스크롤을 계속 위아래로 움직여야 하고, 코드를 읽는 데 걸리는 시간도 점점 늘어납니다. 사람의 눈은 자동적으로 주변의 모든 것을 인지하려 하기 때문에 화면에 보이는 방대한 코드 그 자체만으로 방해 요소가 됩니다. 이는 결국 코드를 읽고 이해하는 속도 저하로 이어지게 되죠.

코드를 관리하기 쉽고 읽기 좋은 크기로 유지하려면 반드시 코드를 작게 분리해야 합니다. 그러나 무작정 분리한다고 해서 좋은 코드가 되는 것은 당연히 아니겠죠. 같은 관심사를 가진 코드는 한곳에 모아야 하고, 코드의 접근 범위와 의존성 순서도 신중히 고려해야 합니다. 또한, 분리된 코드를 어떤 이름으로, 어떤 폴더 구조에 배치할지도 매우 중요한 부분입니다.

아마 여러분들도 회사의 레거시 코드를 열었는 데 1000라인이 넘는 거대한 코드 파일을 마주치면 갑갑함을 느낄거에요. (지금 제가 그렇습니다. 😅) 또는 /component 폴더를 열었는데, 혹은 /utils 폴더를 열었는데 그 안에 파일이 100개가 넘어가도 답답함이 몰려옵니다.

이런 코드들을 의미 있게 나누고 구조화하는 것은 정말로 쉽지 않은 과제입니다. 프론트엔드 생태계가 성장하면서 수년간 진화(?)해온 거대한 레거시 코드들을 마주하는 일이 잦아졌고, 이러한 대규모 프로젝트에 맞는 체계적인 코드 구조화 방법론이 필요합니다.

역할별로 나누면 어떨까?

코드를 분리하고 관리하는 가장 일반적인 방법은 역할별로 나누는 것입니다. 컴포넌트는 /components에, 유틸리티 함수는 /utils에, API 관련 코드는 /api/services 폴더에 넣는 식이죠. 여기에 /hooks, /constants, /libs 등 다양한 역할을 하는 폴더들이 추가되기도 합니다.

이런 구조의 장점은 명확합니다. 코드가 하는 역할에 따라 분류되므로 일관된 기준이 생기고 코드를 분리하기가 쉽습니다. 역할별로 찾아가기도 쉬운 편이라 상당히 정돈된 느낌을 줍니다. 그래서 실제로 많은 스타터 패키지들이 이러한 구조를 기본으로 제공하고 있죠. 역할별로 분류된 구조는 코드 탐색과 이해를 용이하게 해주기에 적당한 규모의 프로젝트에서는 충분히 효율적입니다.

하지만 프로젝트의 규모가 커지기 시작하면 이 구조의 한계가 드러납니다. /components 폴더를 보면, 수십 수백 개의 컴포넌트가 한 폴더에 쌓이게 됩니다. 이런 상태에서 폴더를 다시 분리하려고 하면 또 다른 고민이 시작됩니다. ui 폴더로 나눠야 할지, pages 폴더로 둘지, 아니면 domain별로 나눠야 할지 고민하게 되죠. 이러한 고민은 프로젝트마다 다른 결론에 이르게 되고, 일관된 기준을 잃어버리기 쉽습니다.

결국 역할별 구조는 프로젝트가 커질수록 체계적인 기준을 유지하기 어려워집니다. 프로젝트가 성장하면서 일관성을 잃지 않고 확장 가능한 구조를 유지하려면 더 체계적이고 의미 있는 코드 구조화 방법이 필요합니다.

그렇다면 FSD는 뭐가 다른 걸까?

FSD(Feature-Sliced Design)는 프론트엔드 애플리케이션을 구조화하는 새로운 방법론입니다. 기존의 단순한 역할별 분류(components, utils 등)에서 벗어나, 애플리케이션을 더 체계적으로 구성할 수 있는 방법을 제시합니다.

기존의 역할별 폴더 구조는 중규모 프로젝트까지는 잘 동작하지만, 프로젝트가 커지면서 한계를 드러냅니다. 앞서 말했듯 코드베이스가 커지게 되면 /components, /hooks, /utils 폴더 등에 수많은 파일이 쌓이고, 이를 다시 분류하려고 하면 프로젝트마다 제각각의 구조를 가지게 됩니다.

이러한 현상이 벌어지는 이유는 ‘역할’이라는 단일 관점의 관심사만으로 코드를 구성했기 때문입니다. 실제로 프로젝트에는 역할 뿐만이 아니라 도메인, 기능, 데이터의 흐름 등 다양한 관심사가 존재하며, 이를 세밀하게 분리할 필요가 있습니다.

FSD는 이런 문제를 해결하기 위해 다음과 같이 세 가지 축으로 코드를 구조화하는 방식을 제안합니다.

  1. 레이어(Layer): 프로젝트 기능적 역할에 따른 수직적 관심사 분리
  2. 슬라이스(Slice): 비즈니스 도메인별 관심사 분리
  3. 세그먼트(Segment): 기술적 관심사 분리

이러한 구조는 미리 정해진 규칙에 따라 코드를 작성하도록 하여 두 가지 주요 장점을 제공합니다.

첫째, 다양한 관점의 관심사를 세밀하게 분리하여 각각의 관심사를 특정하여 분리할 수 있습니다.
둘째, 코드의 일관성과 탐색의 용이성을 높이는 명확한 컨벤션을 제공합니다.

이로 인해 프로젝트가 커지더라도 파일의 위치를 쉽게 예측할 수 있어 팀원 간 협업이 원활해집니다. 각 구분에 대해서 조금 더 자세히 한번 알아봅시다.

1. 계층적 구조 (Layer)

코드를 책임과 범위에 따라 다음과 같은 레이어로 구분합니다.

  • app: 애플리케이션의 전역 설정 (라우팅, 스토어 등). 예를 들어, 글로벌 스타일 설정이나 다국어 설정이 이 레이어에 포함됩니다.
  • processes: 페이지 간의 비즈니스 프로세스. 사용자 인증 흐름이나 결제 프로세스 같은 다중 페이지에 걸친 로직을 관리합니다.
  • pages: 사용자와 상호작용하는 실제 페이지 컴포넌트. 예를 들어, Homepage, ProductListPage 등이 여기에 속합니다.
  • widgets: 재사용 가능한 복잡한 UI 블록. 예를 들어, 헤더 네비게이션, 검색 필터, 댓글 위젯 같은 것이 있습니다.
  • features: 특정 비즈니스 기능을 담당. 좋아요 버튼, 리뷰 작성 폼, 상품 정렬 기능 등이 포함됩니다.
  • entities: 핵심 비즈니스 엔티티와 관련된 코드. 상품, 사용자, 주문 등 도메인 모델의 로직을 다룹니다.
  • shared: 공통 유틸리티와 컴포넌트를 포함. UI 버튼, 인풋 컴포넌트, 유틸리티 함수 등이 여기에 속합니다.

원칙: 상위 레이어는 하위 레이어를 사용할 수 있지만, 그 반대는 불가능합니다. 이 원칙은 각 레이어의 독립성과 안정성을 보장합니다.

2. 도메인별 구조 (Slice)

각 레이어는 비즈니스 도메인에 따라 나뉩니다. 이를 통해 프로젝트의 기능을 중심으로 코드를 그룹화할 수 있습니다.

  • 예시:
    • user: 사용자와 관련된 모든 코드(프로필 관리, 인증 등).
    • product: 상품과 관련된 코드(상품 리스트, 상세 페이지 등).
    • cart: 장바구니 기능.
    • order: 주문 관리 기능.

설명: 슬라이스는 각 도메인별로 코드를 나누어 유지보수를 쉽게 하고, 관련된 코드를 함께 둘 수 있도록 합니다.

3. 기술적 구조 (Segment)

각 슬라이스는 다시 기술적 관심사로 분리됩니다. 이를 통해 도메인의 코드가 명확하게 구조화되고 관리됩니다.

  • api: 외부 서비스와의 통신을 담당하며 API 엔드포인트와 데이터 변환 로직을 포함합니다.
  • config: 프로젝트 전반에 걸친 설정 값과 상수.
  • lib: 순수 함수와 도메인 특화 헬퍼 함수 모음.
  • model: 데이터 구조, 상태 관리, 비즈니스 로직.
  • ui: 순수 표현 컴포넌트. 데이터와 이벤트 핸들러를 받아 화면을 렌더링합니다.

이렇게 세 가지 축이 결합되어 다음과 같은 구조가 만들어집니다

src/
├── entities/
│   ├── user/
│   │   ├── api/
│   │   ├── model/
│   │   └── ui/
│   └── product/
│       ├── api/
│       ├── model/
│       └── ui/
├── features/
│   ├── auth/
│   │   ├── api/
│   │   ├── model/
│   │   └── ui/
│   └── cart/
│       ├── api/
│       ├── model/
│       └── ui/
...

FSD는 대규모 프로젝트에서도 일관성을 유지할 수 있도록 돕습니다. 코드를 작성할 때 미리 정해진 규칙에 따라 구성하기 때문에 코드의 위치가 예측 가능하며, 각 관심사가 명확히 분리되어 있어 유지보수도 수월합니다. 특히, 여러 개발자가 협업할 때 팀원들이 동일한 구조를 따르게 되어 코드 탐색과 이해가 훨씬 용이해집니다. FSD는 코드의 일관성, 유지보수성, 확장성을 높여 주는 현대적인 프론트엔드 프로젝트 구조화 방법론입니다.


2부. FSD 도입기

FSD라는 개념을 처음 접했을 때에는 신선하기도 했고 재미있어 보이기도 했고 폴더 구조에 대한 체계적인 구조에 대해서는 공감대가 있었기에 적용을 해보고 싶었습니다. 최근에 몇 안되는 프론트엔드의 주목받았던 키워드이기도 했구요.

개념자체는 어렵지 않았는데 막상 이걸 적용을 하려고 하니 상당히 헷갈렸습니다. 특히나 entitiy, features, widgets을 구분하는 것이 굉장히 어려웠죠. app와 shared에 두어야 하는 것들도 어떻게 하면 좋을지 쉽지 않았습니다. 경계가 명확하지 않다보니 어떻게 배치를 해야할지가 막막하더라구요.

어떻게 하면 경계를 조금 더 이해하기 접근할 수 있을까에 대해 고민을 했던 이야기입니다.

비교적 이해하기 쉬운 FSD Segment부터 접근해보자

우선 FSD(Feature-Sliced Design)를 더욱 깊이 이해하기 위해 각 용어의 특성을 자세히 살펴보았습니다. 이번 섹션에서는 FSD의 핵심 개념에 대해 함께 알아보고자 합니다. 그중에서 Segment는 전통적인 역할을 통해서 구분을 하는 방식과 유사하니 이들의 경계를 구분하는 건 어렵지 않습니다.

/ui

  • 두말할 것 없이 .tsx 파일, .vue, .svelte 등 컴포넌트 파일

/api

  • fetch를 이용해서 서버를 호출해야 하는 모든 행위
  • 드물지만 webSocket, WebRTC등의 외부 통신도 포함
  • 이들을 감싸서 활용하는 다른 모든 코드들

/model

  • 비즈니스 로직이 들어간 코드
  • 우리가 아는 일반적인 hook
  • 혹은 상태관리
  • 기타 도메인과 연관된 함수들

/lib

  • 꼭 우리 비즈니스에서 쓰이지 않아도 되는 유틸리티 함수들
  • model과 살짝 헷갈리는 구간이 있지만 함수의 인자에서 비즈니스 도메인을 쓰지 않는 함수로 구분

/config

  • 동작이 전혀 없는 상수들의 모음

사실 이렇게 구분하지 않더라도 직관적으로 5개의 성격이 극단적으로 다르기에 model와 lib의 살짝 애매한 경계(애매하다고 할 수 도 없지만)를 제외하고는 아주 쉽게 이해할 수 있었습니다.

// js에서는 model과 lib의 미묘한 경계이지만,
const addProductToCart = (cart, product) => [...cart, product]

const addItem = (arr, item) => [...arr, item]
// typescript를 쓰면 더 직관적으로 확연한 차이를 알 수 있다.
type Product = {id:string, name:string, price:number }
type Cart = Product[]

// model
const addProductToCart = (cart:Cart, product:Product) => [...cart, product]

// lib
const addItem = <T>(arr:T[], item:T) => [...arr, item]

경계를 선명히 하기

FSD를 도입하면서 가장 큰 고민은 7개의 레이어에서 각 세그먼트를 어떻게 구분할지였습니다. entities + api, features + model, widgets + ui와 같은 조합들은 각각 어떤 책임을 가져야 하고, 어떻게 명확히 분리할 수 있는지에 대한 고민을 불러일으켰습니다. "이게 과연 feature인가? entitiy인가?"하는 의문이 계속 반복되곤 했습니다.

FSD를 도입하면 코드가 더 명확하게 구분되어야 하며, 구분이 어려워진다면 본래의 목적에 어긋납니다.

이에 대한 고민 끝에, 폴더 구조나 규칙에 맞추어 코드를 억지로 나누기보다는 내가 평소 코드를 작성하면서 이미 자연스럽게 인식했던 경계들을 먼저 찾아보기로 했습니다. 코드를 작성하다보면 내가 자연스럽게 분리를 하고 있던 경계들이 있었기 때문이죠.

컴포넌트를 작성할 때 우리는 이미 여러 관점에서 컴포넌트를 구분하고 있었습니다. 단순히 UI를 렌더링하는 컴포넌트, 특정 도메인 데이터를 다루는 컴포넌트, 사용자 액션을 중심으로 설계된 컴포넌트 등이 그 예입니다. 이러한 구분은 우리가 이미 직관적으로 알고 있던 자연스러운 경계였습니다.

상태 관리에서도 명확한 경계가 존재합니다. React에서 우리는 종종 상태를 다루는 훅과 순수하게 계산만 담당하는 함수를 구분할 수 있습니다. 어떤 코드는 상태를 직접 변경하고, 다른 코드는 단순히 계산된 값을 반환합니다. 이러한 구분은 특히 함수형 프로그래밍 관점에서 명확합니다.

API와 관련된 코드에도 자연스러운 경계가 있습니다. 서버와의 통신을 담당하는 코드, 응답 데이터를 가공하는 코드, 그리고 그 데이터를 클라이언트 상태로 관리하는 코드들은 각기 다른 책임을 지니고 있습니다. 또한, 서버 상태와 클라이언트 상태를 구분하는 명확한 기준도 존재합니다.

도메인 코드도 자연스럽게 경계를 가집니다. 프로젝트 내에서만 의미가 있는 값, 비즈니스 규칙을 포함하는 코드, 그리고 공통 유틸리티 함수 등 다양한 요소들이 이에 해당합니다.

무엇보다, 프론트엔드 개발에서 중요한 자연스러운 경계는 데이터 흐름입니다. 데이터가 화면에 렌더링되고, 사용자의 행동이 일어나며, 그 행동이 다시 데이터의 변화로 이어지는 이 순환적인 흐름안에서 데이터 -> 화면, 화면 -> 행동 , 행동 -> 데이터 라고 하는 3가지의 경계들도 있죠.

이러한 자연스러운 경계들을 인식하자, FSD 구조가 훨씬 명확해졌습니다. 이제는 "이게 feature인가 entity인가?"를 고민하기보다 "이 코드가 어떤 책임을 지니고 있는가?"를 먼저 생각할 수 있게 되었습니다. 이미 알고 있던 경계들로 먼저 분리해두고 FSD에 억지로(?) 우겨넣다보니 더 체계적으로 정리할 수 있게 되었습니다.

세그먼트별 경계를 정리해보자!

이러한 경계를 바탕으로, layer와 관계없이 성격이 다른 코드들을 분리해보았습니다. FSD라고 특별히 다른 방법으로 나누기보다는, 본래 코드가 지녀야 할 자연스러운 방식대로 나누는 것이 최선이라 생각했습니다.

/ui 세그먼트

  1. 스타일과 레이아웃만 담당하는 순수 UI 컴포넌트
  2. 도메인 데이터를 표시하는 읽기 전용 컴포넌트
  3. 상태 관리와 사용자 액션을 처리하는 대화형 컴포넌트
  4. 여러 컴포넌트를 조합한 독립적 UI 블록
  5. 페이지와 레이아웃을 구성하는 최상위 컴포넌트

/model 세그먼트

  1. 도메인 타입과 인터페이스를 정의하는 타입 시스템
  2. 순수한 도메인 계산과 변환을 처리하는 순수 함수
  3. 상태 관리와 부수효과를 처리하는 커스텀 훅
  4. 복잡한 비즈니스 규칙과 다단계 프로세스를 처리하는 도메인 로직

/api 세그먼트

  1. 단일 리소스에 대한 기본 CRUD 작업을 처리하는 API 호출
  2. 여러 엔드포인트를 조합하여 데이터를 처리하는 복합 API 요청
  3. 캐싱과 낙관적 업데이트를 포함한 서버 상태 동기화
  4. 다단계 API 흐름과 에러 처리를 포함한 트랜잭션 관리

/lib 세그먼트

  1. 기본 데이터 타입을 다루는 순수 유틸리티 함수
  2. 도메인 특화 데이터를 처리하는 헬퍼 함수
  3. 복잡한 유효성 검사와 에러 처리를 수행하는 검증 로직
  4. 다단계 데이터 처리를 수행하는 파이프라인 유틸리티

/config 세그먼트

  1. 기본 상수값과 열거형을 정의하는 설정
  2. 환경별 설정값과 환경 변수 구성
  3. 도메인별 규칙과 제약조건 설정
  4. 조건부 설정과 동적 설정을 포함한 고급 설정 관리

3부. FSD 적용과 팀 컨벤션

이제는 이렇게 분리된 경계를 통해서 FSD가 제시하는 구분에 어떻게 적용을 할지 그리고 이 모든 과정을 어떻게 팀 내에서 함께 적용을 할지 고민을 해보았습니다.

FSD와 REST API의 인사이트

가급적 LayerSegment 에서 사용되는 이름은 변형하거나 늘리지 말고 그대로 써주세요.

FSD(Feature-Sliced Design)를 깊이 이해하고 적용하다 보니 문득 REST API가 발전해온 과정과 유사하다는 느낌을 받았습니다. 지금은 국룰이 된 서버 API의 작명법인 REST API이지만 API의 모습이 처음부터 지금의 형태는 아니었습니다. 초기에는 API 엔드포인트의 이름을 마치 함수처럼 지었던 때가 있었죠.

POST /createUser
POST /getMyProfile?id=12
POST /updateUserPost?id=22
POST /deleteComment?id=12

이런 동사 기반의 엔드포인트명은 직관적으로 보일 수 있지만, API가 늘어날수록 일관성을 유지하기가 어려웠습니다. 게다가 회사마다 팀마다 이름이 다르다보니 이 API가 무엇을 하는지 예측하기가 쉽지 않았습니다.

REST 아키텍처가 제시한 해결책은 단순했습니다. 매번 새로운 이름을 짓기보다는 동사는 HTTP 메서드로 고정하고, 엔드포인트는 명사(리소스)만으로 구성하자는 것이었죠. 리소스는 대개 이름이 원래부터 있으니까요. 그래서 다음과 같은 모양을 하게 되었습니다. 그리고 이 규칙으로 인해서 우리는 대부분의 API를 모양만 보고도 쉽게 예측할 수 있게 되었습니다.

POST   /users
GET    /users/me
PUT    /posts/{id}
DELETE /comments/{id}

무슨 말을 하고 싶은지 느낌이 오셨을까요? 마치 REST API가 HTTP 메서드와 리소스의 조합으로 명확한 구조를 만들어냈듯이, FSD도 마치 가운데 slice(=resource)를 기준으로 앞뒤의 정해진 이름을 붙이고 이를 조립해서 어떤 파일이 있을지 예측 가능한 구조를 만들어내고 있었습니다.

entities/user/api
features/auth/model
widgets/header/ui

이러한 유사성을 발견한 후, FSD의 구조를 이해하는 것이 훨씬 쉬워졌습니다. REST API에서 GET /users를 보면 자연스럽게 "사용자 목록 조회"라고 이해되듯이, features/cart/ui를 보면 "장바구니 관련 사용자 인터페이스 컴포넌트"라고 직관적으로 이해할 수 있게 됩니다.

FSD 상상 놀이를 통한 팀 컨벤션 만들기

폴더 구조는 팀 컨벤션의 역할을 하기에 예측가능해야 하고 모두의 합의가 필요합니다.

  • "features/cart/ui는 어떤 코드가 들어가야 할까?"
  • "entities/product/model에는 무엇이 있어야 하지?"
  • "widgets/header/api는 어떤 모습이어야 할까?"

FSD의 공식 홈페이지에서도 그리고 샘플에서도 저마다의 생각들이 다르고 적용하는 방식들이 달랐습니다. 결국 FSD는 이름을 통해서 예측가능한 구조를 만들어내는 것이 목표이고 폴더 이름과 구조를 고민하는 에너지를 덜어주는데 목적이 있었습니다.

그렇기에 고민하지 않고 폴더구조를 작성하고 모두의 예측에 맞게 적용하기 위해서는 다 같이 resource와 함께 layer와 segment가 조립이 되면 어떤 파일들이 있을지 상상해보고 맞춰가는 방식을 통해서 팀의 컨벤션을 만들어 낼 수 있었습니다. 정해진 정답이 있는게 아니라면 우리 팀의 모두가 함께 상상 가능한 것이 예측가능하고 일관성이 있는 답이 될테니까요.

아래 폴더에는 어떤 파일들이 들어가 있을지 상상해보세요.

1 features + auth + ui = ?
2 features + cart + model = ?
3 features + payment + api = ?
4 features + search + ui = ?
5 features + notification + lib = ?
6 features + profile + model = ?
7 features + product + model = ?
8 features + review + ui = ?
9 features + order + api = ?
10 features + user + config = ?
11 entities + auth + api = ?
12 entities + cart + model = ?
13 entities + payment + api = ?
14 entities + search + lib = ?
15 entities + notification + ui = ?
16 widgets + auth + ui = ?
17 widgets + cart + model = ?
18 widgets + payment + ui = ?
19 widgets + search + lib = ?
20 widgets + notification + ui = ?
21 features + product + ui = ?
22 features + order + model = ?
23 features + review + api = ?
24 features + profile + ui = ?
25 features + notification + model = ?
26 entities + order + api = ?
27 entities + user + model = ?
28 entities + profile + lib = ?
29 entities + review + config = ?

느낌가는대로 각 레이어들의 감각 정해보기

app과 shared는 공용으로 쓰는 녀석
그런데 app은 우리 프로젝트에서만 쓸모있고
shared는 다른 프로젝트에서도 재사용이 가능한 느낌

entities는 data, read, pure, query, render 이런 느낌, 수정되지 않을 것 같고 원본 그자체
features는 action, write, mutation, state, store 이런 느낌, 사용자의 행동과 상태변경
widgets은 이렇게 딱 떼어놔도 위화감 없는 블록. 건드는 거 없이 붙였다 뗐다가 가능해야 됨.
pages는... 뭐 그냥 pages

뭔가 대충 이런 느낌적인 느낌?

너무 구체적으로 정하지는 않더라도 대강 느낌가는대로 이야기를 주고 받으며 어느정도 그림으로 타협을 보았습니다.

그렇게 정해진 우리 맘대로 FSD의 경계

NOTE: 어디까지나 주관적 해석이며 적용 이후에 기준은 언제든 달라질 수 있습니다. 참고용으로만 봐주세요. 하지만 나중에 FSD에 대한 해석이 달라진다 하더라도 코드를 나누는 기준은 바뀌지 않을 거라 생각합니다. 코드만 잘 분리되었다면 폴더 이동과 이름 바꾸기는 아주 쉬운 리팩토링에 속한다 생각합니다.

App 레이어

  • 목적: 프로젝트 내에서만 사용되는 전역적인 공통 컴포넌트나 설정을 포함.
  • 사용 예: AppProvider, AppTheme, 프로젝트 전역에 걸쳐 사용되는 레이아웃이나 상태 관리 설정.
  • 핵심 특징: 프로젝트 전용, 프로젝트 내부에서만 사용됨.
  • app + ui: 프로젝트 전반에 걸친 레이아웃이나 테마 설정 UI (예: AppLayout).
  • app + api: 프로젝트 전용 초기 API 설정 (예: initApiClient).
  • app + model: 프로젝트 상태 관리와 관련된 공통 훅이나 타입 정의 (예: useAppState).
  • app + lib: 프로젝트 전용 유틸리티 함수 (예: initializeApp).
  • app + config: 전역 프로젝트 설정 (예: APP_NAME, DEFAULT_LANGUAGE).

Shared 레이어

  • 목적: 여러 프로젝트에서도 재사용 가능한 공통 컴포넌트, 유틸리티, 설정을 포함.

  • 사용 예: Button, Modal, useFetch 같은 재사용 가능한 요소나 debounce 같은 유틸리티 함수.

  • 핵심 특징: 프로젝트 안팎으로 재사용 가능, 범용성이 높음.

  • shared + ui: 프로젝트 전반에 걸쳐 사용되는 재사용 가능한 UI 컴포넌트 (예: FormField, Card).

  • shared + api: 공통 API 설정 또는 유틸리티 (예: Axios 인스턴스 설정).

  • shared + model: 공통 기능을 제공하는 훅 (예: useAuth).

  • shared + lib: 반복적인 작업을 위한 유틸리티 함수 (예: formatDate, deepClone).

  • shared + config: 전역 상수 (예: 반응형 브레이크포인트, 색상).

Entity 레이어

  • 목적: 도메인 데이터 중심의 독립적인 요소를 표현.

  • 사용 예: 순수 함수, 데이터 모델, 타입 정의, 기본 CRUD 연산.

  • 핵심 특징: 순수성; 간단하고 독립적이며 데이터 중심적임. 상태를 관리하지 않으며 부작용이 없음.

  • entity + ui: 읽기 전용 프레젠테이셔널 컴포넌트 (예: ProductDisplay).

  • entity + api: 기본 데이터 페칭 함수.

  • entity + model: 비즈니스 로직과 타입 정의.

  • entity + lib: 엔터티 전용 유틸리티 함수.

  • entity + config: 엔터티별 상수나 설정 값.

Feature 레이어

  • 목적: 특정 행동과 사용자 상호작용에 관련된 기능을 포함.

  • 사용 예: 상태를 포함한 컴포넌트, 비즈니스 로직을 담은 커스텀 훅, 기능별 API 호출.

  • 핵심 특징: 행동 중심적; 특정 기능이나 사용자 상호작용과 관련된 비즈니스 로직과 상태 관리.

  • features + ui: 주요 행동을 구현하는 컴포넌트 (예: AddToCartButton).

  • features + api: TanStack Query 같은 도구를 이용한 기능별 API 호출.

  • features + model: 로직을 다루는 커스텀 훅 (useAddToCart).

  • features + lib: 기능에 관련된 유틸리티 함수.

  • features + config: 기능별 상수나 설정 값.

Widget 레이어

  • 목적: 여러 기능이나 UI 요소를 독립적인 단위로 조합한 컴포넌트.

  • 사용 예: Header, Footer, DashboardWidget처럼 여러 기능을 묶지만 자체 로직을 다루지 않음.

  • 핵심 특징: 독립적인 단위로 다양한 작은 구성 요소를 집합함.

  • widgets + ui: UI 요소들의 조합.

  • widgets + api: 위젯 전용 데이터 페칭 (예: fetchWidgetData).

  • widgets + model: 데이터 집계 로직 (자주 사용되지 않음).

  • widgets + lib: 위젯 전용 유틸리티 함수.

  • widgets + config: 위젯의 동작에 관련된 설정.

Page 레이어

  • 목적: 개별 페이지의 레이아웃과 로직을 구성하며, 위젯과 기능, 엔터티를 통합함.

  • 사용 예: HomePage, ProfilePage처럼 전체 페이지를 구성.

  • 핵심 특징: 전체 페이지; 경로 단위의 구조를 표현.

  • pages + ui: 페이지의 레이아웃과 콘텐츠 구조.

  • pages + api: 페이지 전용 API 호출 (예: useFetchHomePageData).

  • pages + model: 경로 처리 및 페이지별 데이터 타입.

  • pages + lib: 페이지 동작에 필요한 유틸리티.

  • pages + config: 페이지 관련 설정 값.

하지만 실제로 해보니 이걸 다 구분해서 쓰이지는 않았습니다.

실제로 여러 프로젝트에 FSD를 적용해보면 FSD가 제시하는 모든 레이어를 완벽하게 구분해서 쓰게 되지는 않았습니다. 처음에는 이러한 경계대로 분리를 억지로 다 해보려고 했는데 오히려 그렇게 접근했기 때문에 더 어려웠던 것 같아요. 우선 세그먼트(ui, api, model)을 중심으로 분리를 하고 난 후 코드가 커지고 분리를 하나보니 프로젝트의 성격에 따라 자연스럽게 필요한 레이어가 정해지더라구요.

프론트엔드 프로젝트들을 성격으로 크게 나눠보자면 크게 홈페이지, 어플리케이션, Admin Console등으로 나눠 볼 수 있습니다. 아! DevTool 정도도 있겠네요. 각각의 특성이 다른 만큼 FSD를 적용을 하더라도 모두가 동일하게 적용이 되지는 않았습니다. Admin Console은 엔티티를 중심으로 CRUD 작업을 하는 게 핵심이었고, 어플리케이션은 페이지 수는 적은데 다양한 features와 widgets이 필요했습니다.

반면 홈페이지 프로젝트의 경우에는 entities나 features들이 페이지별로 독립적인게 많다보니 홈페이지는 또 일반적인 FSD구조를 따르게 되면 오히려 더 파편화가 되는 것이 많았습니다. 이럴때에는 pages 안에서 또 다른 FSD 패턴을 적용하는 식으로 독립적인 중첩 FSD 구조를 깊게 가져가니 훨씬 관리하기 좋더라구요.

FSD의 장점인 이름의 조합을 통해 예측가능한 구조를 만들고 네이밍을 하는 고민을 덜어주는 반면 어떤 구조를 가져가야 좋을지는 상황에 따라서 응집도를 바탕으로 프로젝트에 따라 다르게 가져가야 했습니다.

FSD를 효과적으로 적용하는 팁

분명 FSD는 네이밍 걱정이 없는 좋은 구조를 가지고 있지만 처음에는 코드의 크기에 비해 너무 세밀합니다. 애초에 코드베이스가 엄청 클 때 유용하다고 공식문서에도 나와있었죠. 처음부터 모든 경계를 만들어서 분리하여 적용하겠다는 것 보다는 코드가 커질때 마다 적절히 가지치기 해가듯 확장하는 게 주요했습니다.

혹시 FSD를 적용을 해보고 싶은 분들이라면 네이밍만 그대로 따라하고 다음과 같은 방법으로 한번 시도를 해보는건 어떨까요?

  1. 초반에는 최소한의 조합으로 시작해서 세그먼트들을 중심으로 작성을 합니다.

    • app/config: 전역 설정
    • shared/ui, shared/lib: 공통 컴포넌트와 유틸리티
    • pages/ui, pages/model, pages/api: 페이지별 구성요소
      • pages 단위는 확실히 알고 있죠.
      • ui, model, api로 나누는건 기본 중의 기본에다가 하나도 어렵지 않습니다.
      • 공용이라면 타 프로젝트에서도 재사용이 가능하면 shraed, 아니면 app
  2. 그러다가 도메인 패턴이 보이면 분리를 entitiy나 feature등으로 분리해두기 시작합니다.

    • entities: 데이터와 렌더링이 주 관심사일 때

      • 도메인이 확정되면 도메인 관련 코드를 분리해주면 좋습니다.
      • 특히 읽기 전용인 데이터와 계산 코드들은 분리를 해두면 아주 좋답니다. 그저 데이터!
    • features: 사용자 행동과 데이터 변경이 주 관심사일 때

      • 주로 하나의 컴포넌트에 이벤트 핸들러가 2~3개가 되어 관심사가 복잡해져 컴포넌트가 복잡해진다면 분리하면 좋습니다.
      • 이벤트 핸들러는 props로 넘기지 말고 컴포넌트가 하도록 하는게 좋습니다. 단일 책임!
  3. 또 그러다가 코드가 커지면 widgets으로 분리를 해둡니다.

    • 여러 features나 entities가 조합되어 덩치가 커지면 widgets으로 분리합니다. widgets에서는 말그대로 조립만 하는 역할을 담당하면 좋습니다. 구성과 레이아웃과 같은 컴파운드가 핵심이죠.
  4. 독립적인 구조가 필요하다면 새로운 app을 만들어도 좋습니다.

    • 핵심은 예측가능하고 응집도가 높은 구조이므로 이름만 유지한 채 폴더를 이리저리 옮겨서 도움이 되는 방향으로 폴더구조를 만듭니다. 이름에 대한 고민을 할 필요가 없으니 정말로 도움이 되는 구조로 구성하는게 어렵지 않게 되요.

이렇게 코드가 자라나는 방향에 따라 자연스럽게 구조를 확장해 나가는 게 핵심입니다. 미리 모든 폴더를 만들어두는 게 아니라, 필요할 때 적절한 레이어로 분리하는 거죠. 그리고 이렇게 하다보면 레이어에 대한 더 선명한 경계를 만들 수 있을거에요.


끝으로..

FSD가 최근에 유행하는 키워드다 보니 관심들이 많은데 부정적인 반응을 보이는 분들의 대부분은 이게 feature인가 entity인가 widgets인가? 너무 헷갈려서 도움이 안되는 것 같다. 특히나 이걸 팀원들과 함께 하려고 하면 서로 생각이 달라서 오히려 역효과가 나더라 라고 얘기를 하더라구요. 우리 코드 베이스 볼륨에는 적당하지 않은 것 같다라는 얘기도 하구요.

너무나 공감을 하는 이야기입니다. 처음에 저도 흥미와 학습을 목표로 시작했지만 그 경계를 따라하고 나누는 과정과 하라는 대로 폴더구조를 만들어도 그렇게 의미있어 보이는 구조가 만들어지지 않았습니다. 그렇게 생각이 달라지고 커뮤니케이션 비용이 높아지는 건 좋은 방식이라 생각하지 않았습니다.

그러다가 FSD가 REST API와 비슷한 느낌이구나 하고 깨닫게 되자, FSD가 말하는 경계를 이해하고 나누는게 중요한게 아니라는 것을 알게 되었습니다. 오히려 커뮤니케이션 비용을 줄이고 네이밍의 고민을 줄이고 일관된 폴더 구조를 만드는 컨벤션의 역할을 해주려고 했던 거구나 하면서 긍정적으로 받아들일 수 있게 되었습니다.

FSD를 도입하면 코드가 더 명확하게 구분되어야 하며, 구분이 어려워진다면 본래의 목적에 어긋납니다.

가급적 LayerSegment 에서 사용되는 이름은 변형하거나 늘리지 말고 그대로 써주세요.

폴더 구조는 팀 컨벤션의 역할을 하기에 예측가능해야 하고 모두의 합의가 필요합니다.

그냥 원래대로 내가 코드를 나누는 방식대로 나누되 REST의 GET, POST, PUT, DELTE 마냥 네이밍 컨벤션으로만 쓰여도 괜찮다는 것을 알게 되었습니다. 그리고 이 과정에서 feature + user + ui 와 같은 조합식을 통해서 코드의 경계를 예측해보는 것들이 생각을 맞추고 경계를 선명하게 하는데 큰 도움을 주었습니다.

무지개는 원래부터 일곱가지 색깔은 아니었습니다.

무지개는 원래 7색이 아니었다고 합니다. 한국에서는 예로부터 오색찬란한 무지개라며 5가지의 색을 가진다고 했었고 레인보우 식스라는 말이 있듯 6개로 보는 시각도 있었죠. 지금은 우리가 빨주노초파남보라고 7가지색으로 배우기에 우리는 무지개에서 7개의 색을 발견할 수 있습니다. 명명이 되어 있고 인식이 있기에 경계를 구분할 수 있게 되는 것이죠.

FSD의 의의는 마찬가리라 생각합니다. app, shard, pages, widgets, features, entitiy라는 레이어와 api, ui, model, config, lib 라는 세그먼트들 그리고 이러한 레이어와 슬라이스와 세그먼트의 조합을 통해서 우리는 코드에서 더 분명한 경계를 발견할 수 있고 더 정돈된 구조를 가질 수 있게 됩니다. 꼭 FSD가 아니더라도 적어도 내가 알고 있는 폴더 구조명 만큼이나 코드를 구분할 수 있게 되는 것이겠죠.

그래서 FSD를 실제로 쓰지는 않더라도 이 경계에 대해서 생각을 해보는 것만으로도 공부를 하는데 있어서는 큰 도움이 되겠다 생각을 해봅니다.

중요한 건 경계를 정하고 유지하는 일관성과 분리의 감각

FSD에서 가장 어렵다 여겨지만 entitiy, features, widgets에 대한 경계를 나눠서 코드를 작성하려고 하는 것은 무지개에서 빨강과 노랑 사이의 주황의 경계를 정하는 것과 같다 여겨집니다. 이게 맞는가에 대해서도 애매하고 완벽한 구분도 잘 모르겠습니다. 하지만 중요한건 경계를 선명하게 하는 것보다 하나의 경계를 정했다면, 그 기준을 일관되게 유지하는 게 중요합니다. 그래야 선명해지는 법이니까요.

그렇게 억지로라도 경계를 만들고 나누다보면 자연스럽게 코드의 베이스가 커졌을때도 작은 코드로 유지보수할 수 있는 힘이 생긴다 생각합니다. 코드가 100줄에서 어느순간 200줄 400줄로 커져가고 있는데 이제는 어디서 어떻게 분리를 해야할지 모르는 채로 엉켜있는 코드가 생기는 순간 나중에는 아무도 그 엉킴을 풀어 낼 수가 없겠죠.

일단 확실치 않더라도 코드를 분리해두는 감각을 익혀둔다면 설사 잘못된 구조를 만들었다 하더라도 재배치하여 구조를 만드는 건 어렵지 않습니다. 엉켜있지만 않는다면 재배치를 하는건 어려운 일이 아니니까요.

요즘은 AI가 코드 작성의 대부분을 도와주는데 결국 정리정돈과 청소는 아직까지 개발자의 몫이라 생각합니다. 그리고 AI는 아쉽게도 알아서 하라고 하면 80~90점 밖에 하지를 못합니다. 내가 더 세세하게 알려줄수록 일을 잘하는 똑똑하지만 손이 많이 가는 녀석이에요. 어떻게 세상이 바뀔지는 모르겠지만 아직은 내가 더 많이 알아야 하고 특히 구조와 코드 분리, 그리고 정리정돈의 영역은 참 중요하다 여겨집니다.

FSD는 이러한 코드의 체계와 분류기준 그리고 관점을 만들어주는 좋은 참고자료라고 생각합니다. 적어도 무지개 색깔이 빨주노초파남보 7개야 라고 알려주면 어쨌든 우리는 이안에서 7개의 색깔을 발견하려고 애를 쓰듯 app, shared, process, pages, widgets, features, entities / api, config, model, lib, ui 등 어렴풋이 알고 있던 코드의 경계를 구조화하고 명명화 해주는 것으로 충분히 배워보고 시도해볼만한 가치가 있다고 생각합니다.

어떻게 하는게 FSD를 잘하는 거다 고민하지 마시고 다같이 모여서 재미나게 widgets + cart + ui는 뭐일꺼 같아? 하면서 함께 컨벤션과 경계를 찾아보는 걸 해보는건 어떨까요?

FSD를 해보고 싶거나 관심이 있는 분들에게 도움이 되는 글이길 바랍니다. 감사합니다 :)


[Appendix A] widgets?

개인적으로 FSD 폴더구조가 좋은 점은 생각보다 폴더 이름이 알파벳 순으로 app, entities, features, pages, shared, widgets 순으로 꽤나 순서대로 되어 있다는 점입니다. 그러니 막상 이 /widgets 이라는 폴더 명이 순서상 상당히 거슬리더라구요 (혼자만 외딴 섬에 있는 느낌) ㅋ 그래서 고민 고민하다 저는 /modules 이라는 이름으로 사용하고 있습니다.

/app
/entities
/features
/modules
/pages
/shared

어떤가요? 사실 app과 shared는 의존관계로 따지면 둘의 위치가 바뀌어야 하지만 양극단이 그런지 위화감이 없답니다. 저는 만족하는데 Cursor가 코드리뷰하면 modules 이름가지고 항상 지적을 하네요. 😅

그리고 반드시 이러한 레이어에 집착할 필요는 없습니다. slice가 더 중요하다면 slice아래 entities, features, module등으로 얼마든지 세부화 할 수 있고 pages들이 독립적이라면 pages 아래에 새로운 app들이 있어도 무방하다고 생각합니다.

이 이름과 구조를 유연하게 사용하면 좋은 것이지 이 구조 자체에 집착을 하게 되면 나에게 딱 맞는 옷인 경우도 있지만 프로젝트와 전혀 맞지 않는 옷이 되는 경우도 있으니 도움이 되지 않는다 생각을 하면 프로젝트에 도움이 되는 방향으로 구조는 얼마든지 고쳐도 된다 생각합니다.

[Appendix B] Teo's FSD Convention Guidelines

컨벤션을 정해보면서 문서화를 해보면 어떨까 해서 GPT와 Claude의 합작으로 사용하고 있는 컨벤션을 문서를 만들어보고 있는데 코드를 정리하다보면 계속 고민할 거리들이 생겨서 앞선 글에서는 FSD은 이렇게 해야 정답이다라는 틀에 갇히면 실제 프로젝트와 맞지 않는 구조를 만나게 된다고 했지만 구체적인 내용이 또 아예 없으면 감을 잡기도 어려울 것 같아서 공유해봅니다.

1. Standard Roles in Each Slice

slice/
  ├── api/     // Data fetching
  ├── config/  // Constants, configurations
  ├── model/   // Business logic, types, state
  ├── lib/     // Utils, helpers
  └── ui/      // Visual components

2. Layer-Specific Conventions

Entity Layer

entities/product/
  api/    // axios, fetch basic CRUD operations
  config/ // Entity configurations, e.g., MAX_POST_LENGTH
  model/  // Pure business logic + types
  lib/    // Entity-specific utilities, e.g., removeItem
  ui/     // Read-only presentational components

Rules:

  • All functions must be pure (same input = same output)
  • No side effects
  • No state management
  • No external dependencies
  • Focus on data transformation with own schema
  • Reusable across different features
  • Testable with pure unit tests
  • ui: Pure presentational components
  • model: Contains pure functions, business logic, and type definitions

Key Point: pure!

Example:

✅ Good

// entities/product/model/validation.ts
export const isValidProduct = (product: Product): boolean => {
  return product.price > 0 && product.stock >= 0;
};

❌ Bad

// Impure function with side effects
export const updateProductPrice = (product: Product, newPrice: number) => {
  product.price = newPrice;  // Mutation!
  api.updateProduct(product); // Side effect!
};

Features Layer

Components to Include:

  1. Main action component
  2. All related form inputs
  3. All supporting UI elements

Rules

  1. One Feature = One Primary User Action

    • Include one primary user action
    • Include all supporting UI elements
    • All related form inputs belong together
  2. Hook-Based Logic

    • Business logic lives in hooks
    • Each hook has single responsibility
  3. Props Guidelines

    • Accept only domain data
    • No event handler props
    • No UI configuration props

Examples

✅ Good Example

  • Primary user action + Domain Props + hooks
// features/addToCart/ui/AddToCartButton.tsx
export const AddToCartButton = ({ product }: { product: Product }) => {
  const { handleAddToCart, isLoading } = useAddToCart();

  return (
    <Button onClick={() => handleAddToCart(product)} disabled={isLoading}>
      Add to Cart
    </Button>
  );
};

// features/addToCart/model/useAddToCart.ts
export const useAddToCart = () => {
  const { mutate, isLoading } = useAddToCartMutation();

  const handleAddToCart = (product: Product) => {
    mutate(product.id);
  };

  return { handleAddToCart, isLoading };
};

✅ Good Example 2: Action with Input State

  • All related form inputs + All supporting UI elements
// features/addComment/ui/AddCommentForm.tsx
export const AddCommentForm = ({ productId }: { productId: string }) => {
  const [comment, setComment] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!comment.trim()) return;

    api.addComment(productId, comment);
    setComment("");
  };

  return (
    <Form onSubmit={handleSubmit}>
      <TextArea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Write a comment..."
      />
      <Button type="submit">Add Comment</Button>
    </Form>
  );
};

❌ Bad Examples

  • Many Props with Event Handlers
// ❌ Bad Example 1: Too Many Responsibilities
// features/product/ui/ProductActions.tsx
export const ProductActions = ({
  product,
  onAddToCart,  // Receiving handlers as props
  onWishlist,    // Should handle internally
  theme,         // UI config props
  className      // Style props
}) => {
  return (
    <div className={className}>
      <Button onClick={() => onAddToCart(product)} theme={theme}>
        Add to Cart
      </Button>

      <Button onClick={() => onWishlist(product)}>
        Wishlist
      </Button>
    </div>
  );
};
  • Feature Has No User Action
// ❌ Bad Example 2: No Handler (Should be in Entity or Widget)
// features/product/ui/ProductPrice.tsx
export const ProductPrice = ({ price }: { price: number }) => {
  return (
    <div className="text-lg">
      ${price.toFixed(2)}
    </div>
  );
};

Widgets Layer

widgets/header/
  api/    // Widget-specific APIs
  config/ // Widget configurations
  model/  // Types + minimal logic
  lib/    // Widget-specific utilities
  ui/     // Composition of features

Rules:

  • Compose features; avoid own handlers
  • Only domain data as props
  • Or no props at all

Pages Layer

pages/main/
  api/    // Page-specific data
  config/ // Page configurations
  lib/    // Page-specific utilities
  model/  // Routes + types
  ui/     // Page composition

3. Props Guidelines

For shared/ui ONLY:

  • Can accept event handlers
  • Can accept styling props
  • Can accept configuration props

For All Other Components:

✅ Good

// Accept only domain data
const ProductCard = ({ product }) => { ... }

❌ Bad

// Avoid event handlers and styling/configuration props
const ProductCard = ({ product, onAddToCart, onWishlist }) => { ... }
  • Accept only domain data
  • Avoid event handlers
  • Avoid configuration props
  • Avoid styling props
profile
AdorableCSS를 개발하고 있는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

18개의 댓글

comment-user-thumbnail
2024년 11월 19일

개인적으로 프론트엔드 코드 관심사 분리를 어떻게 할지를 위한 기준을 잡는데 정말 도움이 되는 폴더 구조 같습니다.
이 글을 통해서 더욱 쉽게 이용할 수 있겠네요!

1개의 답글
comment-user-thumbnail
2024년 11월 20일

Bad Examples에서 Many Props with Event Handlers의 예시를 올바른 예시로 들게되면 어떻게 변화가 되나요 ? 현재 저희 프로젝트의 폴더 구성 자체가 저런식으로 엄청 많은 props들을 자식 컴포넌트에 또 props로 내려주는 구조로 이어져있는데.. 개선을 어떻게 해야할까요?

2개의 답글
comment-user-thumbnail
2024년 11월 20일

entities, features, widgets에서 항상 고민했는데 감사합니다~ 역시 GOAT

1개의 답글
comment-user-thumbnail
2024년 11월 22일

너무 잘읽었습니다! FSD를 적용하기 전에 고민이 되었던 부분이 많이 해소되었습니다 :)
FSD로 설명을 해주셨지만 그 안에서 프론트엔드 개발을 하면서 중요한 인사이트가 녹아있어서 너무 좋았습니다!

추가로 질문하나 드려보아요!
features의 AddToCartButton 예제에서 useAddToCartMutation 내부에서 api와 useMutation을 조합할텐데요,
궁금한 점과 고민되는 부분은 useAddToCartMutation 내부에서 사용된 api는 어느 레이어에 속해야하는가 입니다. 이 지점이 엔티티와 피쳐의 고민이 되는 부분인 것 같아요.
entities에 들어가는 api와 features에 들어가는 api의 차이점과 기준이 혹시 있을까요?

1개의 답글
comment-user-thumbnail
2024년 11월 26일

훌륭한 설명과 이 주제에 대한 자세한 지식을 제공해 주셔서 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 12월 2일

Thank you for sharing your great knowledge

답글 달기
comment-user-thumbnail
4일 전

프로젝트 규모가 커져가면서 폴더 구조를 어떻게 다룰지 깊은 고민에 빠졌었는 데, 이 글을 통해 좋은 인사이트를 얻어간 것 같아 감사합니다 :)

1개의 답글