FSD 맛보기

2026. 1. 25. 20:46·개발

 

항해 회고글에서는 FSD가 제일 공감이 안 된다고 투덜대었는데 간단한 나의 토이 프로젝트...를 FSD로 쓸데없이 오버엔지니어링하여 적용해보겠다. 이 프로젝트를 꺼내온 건 볼륨이 작고, 간단하고(비즈니스 로직이나 서버/api 연동이 없다... 아예 없어서 문제인가?), 캐릭터가 귀엽기 때문이다.

 

내가 친구랑 지어낸 머지영화제의 홈페이지를 개선해보겠다.

기존 프로젝트는 react+framer motion+jotai+tailwind css로 구성되어있다.

2023년에 만든 것이라 수제 코딩+챗지피티로 한땀한땀 작업했다. (그림은 친구가 그려줌. 이것도 수제임)

https://feature-sliced.github.io/documentation/kr/

 

Welcome | Feature-Sliced Design

Architectural methodology for frontend projects

feature-sliced.github.io

 

기존에는 이런 국룰 방식으로 구조가 되어있었다. 이 규모의 앱은 이 구조면 사실 충분하다. 그래도 FSD의 개념이 헷갈리기 때문에! 이 프로젝트를 마이그레이션 해볼 것이다.

 

맨날 보는 그 이미지

 

Layer는 모든 FSD 프로젝트의 표준 최상위 폴더입니다.

  1. App - Routing, Entrypoint, Global Styles, Provider 등 앱을 실행하는 모든 요소
  2. Processes - 더 이상 사용되지 않음
  3. Pages - Route 기준으로 구성된 주요 화면 단위
  4. Widgets - 크고 독립적으로 동작하는 UI 구성 단위, 일반적으로 하나의 완결된 화면 기능(use case)을 제공합니다.
  5. Features - 사용자에게 비즈니스 가치를 제공하는 액션을 구현한 재사용 가능한 제품 기능 단위
  6. Entities - 프로젝트가 다루는 비즈니스 Entity
  7. Shared - 모든 Layer에서 재사용되는 코드(라이브러리, 유틸리티 등)

App/Shared Layer는 Slice 없이 Segment로 구성됩니다.
상위 Layer는 자신보다 하위 Layer를 참조 할 수 있지만, 하위 Layer가 상위 Layer를 참조하는 것은 허용되지 않습니다.
예를 들어 pages는 features나 entities의 모듈을 참조할 수 있지만, features가 pages를 참조하는 것은 금지됩니다.

 

 

 

https://feature-sliced.github.io/documentation/kr/docs/guides/migration/from-custom

 

기존 아키텍처에서 FSD로의 마이그레이션 | Feature-Sliced Design

이 가이드는 기존 아키텍처를 Feature-Sliced Design(FSD) 으로 단계별 전환하는 방법을 설명합니다.

feature-sliced.github.io

 해당 페이지 전용 폴더를 만들고 그 안에 index.tsx 파일을 추가해 진입점(entry point) 를 노출합니다.

 

라고 되어있으니 그렇게 해보겠다.

 

 

기존에 1depth여서 편안했던 구조를, 페이지별 폴더와 엔트리 포인트 구조로 변경했다.

 

 src/shared 폴더를 만들고, 📁 pages 또는 📁 routes를 import하지 않는 모든(파일)은 이 폴더로 모읍니다.
📁 src/app 폴더를 만들고, 📁 pages 또는 📁 routes를 import하는 모듈과 라우트 정의 파일은 이 폴더에 배치합니다.

Shared layer는 slice 개념이 존재하지 않기 때문에, 서로 다른 segment 간에도 자유롭게 import할 수 있습니다

 

음..!  라우터를 App에 선언해놓았는데 분리하고 app으로 옮기고 나머지 컴포넌트들을 우선 다 shared에 때려박겠다. 프로바이더 종류는 app에 옮기고, shared에는 jotai의 atom도 포함.

 

헷갈리는 것들은 예제를 따라가고. 우선 이런 식으로 나누었고 Router도 엔트리 포인트를 만들었다.

목표한대로 app / pages / shared 세 폴더로 나누었다.

 

이제 페이지간 shared-imports를 해결하며 한 페이지 안에서만 사용하는 것은 페이지 슬라이스로 옮겨 정리한다.

이 프로젝트는 간단한 프로젝트라  크게 옮길만한 것은 없고, shared에 옮겨놨던 컴포넌트 일부를 home으로 옮기며 여기서만 사용하는 atom 이 있었기 때문에 그것도 가져왔다.

 

무엇을 위해 존재하는지를 기준으로 세그먼트를 정리한다. 

 

이런 작품에 대한 내용은 Film 페이지에서만 사용되기 때문에 pages의 film slice의 model로 옮겼다.

슬슬 헷갈리기 시작한다.

 

Footer 같은 나중에라도 비즈니스 로직으로 오염이 될 여지가 있는 것은 widgets으로 이동했다.

배경으로 쓰이는 파티클 애니메이션도 widgets로 이동시켜서 레이아웃에서 사용하도록 했다.

 

app/layout/CommonLayout.tsx

import { Outlet } from 'react-router-dom';
import Stars from '@/widgets/Stars';
import Footer from '@/widgets/Footer';
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';

export const CommonLayout = () => {
  const { pathname } = useLocation();
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);
  return (
    <div className='w-screen min-h-screen'>
      <Stars />
      <Outlet />
      <Footer />
    </div>
  );
};

레이아웃에서 Outlet을 사용하며 Footer와 Stars가 사용되도록 했다. 

shared의 ui에는 순수 프레젠테이션 컴포넌트만 남게 되었다.

 

이 프로젝트에는 이외엔 비즈니스 로직이라든가 api가 없기 때문에 간단하게 FSD 아키텍처로 마이그레이션이 가능했다. 

 

우려했던 것 만큼 어렵거나 복잡하지 않고 일관성 있으며 예측 가능하고 누가 와서 보더라도(그 사람이 FSD를 안다면...) 인지하기가 쉽다.

 

그런데 FSD에서 어렵고 번거로운 건 사실 Features와Entities 이기 때문에 그런 것 같기도 하다. 점진적 도입을 위한 순서도 'Import 위반을 하나씩 해결하면서, 코드에서 로직을 분리해 entities와 features로 옮깁니다'가 마지막이다.

 

 

우선 나는 features와 entities 디렉토리를 추가적으로 만들었다.

여기까진 좋았는데 그래서 만약에 훅을 만들고 싶으면 어떻게 할 것인가? 

 

export const useScrollToTop = () => {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);
};

이런 공통으로 쓰이는 훅은 @/shared/lib/hooks에 집어넣었다. 

 

이제 좀 더 세부적으로 컴포넌트를 나눠보려고 한다.

 

상관 없는 이야기지만 <킬링 로맨스>라는 영화를 꼭 봐주시길 바랍니다. 

설령 이걸 보고 재미가 없었다 하더라도 저는 한 명이라도 이 영화를 본 사람이 늘어난다면 그것만으로 만족합니다.

 

상영작 페이지에는 스와이퍼 컴포넌트가 존재한다.  이건 어떻게 FSD적으로 다룰 수 있을까?

슬슬 헷갈려서 제미나이에게 질문을 했는데, 각각의 카드를 엔티티, 그리고 스와이퍼를 위젯으로 두라고 하고 있다.

 

보통 엔티티를 명사, 피처를 동사로 비유하고는 하는데 조회성이라 카드는 엔티티의 컴포넌트가 되는 것이고 그것을 조합한 스와이퍼는 위젯이 된다. 

 

이 앱에는 티켓 구매 기능이 있는데 물론 가짜다... 하지만 FSD상으로 티켓 구매 버튼이 피처로 분리하면 적절할 것이다... 기능이 실제론 있는데 비활성화 된 거라고 가정하고 FSD적으로 분리를 할 수 있다.

 

button → shared/ui/Button 으로 버튼 공통 컴포넌트로 만든다.

구매 버튼 → features/buy-ticket/ButTicketButton 에 추가한다. 

(만약 구매 api가 있을 경우 feature/buy-ticket/api에 함수를 만든다.)

 

티겟 그림 컴포터는트는 페이지 슬라이스 내부의 ui 컴포넌트로 만든다.

행사 정보 페이지에는 카카오 지도와 연동하여 지도를 표시하고 있는데 이 부분은 어떻게 FSD로 나눌 수 있을까?

import { useRef } from 'react';
import { useKakaoMap } from '@/shared/lib';

const KAKAO_KEY = ''
const locationLatLng = { lat: 37.5569115255206, lng: 126.867878866131 };

export const EventMap = () => {
  const mapRef = useRef<HTMLDivElement | null>(null);

  useKakaoMap({
    mapRef,
    appKey: KAKAO_KEY,
    center: locationLatLng,
    onMarkerClick: () => {
      window.open('https://map.kakao.com/link/map/472996223');
    },
  });

  return <div ref={mapRef} className='w-full md:w-96 aspect-video mt-3'></div>;
};

페이시 슬라이스 내 EventMap 컴포넌트를 만들고 (왜냐하면 카카오맵은  우리 프로젝트의 비즈니스와는 상관이 없기 때문.)  useKakaoMap이라는 커스텀 훅을 추가하여 초기화 되도록 했다. 이 커스텀훅은 shared/lib/hooks에 들어간다.  일단은?

 

그리고 계속 신경이 쓰이는 홈에만 있는 '메뉴'컴포넌트를 개선한다. 

 

shared/ui/button을 통해서 widgets/menu 아래에 Menu.tsx와 MenuTrigger.tsx 컴포넌트를 추가했다! 

메뉴 온오프를 위한 atom 은 shared/model아래로 옮긴다. 

 

src/app/layout/CommonLayout.tsx

최종적으로 레이아웃에서 조립한다.

import { Outlet, useLocation } from 'react-router-dom';
import { Stars, Footer, Menu, MenuTrigger } from '@/widgets';
import { useScrollToTop } from '@/shared/lib';

export const CommonLayout = () => {
  const { pathname } = useLocation();
  useScrollToTop();

  const isHome = pathname === '/';

  return (
    <div className='w-screen min-h-screen'>
      <Stars />
      <Menu />
      {!isHome && <MenuTrigger />}
      <Outlet />
      <Footer />
    </div>
  );
};

 

이렇게 해서 FSD로 기존 프로젝트를 간단하게 마이그레이션 해보았다. 

 

앞서 말했듯이 이건 딱히 비즈니스 로직도 없고 API 연동도 없고 볼륨도 작은 간단한 프로젝트이기에 😅 기존 방식이 크게 나쁠 것은 없었지만 FSD로 비교적 쉽게 전환을 할 수 있었다! 내 지난 투덜거림과는 달리, FSD 방식이란 것이 (뭘 어디에 둘지 고민하는 초반의 어려움을 지나치면) 의외로 예측 가능하고 일관성 있고 정해진 방식으로 확장해나갈 수 있다는 점이 슬슬 눈에 들어오고 있다. 그리고 어디다 넣을지 헷갈리는 게 이제 큰 문제는 아닐 수 있는 게 어차피 ai 한테 물어보면 된다. 이런 분야에 대해서는, ai가 전문가다. 

 

https://github.com/milmilkim/project-merge

 

GitHub - milmilkim/project-merge

Contribute to milmilkim/project-merge development by creating an account on GitHub.

github.com

최종적으로 변경된 것은 위의 깃 레포에 반영되어 있다. 당연하게도 우상단 메뉴 버튼 말고는 보이는 건 바뀐 게 없다.

저작자표시 비영리 (새창열림)

'개발' 카테고리의 다른 글

크롬 말고 엣지 브라우저 자동 번역은 또 어떻게 문제를 일으키는가  (2) 2026.06.21
구글 크롬 자동번역과 Next 500 에러  (0) 2026.05.10
내가 없는 동안 Vue3.5+에는 무슨 일이 생겼나  (0) 2026.01.16
SSR 간략하게 해체하기  (0) 2026.01.16
AI로 개발하기 3: OpenAI-compatible API 활용하기  (0) 2026.01.05
'개발' 카테고리의 다른 글
  • 크롬 말고 엣지 브라우저 자동 번역은 또 어떻게 문제를 일으키는가
  • 구글 크롬 자동번역과 Next 500 에러
  • 내가 없는 동안 Vue3.5+에는 무슨 일이 생겼나
  • SSR 간략하게 해체하기
밀밀 킴
밀밀 킴
생계형개발자
  • 밀밀 킴
    Bug & Burger
    밀밀 킴
    • 전체 (57)
      • 개발자적삶 (18)
      • 개발 (22)
      • 보관 (12)
        • 티스토리 스킨 (5)
  • hELLO· Designed By정상우.v4.10.5
밀밀 킴
FSD 맛보기
상단으로

티스토리툴바