따라 익혀 보는 FSD 아키텍처

manNomi·2024년 10월 10일

WEB

목록 보기
3/9
post-thumbnail

FSD 아키텍처

React 의 폴더 구조 방식인 FSD 에 대해 알아보겠습니다

 FSD(Feature-Sliced Design)

  • FSD 패턴을 삽질 후 얻은 완벽한 패턴 분석에 대해서 이야기 나눠보자!
  • 실제로 사용할때 참고할 수 있도록 작성해보겠습니다.

FSD 패턴이란?

기능분할설계 패턴으로 뷰와 로직을 분리하는 것에서 시작해서 응집도의 중요성을 강조하는 패턴입니다 .

적절하게 파일들을 폴더구조를 통해 고립시키는 것이 특징입니다

그렇다면 가장 중요한 키워드인 응집도란 무엇일까요?

  • 결합도의 반대되는 언어입니다. 결합도가 높으면 개발이 간단하다! 코드를 으라라라라 작성해서 한파일로 작성하면 결합도가 높겠다 . 빠르게 만들 수 있고 단순한 설계 일것이다
  • 허나 결합도가 높으면 치명적인 단점이 있는데 종속성이 심해진다는 것이다. 듣자마자 이해가 될것이다 종속성이 높을수록 코드 수정은 힘들고 유지보수성이 떨어질 수 밖에없다.
  • 프론트엔드의 최고점은 유지보수성의 최적화라고 생각한다
  • 따라서 응집도를 높이는 방식을 고수해서 작성해야하는데 FSD 패턴에서는 응집도가 정말 중요한 키워드로서 작용한다 매우추천!!

React는 기본적으로 응집도를 높게 설계해야 합니다. 응집도가 높아지면 재사용성과 테스트성이 향상되지만, 코드의 흐름을 해석하기 어려워질 수 있습니다….

결합도가 높으면 한 파일에 코드가 너무 많아져 관리가 어려워집니다. 설계를 경험해보신 분들이 모두 겪는 문제인데 이를 해결하기 위해 FSD 패턴이 필요합니다.


FSD 패턴 구조

  • FSD 구조:
    • FSD는 Layer -> Slice -> Segment의 구조로 나누어집니다.
    1. Layer: 응집도와 결합도의 단계를 나타내는 단위로, 상위 레이어는 하위 레이어를 참조할 수 있지만, 반대는 불가합니다.
      • App -> Page -> Process -> Widgets -> Features -> Entities -> Shared의 구조를 따릅니다.
    2. Slice: 해당 레이어에 포함되는 종류로, 같은 레이어의 다른 Slice는 참조할 수 없습니다.
      • 예시: LoginPage, SignupPageApp에서 조합되며, 서로 참조하지 않습니다.
    3. Segment: 해당 Slice를 구성하는 파일 종류로, UI, style, model, lib, asset 등이 있습니다.
      • 다른 Slice의 Segment는 접근할 수 없습니다.

여기서 중요한것이 Layer의 하위에서 상위로의 참조는 절대 불가하다는 것입니다.

역할에 맞게 컴포넌트를 고립 시켜야합니다 !

FSD의 원칙!

fsd패턴은 규약입니다. 서로가 알아볼 수 있게 만들려면 그리고 react가 추구하는 방향에 옳게 개발하기 위해서는 아래 규약을 지키는것이 필수적입니다.

  1. 상위 레이어에서는 하위 레이어 접근 가능, 반대는 불가:
    • 상위 레이어에서 하위 레이어의 Slice를 가져다 씁니다.
    • App과 Page 레이어에서만 index.js가 존재합니다.
  2. 같은 레이어의 Slice끼리는 접근 불가:
    • 오직 상위 레이어에서 조립이 되어야 합니다.
  3. Segment끼리는 상호 접근 가능:
    • 단, 다른 Slice의 Segment는 접근할 수 없습니다.
  4. 재사용이 안 되는 경우:
    • Slice 안에 Slice를 넣더라도 고정시켜 사용합니다.
  • 직접 접근을 최소화하고 import를 고립시킨다!! 이 문장을 생각하고 개발하시면 이해하시기 편할겁니다
  • 예시를 들면 page에서 index.js 에서 main과 shorts 페이지를 조립해주고 app에서는 page를 임포트하는 것만으로도 컴포넌트가 사용이 가능할겁니다 !
  • ( index.js 로 이름을 지으면 폴더이름으로 참조가 가능하다는 장점을 이용 ! )

한번 쓰이는 컴포넌트들은 최대한 묶어야 합니다 재사용성이 없거나 백엔드 통신을 통해 값이 변하는 부분을 제외한 나머지를 모두 결합시켜야합니다.

아토믹 디자인처럼 가능한 모든것을 분리하는 것 과는 조금 다르게 느껴질 수 있다고 생각합니다. - ( 아토믹 디자인도 모두 나누는 것은 아니기 때문에 비슷하기도 합니다)

개발하는 과정에서 재사용이 필요하겠는데? 싶은것만 layer를 내려서 개발을 하면 쉽게 FSD 패턴을 적용할 수 있을 것이라고 생각합니다 . 또한 state에 따라 렌더링을 구분짓고 싶을때도 layer에서 slice로 구분해서 개발을 하면 좋습니다 !

이론을 너무 오래설명 했던것 같은데 FSD 패턴을 직접 적용해보는것 만큼 좋은 방법이 없을 것 같습니다


그렇다면 FSD 패턴을 직접 만들어보면서 test 해봅시다!

JS 에서 React 로 넘어오면서 다들 node.js 를 사용했을 것이다.

브라우저 종속성을 해결해주고 js 파일을 호출할때만 메모리를 올리는등 장점이 너무나도 많은데 이러한 node를 npm 구조를 통해 쉽게 관리하는데 이 node의 npm 을 사용해봅시다!

CRA 프로젝트 설정 (Create React App)

node 기반으로 React 프로젝트 자동으로 설정해주는 패키지로 Meta에서 개발했습니다

CRA에는 Webpack기능이 내장되어있습니다. Webpack이 무엇인지는 다른 게시글에서 설명해놓았으니 참고 바랍니다 !

npx 명령어를 통해 CRA를 설치하지 않고 바로 사용가능합니다

npx create-react-app 'your-project-naem'

설치가 안되는 사용자도 있을텐데 이는 권한 문제일 가능성이 큽니다 구글링을 해보거나 아래 코드를 기입 해보세요 ! 설치할때 관리자 권한으로 설치하지 마시길 바랍니다 ! 만약 관리자 권한으로 설치했다면 권한을 변경해야만 합니다

sudo chown -R $(whoami) ~/.npm

프로젝트는 아래와 같이 구성됩니다.

  • 프로젝트 구성 파일:
    • package.json:
      • 프로젝트에 대한 정보와 설치된 패키지들의 종속성을 관리합니다.
    • package-lock.json:
      • 프로젝트를 설치한 시점의 패키지 경로와 버전을 관리하며, 이를 통해 몇 년이 지나도 동일한 환경을 유지할 수 있습니다.
    • node_modules:
      • npm을 통해 설치된 모든 모듈이 저장되는 폴더입니다.
  • 파일 구조:
    • src: 경로 접근이 불가능한 파일들이 위치합니다.
    • public: 경로 접근이 가능한 파일들이 위치하며, index.html과 같은 파일이 여기에 포함됩니다.
  • npm install:
    • package.json을 참고하여 필요한 모듈을 다운로드합니다.

src 파일에 일반적으로 우리가 사용하는 코드를 작성합니다 public의 코드는 사용자가 참조가 가능하기 때문에 대부분의 코드는 src에 작성하고 public에는 이미지를 넣을때 쓰곤 합니다 .

public의 이미지와 src 이미지 참조방식이 다릅니다

public 폴더 안에 images/logo.png 파일이 있다면, HTML이나 JSX에서 이를 다음과 같이 참조할 수 있습니다:

<img src="/images/logo.png" alt="Logo" />

src에 있을때는 import 통해서 해야합니다

import logo from './assets/logo.png';

이것만 봐도 딱 감이 오실것이라고 생각합니다

webpack의 기능이 번들링 해주는 기능이기 때문에 src의 코드를 app에서 번들링 해주고 public의 index.html에 넣어주는 것이다

이 과정을 거치고 public에 index.html을 작성하고 아래 코드를 입력하면 HMR (Hot Module Reload) 코드를 바로 화면에 반영해주도록 해서 Babel을 통해 ES5로 변환해서 브라우저에 전달합니다. 브라우저의 종속성을 고려해주기 때문에 ES5 이후 기술도 사용가능합니다! ( 노드의 핵심!! )


Layer

일반적으로 레이어는 App -> Page -> Process -> Widgets -> Features -> Entities -> Shared의 구조를 가집니다!

그중 우리는 아래중 체크된 부분만을 사용하자

  • App -> Page -> Process -> Widgets -> Features -> Entities -> Shared의 구조
    • process : 페이지의 순서를 관리해주는데 크게 필요가 없기에 뺐다
    • features : 로직을 관리하는데 굳이 필요가 없기에 뺐다

SRC 의 index.html

  • 리액트 돔에 App 컴포넌트를 등록한다 ! 아래코드는 리코일 사용을 위해 RecoilRoot를 App에 감싸주었다
ReactDOM.createRoot(document.getElementById("root")).render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
);

App

  • 프로젝트의 입구다 주로 page의 슬라이스들을 조합해준다! 테마를 전역으로 설정하고 브라우저 라우터를 적용한 모습이다
const App = () => {
  const [theme_dark] = useDarkAtom();
  return (
    <BrowserRouter>
      <ThemeProvider theme={theme_dark ? darkTheme : lightTheme}>
          <Header />
          <Page />
      </ThemeProvider>
    </BrowserRouter>
  );
};
export default App;

Page

  • 페이지들을 조합해준다 !
  • 라우트를 이용해서 페이지를 이동해주고 있습니다
  • slice간 참조를 막기위해 page의 루트에서 index.js를 다음과 같이 선언 합니다 .
  • app과 page만 index.js가 존재하는 이유입니다!
const Page = () => {
  return (
    <>
      <Aside />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/shorts" element={<Shorts />} />
        <Route path="/detail" element={<Detail />} />
        <Route path="/subscribe" element={<ExceptPage />} />
        <Route path="/youtubeMusic" element={<ExceptPage />} />
        <Route path="/myVideo" element={<ExceptPage />} />
        <Route path="/offline" element={<ExceptPage />} />
      </Routes>
    </>
  );
};
export default Page;

SPA 방식으로 설계하는 것을 추천한다

싱글 페이지 어플리케이션의 약자인데 서버와의 통신없이 페이지 전환은 엄청난 장점이라고 생각한다

부드럽고 트래픽 적고 코드 재사용하기 좋고 추천!

Widget

  • 위젯에는 다양하게 쓰이는 쉐어드의 집합을 사용한다 !
  • 예를 들어 댓글의 경우 컴포넌트들의 집합이다
  • 댓글 사용자 좋아요 싫어요 등 서버와의 통신이 필요하기도 하고 다양한 곳에서 쓰이므로 위젯으로 분리한다
  • comment 의 root에 다음과 같은 형식으로 작성해 폴더로 참조가능하도록 한다
import Comment from "./ui/Comment";
export default Comment;

Entities

  • 엔티티는 본래 데이터와 관련된 로직을 작성한다
  • 허나 저 같은 경우에는 데이터와 관련된 백엔드 통신을 entities 에 모아서 작성하려고 합니다
  • 보통 각 레이어의 model에 작성하는것이 일반적일 수 있습니다
  • 데이터 불러오는 API가 한곳에서만 사용한다고 장담할 수 있을까?
  • 댓글을 가져오는 API를 page마다 다른곳에서 사용한다면 model 을 두개 만드는것은 상당히 증복성을 높인다고 생각합니다 응집도와도 반대된다고 생각
  • 따라서 entities에 useGetData를 하는것을 추천합니다
  • 폴더로 data 명명을 구분하고 useEffect 를 이용해서 가져오자!
  • 아래는 entities에 최적화된 코드라고 생각한다
const useDetailCommentData = () => {
  const [commentDataList, setCommentDataList] = React.useState([]);

  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(false);
  React.useEffect(() => {
    const fetchData = async () => {
      try {
        const commentData = getCommentData();
        //getData부분에 본인이 원하는 API를 실행시키면 되겠다 ! 
        // 주소를 입력하는 방법도 있겠다 
        setCommentDataList(commentData);
        setLoading(false);
      } catch (error) {
        setError(true);
      }
    };
    fetchData();
  }, []);
  return [commentDataList, loading, error];
};
export default useDetailCommentData;

Shared

  • shared에는 어디서든 재사용 가능 한
  • Shared 사용을 피합시다
  • 어디서든 쓸것이라고 생각하고 shared를 만들어 놓으면 참조할때 shared를 조금이라도 수정하게 된다면 모든 파일에서 수정해야하는 유지보수 저해되는 코드가 작성될 수 있습니다.
  • 필요한 코드만을 작성하도록 유지하는 것을 추천합니다
  • recoil, redux 같은 글로벌 state의 경우 shared에 작성하는게 옳습니다

놀랍게도 layer만 살펴봤는데도 벌써 꽤 내용이 많습니다 .

하지만 slice는 별거 없습니다 !

Slice

slice 구조는 폴더의 집합입니다

예시를 들자면 Page 폴더의 슬라이스는

Shorts , main , …etc 가 있을것입니다.

이 슬라이스 안에 이제 각각의 세그먼트들을 조합합니다

slice에도 index.js 를 두어

자주 사용하는 슬라이스만 얘기 해보자면 model , ui , assets 정도 있습니다

slice는 다른 요소들과 조금 독특합니다

assets

  • 말 그대로 리소스들을 모아놓습니다 !
  • 이미지들을 assets에 모아두면 되는데 필요한 부분에서 assets를 설정해줘야합니다

UI

  • UI 뷰와 관련된 파일을 넣습니다!
  • 여기서 중요한점이 재사용 되는 코드지만 한개의 슬라이스에서만 사용되면 슬라이스의 ui에서 만들고 세그먼트간 호출하는 방식으로 사용해야합니다 !!
  • 응집도를 높이는 방법 !
  • 위 구조에서도 aside_element가 있는데 이 aside_element는 aside에서만 사용하기 때문에 같은 ui 에서 생성해주는것이 맞는 방식입니다

만들어진 Aside 루트 index.js 에서 다음과 같은 방식으로 export 해주면 폴더를 import 하는 것만으로 Aside 컴포넌트를 호출할 수 있기에 import 깊이도 조절할 수 있습니다. + 고립의 목적 다른 폴더들은 export 하지 않게 막기 위함도 있습니다

import Aside from "./ui/aside/Aside.js";
export default Aside;

Model

  • 비즈니스 로직들이 들어갑니다.
  • 보통 상태를 관리하는 hook을 넣으며 state나 action등을 넣습니다
  • useState관리하는 폴더라고 생각하면 됩니다!

API

  • api 에서는 요청을 하는 코드인데 entities로 옮겼기에 사용할일이 거의 없습니다
  • 극한의 응집도와 결합도의 조화를 위해서 넣을 수 는 있겠지만 후에 개발 환경에서 달라질 가능성을 놓기 위해서 API 세그먼트는 따로 만들지 않습니다

이처럼 FSD 패턴을 공부하고 사용 해보았는데 간단한 예시를 보여드리겠습니다

FSD 예시

 src/
├── widgets/
│   └── aside/
│       ├── api/
│       │   └── asideApi.js
│       ├── model/
│       │   └── asideModel.js
│       ├── ui/
│       │   ├── Aside/
│       │   │   ├── Aside.js
│       │   │   └── style.js  
│       │   └── Aside_icon/
│       │       ├── Aside_icon.js
│       │       └── style.js  
│       └── index.js  
├── pages/
│   ├── home/
│   │   ├── api/
│   │   │   └── homeApi.js
│   │   ├── model/
│   │   │   └── homeModel.js
│   │   ├── ui/
│   │   │   └── Home/
│   │   │       ├── Home.js
│   │   │       └── style.js  
│   │   └── index.js  
│   ├── detail/
│   │   ├── api/
│   │   │   └── detailApi.js
│   │   ├── model/
│   │   │   └── detailModel.js
│   │   ├── ui/
│   │   │   └── Detail/
│   │   │       ├── Detail.js
│   │   │       └── style.js    
│   │   └── index.js  
│   └── index.js
├── app/ 
│   │
│   └── index.js
└── index.js
  • 위와 같은 폴더구조가 가장 이상적이라고 생각하는 폴더구조입니다.
  • 설계의 정답은 없다고 생각합니다 특히 폴더구조와 아키텍처는 모두가 다르게 생각 할 수 있다고 생각합니다
  • 그렇기에 더더욱 재밌다고 생각하는데 각자만의 스타일대로 근거를 가지고 설계를 해본다면 어떨까 생각합니다
  • 바쁘더라도 꼬옥 이 예시 자료와 본인의 폴더 구조를 비교해보는 시간을 가지면 좋겠습니다
profile
weapp개발자

0개의 댓글