북마크 익스텐션 FSD 구조에 맞게 설계하기 [3편]

황준승·2024년 8월 24일
2

📌 개요

최근 프로젝트 설계에 대해 고민하다가 FSD에 대한 설계를 우연히 접하게 되었다. 비즈니스로직과 공통 컴포넌트를 분리한다는 개념이 너무 마음에 들어 실제 프로젝트에도 적용하게 되었다.

아키텍처 설명과 실제로 적용해보면서 내가 경험했던 것들과 장단점에 대해 기술해보려고 한다.

✏️ FSD 아키텍처란?

공식문서에 워낙 잘 설명이 되어있어 간단하게 정리만 하고 넘어가려고 한다.

FSD(Feature-Sliced Design)는 기능 분할 설계의 아키텍처이다.
크게 3가지의 개념으로 구분되어 있으며, 각각 Layer, Slice, Segment로 구성되어 있다.

Layer 알아보기

레이어 안에는 7가지 폴더로 구성하면 되고, 각 폴더마다 서로의 역할이 있어서 폴더 별로 구분해서 관리해야 한다.

  1. app: Provider(리액트 쿼리 프로바이더, 전역 컨텍스트 등), 전역 스타일, 전역 컨텍스트 등 애플리케이션의 최상단에서 사용되는 것들이 여기에서 정의된다.

  2. pages: 폴더의 말 그대로 서비스의 페이지들이 들어가게 된다.

  3. widgets: 여러 개의 독립적인 UI를 합쳐서 하나의 컴포넌트로 만드는 곳이 된다.

  4. features: 비즈니스 가치를 전달하는 사용자 시나리오와 기능을 다룹니다. (ex) 게시물을 북마크한다, 메시지를 전송한다 등)

  5. entities: 비즈니스의 엔티티를 나타낸다. 쉽게 생각하면 데이터 모델이라고 생각하면 됩니다. (ex. User 모델: User 생성, 삭제, 수정 등, api로 부터 받은 데이터 가공 등)

프론트엔드에서의 비즈니스 엔티티

우리가 정의하는 비즈니스 로직은 프론트엔드와 백엔드는 서로 다를 수 있습니다. 만약 우리가 무한 스크롤을 통해서 상품 리스트를 요청할 경우, 백엔드의 경우 단순 request 요청이지만 프론트엔드의 경우 스크롤 행위에 해당합니다.
따라서 백엔드로부터 받은 데이터를 프론트엔드에서 필요한 데이터를 가공하거나 프론트엔드의 행위에 맞게 이름을 수정하는 것 또한 비즈니스 로직에 해당합니다.

  1. shared: 재사용이 필요하거나 유틸리티로 쓰이는 함수들 같이 공유가 필요한 것들이 포함됩니다.(ex. 공통 커스텀 훅, 공통 컴포넌트 등)

또한 각 계층은 사용할 수 있는 계층이 제한적입니다. 표를 보면 각 계층에서 사용할 수 있는 것이 제한적이기에 이런 점을 더 생각해서 설계해야 합니다.

Slice 알아보기

슬라이스는 Layer의 각 계층에 모두 존재할 수 있습니다. (pagesshared는 제외)
슬라이스에서는 프로덕트의 성격마다 비즈니스가 모두 다르기에 각 프로젝트에 맞춰서 지정하면 된다.

Segment

세그먼트는 슬라이스 내에 존재하며, 세그먼트 내에 드디어 코드를 작성해 들어가게 되는 폴더이다.

💼 FSD 구조에 맞게 설계하기

이제 FSD에 대해서도 공부했으니까 우리가 설계한 와이어 프레임을 토대로 FSD 구조에 맞게 컴포넌트를 분리해보았다.

위 그림을 자세히 확인하고 싶다면 피그마 링크를 통해서 확인해보자

각 계층을 어떤 방식으로 분류하고 구현했는지 알려드리도록 하겠습니다.

shared: Button, Input 등 공통 컴포넌트 선언 및 BASE_URL 및 공통 커스텀 훅을 선언하였습니다.

entities: tanstack-query를 활용하는 로직들을 주로 선언했습니다. ( 서버 상태를 조작하는 것 만으로도 entites를 잘 표현한다고 생각했기 때문 )

features: bookmark CRUD를 하는 UI, 검색하는 UI 등을 선언

pages: 실제 페이지 - features에 선언한 컴포넌트와 shared 컴포넌트의 합성을 통해 구현

app: Provider(리액트 쿼리 프로바이더, 전역 컨텍스트 등), 전역 스타일, 전역 컨텍스트 등 애플리케이션의 최상단에서 사용되는 것들이 여기에서 정의

🥲 FSD를 적용하면서 어려웠던 점

문제 상황

Modal 컴포넌트를 열기 위해서는 ModalLayer.Opener 컴포넌트를 통해서 열 수 있습니다. 이때, props에 정의된 modalContent에 새로 열릴 Modal 컴포넌트가 선언됩니다.

<ModalLayer.Opener
  modalType="sidebar-panel"
  modalContent={ /* sidebar-panel에 들어갈 컴포넌트 */ }
  >
  /* 버튼 모양 관련 */
</ModalLayer.Opener>

하지만 문제는 아래 features Layer에 사용되는 ModalLayer.Opener 컴포넌트가 modalContent에는 pages단위의 컴포넌트가 정의 선언된다는 점입니다.

// features Layer 내부의 컴포넌트

import SidebarPanel from '@/widgets/sidebarPanel'; 
// 문제 상황: features Layer 내부의 컴포넌트에서 widgets계층의 컴포넌트를 참조


<ModalLayer.Opener
  modalType="sidebar-panel"
  modalContent={<SidebarPanel.UpdateFolderForm folderData={folderData} />}
  etcStyles={{ ...etcStyles }}
  externalAction={externalAction}
  >
  {children}
</ModalLayer.Opener>

해결

app 계층의 경우 Provider, 전역 스타일, 전역 컨텍스트 등을 정의, 하위 계층에서 해당 컨텐츠를 사용할 수 있게 합니다.

ModalLayer.Opener 역시 이와 유사한 성격을 띄고 있다고 판단하여 최상위 계층인 app Layer에 modal-router 폴더를 선언하여 아래 계층(ex. features, widgets)에서 참조할 수 있도록 구현하였습니다.

✨ 구현해보면서 느낀 장단점

장점

  • atomic 패턴에 비해 레이어 분리가 비교적 쉽다.
  • 팀 적으로 소통이 원활하게 될 수 있다.

단점

  • 설계단에서 시간을 많이 가져가야 한다. (초기 설계를 잘못하게 될 경우 고통 받을 수 있음)
  • 팀과의 소통이 중요하다.
  • 아토믹 디자인 패턴과 비슷한 단점인 '이건 어디에 들어가야 맞을까?'와 같은 생각이 들 때가 있다.
  • 폴더가 매우 많이 생기게 된다. (뎁스가 깊어질 수 있음)
profile
다른 사람들이 이해하기 쉽게 기록하고 공유하자!!

0개의 댓글