
썸네일 이미지는 AI를 활용했지만, 글은 모두 직접 작성했습니다.
FSD(Feature-Sliced Design)는 비슷한 코드들을 가깝게 두고(co-location), 의존 방향을 통제해 변경 영향 범위를 줄이기 위한 구조화 방식이다.
FSD 자체에 대해서는 더 좋은 폴더 구조 만들기 : FSD 철학에 Deep dive 글에서 다루어보았고, 이번에는 내 프로젝트에 실제로 적용해보며 어떤 기준으로 구조를 바꾸었는지를 기록해보려고 한다.
React + Vite + TypeScript로 기본 프로젝트 폴더를 만들고, Figma Make로 디자인 시안도 만들었다. 이제 본격적인 개발을 시작하면 되는데, 막상 비어 있는 공간을 보니 이곳에 어떤 파일들을 채워가야 할지 막막하기만 하다.
첫 컴포넌트를 냅다 만들어보기 전에, 앞으로 계속 만들고 지우고 수정하게 될 이 공간에 어떤 내용들을 채워가면 좋을지 뼈대를 잡아보기로 했다. 폴더 구조를 고민하다보니 자연스럽게 구현하고 싶은 기능들을 정리해봐야겠다는 생각이 들었다.
예를 들어 뉴스 모달을 만들 때는 대략 이런 TODO를 적어두었다.
뉴스 모달 TODO
이런 식으로 화면별 TODO를 정리하고 나니, 내가 만들어야 할 것들에 대해 조금씩 감이 잡히기 시작했다. 초반 구상 과정이 생각보다 오래 걸리긴 했지만, 처음부터 할 일을 명확하게 정의해두니 작업 중 헤매는 시간을 줄일 수 있었다.
처음에는 아직 감이 잘 오지 않아서 FSD 공식 가이드의 레이어를 최대한 그대로 따라가 보기로 했다. app / pages / widgets / features / entities / shared를 먼저 만들고, 각 레이어에 파일을 몇 개씩 넣어보는 식으로 출발했다.
초기 구조는 대략 이런 모습이었다.
📂 src
├─ 📂 app
│ ├─ providers/
│ ├─ router/
│ ├─ App.tsx
│ └─ main.tsx
├─ 📂 pages
│ ├─ home/
│ │ └─ ui/HomePage.tsx
│ ├─ issue/
│ │ └─ ui/IssuePage.tsx
│ ├─ login/
│ │ └─ ui/LoginPage.tsx
│ ├─ search/
│ │ └─ ui/SearchPage.tsx
│ └─ stock/
│ └─ ui/StockPage.tsx
├─ 📂 widgets
│ ├─ header/
│ │ └─ ui/Header.tsx
│ ├─ footer/
│ │ └─ ui/Footer.tsx
│ ├─ newsModal/
│ │ └─ ui/NewsModal.tsx
│ └─ newsList/
│ └─ ui/NewsList.tsx
├─ 📂 features
│ ├─ bookmark/
│ │ └─ ui/Bookmark.tsx
│ └─ save/
│ └─ ui/save.tsx
├─ 📂 entities
│ ├─ ticker/
│ │ └─ ui/Ticker.tsx
│ └─ user/
│ └─ ui/User.tsx
└─ 📂 shared
├─ assets/images/
└─ ui/
├─ Button.tsx
├─ Input.tsx
├─ Modal.tsx
├─ Tag.tsx
└─ InfoBox.tsx
당시에는 이 구조가 꽤 그럴듯해 보였다. 하지만 파일이 조금씩 늘기 시작하자, 정석대로 따라하는 것과 지금 프로젝트에 맞는 구조를 만드는 것이 전혀 다른 문제라는 걸 금방 느낄 수 있었다.
각 레이어를 처음 바라봤을 때의 판단은 이랬다.
app
엔트리, 라우팅, 프로바이더 같은 앱 설정을 두었다. 앱 설정을 처음 직접 구성하다 보니, 어떤 걸 넣어야 하는지 감이 잘 안와서 가장 기본적인 구조만 잡아두었다.
pages
페이지 단위 화면은 비교적 쉽게 나눌 수 있었다. 다만 뉴스 모달처럼 화면이긴 하지만 라우트 단위는 아닌 것은 어디에 둬야 할지 애매했다.
widgets
Header, Footer, NewsModal, NewsList를 넣었다. 그런데 여기서부터 슬슬 기준이 흔들리기 시작했다. “사용 단위로 모아둔 것”이라고는 하지만, 그 단위를 어디까지 위젯으로 봐야 하는지가 꽤 모호했다.
features
설명만 보면 “사용자 행동”을 담는 곳인데, 초반에는 어떤 것들이 features인지 감이 잘 오지 않았다. 그래서 일단 동작처럼 느껴지는 것들을 이곳에 두며 감을 잡아보려 했다.
entities
Ticker, User를 만들었다. 처음에는 “서비스 관련 데이터는 여기로 오면 되겠구나” 정도로 이해했다. 다만 데이터를 다루는 컴포넌트만 두어야 하는지, API 호출이나 가공 로직도 함께 들어가야 하는지 여전히 고민이 되었다.
shared
Button, Input, Modal, Tag, InfoBox를 넣었다. shared를 쓰레기통처럼 쓰고 싶지 않아서, 여러 곳에서 재사용될 가능성이 높은 기본 컴포넌트만 넣으려고 했다.
폴더 구조를 혼자 처음부터 제대로 설계해보는 경험 자체가 처음이었다. 그래서 막연히 “좋은 구조를 만들겠다”보다, 만들면서 부딪힌 질문들에 하나씩 답하는 방식으로 기준을 세우게 됐다.
FSD를 보면 가장 안쪽 계층으로 ui, model, api 같은 segment가 등장한다. 문제는 당시 내 프로젝트에서는 ui 외에 무엇이 실제로 들어갈지 아직 선명하지 않았다는 점이었다.
그래서 대부분의 slice 안에 ui/만 만들어두었더니, 구조가 자연스럽게 Layers > Slices > ui > component.tsx 형태가 됐다. 파일 수는 많지 않은데 뎁스만 깊어지는 느낌이 들었다.
내가 내린 결론
segment는 처음부터 과하게 쪼개지 않는다.
우선은 ui 중심으로 시작하고, 파일이 실제로 늘어나 model / api / lib 분리가 필요해질 때 그때 추가한다.
이 질문은 생각보다 오래 붙잡고 있었다.
이 고민을 하면서 결국 분리해야 할 건 모달이라는 틀과 뉴스라는 콘텐츠라는 생각이 들었다.
내가 내린 결론
재사용 가능한 Modal Shell은 shared에 두고, 뉴스 데이터를 담아 실제 내용을 보여주는 NewsModal은 News 안에 둔다. 즉, 공용은 껍데기까지만 공용으로 두고, 도메인 내용은 기능 안에 둔다.
(처음에는 widgets가 맞다고 생각했지만, 나중에 widgets 레이어를 유지하지 않고 기능 중심으로 재구성하면서 News 폴더로 옮기게 됐다.)
처음에는 FSD에서 말하는 public API를 의식해서, export가 필요한 폴더마다 index.ts를 만들어보았다. 그러자 곧바로 관리 포인트가 늘어났다. 구조를 설명하려고 만든 장치가 오히려 작은 프로젝트에서는 파일 수를 늘리고 경로를 더 복잡하게 만드는 원인이 되었다.
내가 내린 결론
public API(index.ts)는 모든 곳에 일괄 적용하지 않고, 경계가 필요한 곳에만 둔다.
예를 들어 shared/index.ts처럼 외부에서 가져다쓰는 진입점에는 두되, 모든 작은 컴포넌트 폴더마다 만들지는 않는다.
이렇게 정리하니 import 경로도 단순해졌고, “지금 이 프로젝트에서 진짜 경계가 필요한 곳이 어디인가”를 더 명확하게 보게 됐다.
몇 번 구조를 갈아엎고 나서 내린 결론은 초반부터 모든 폴더 구조를 완성하려고 하지 말자는 것이었다.
빠르게 MVP를 만들며 컴포넌트를 구현해야 하는 시점인데, 미리 만든 깊은 폴더 구조와 과한 public API가 오히려 작업 속도를 늦추고 있었다. 정석을 따라하다보니, 기능보다 폴더가 더 빨리 늘어났다.
그래서 co-location 원칙만은 살리되, 내가 지금 직접 이해하고 통제할 수 있는 범위 안에서 features 중심의 축소형 구조로 다시 정리했다.
현재 구조는 아래와 같다.
📂 src
└─ 📂 features
├─ 📂 app
│ ├─ 📂 Routes/AppRoutes.tsx
│ └─ App.tsx
├─ 📂 Issues
│ ├─ 📂 hooks/useIssueNavigation.ts
│ └─ 📂 ui
│ ├─ 📂 IssueCard/IssueCard.tsx
│ ├─ 📂 IssueDetailCard/IssueDetailCard.tsx
│ ├─ 📂 NewsTag/NewsTag.tsx
│ ├─ 📂 StockChips/StockChips.tsx
│ ├─ 📂 TypeIcon/TypeIcon.tsx
│ └─ index.ts
├─ 📂 layout
│ ├─ 📂 AppBarLayout/AppBarLayout.tsx
│ ├─ 📂 Header/Header.tsx
│ ├─ 📂 MainLayout/MainLayout.tsx
│ ├─ 📂 SectionEmpty/SectionEmpty.tsx
│ ├─ 📂 SectionHeader/SectionHeader.tsx
│ ├─ 📂 SectionWrapper/SectionWrapper.tsx
│ ├─ 📂 TwoColumnGrid/TwoColumnGrid.tsx
│ └─ index.ts
├─ 📂 News
│ ├─ 📂 NewsCard/NewsCard.tsx
│ └─ 📂 NewsModal/NewsModal.tsx
├─ 📂 pages
│ ├─ 📂 IssueDetailPage/IssueDetailPage.tsx
│ ├─ 📂 IssuePage/IssuePage.tsx
│ ├─ 📂 MainPage/MainPage.tsx
│ ├─ 📂 StockDetailPage/StockDetailPage.tsx
│ └─ 📂 StockPage/StockPage.tsx
├─ 📂 shared
│ ├─ 📂 images/
│ ├─ 📂 lib
│ │ └─ 📂 format
│ │ ├─ formatNumber.ts
│ │ └─ formatPercentage.tsx
│ ├─ 📂 styles
│ │ ├─ 📂 fonts/
│ │ ├─ base.css
│ │ ├─ fonts.css
│ │ └─ tokens.css
│ ├─ 📂 ui
│ │ ├─ 📂 Button/Button.tsx
│ │ ├─ 📂 EffectTag/EffectTag.tsx
│ │ └─ 📂 Icons/Icons.tsx
│ └─ index.ts
└─ 📂 Stocks
├─ 📂 ui
│ ├─ 📂 StockCard/StockCard.tsx
│ └─ 📂 StockDetailCard/StockDetailCard.tsx
└─ index.ts
이 구조도 아직 확정된 답은 아니다. 다만 지금의 나에게는 폴더만 보고도 어느 정도 “이 안에 뭐가 있겠구나”를 예측할 수 있는 구조라는 점에서 작업 효율을 높이는 데 도움을 주고 있다.
실제 작업을 진행하며 폴더 구조를 여러번 바꾸게 되었는데, “아, 이건 여기 두는 게 아니었구나!” 싶어서 위치를 바꾼 사례들을 몇 가지 남겨보려고 한다.

영향도를 나타내는 태그 컴포넌트이다. 처음에는 이슈의 영향도를 보여주는 UI라고 생각해서 Issues 안에 두었다.
그런데 작업을 진행하다 보니 News에서도 같은 성격의 태그가 필요해졌다. 이 시점에서 이 컴포넌트가 정말 이슈 전용인지 다시 보게 됐다. 결과적으로는 특정 도메인보다 “영향도를 표현하는 방식”에 더 가까운 컴포넌트라고 판단했고, shared로 옮기게 되었다.
즉, 두 군데 이상에서 쓰이고 도메인 의미가 옅다면 shared로 올린다는 기준을 이 사례에서 처음 체감하게 되었다.
이슈 타입별 아이콘을 보여주는 컴포넌트였다. 처음에는 TypeIcon 안에서 MUI 아이콘을 직접 import해서 사용했다.
하지만 작업을 하다 보니, 비슷한 아이콘이 다른 화면에서도 조금씩 필요해지기 시작했다. 그러자 아이콘 import가 여기저기 흩어졌고, “프로젝트에서 어떤 아이콘을 쓰는지”를 한눈에 관리하기 어려워졌다.
그래서 shared에 Icons 파일을 만들어 프로젝트에서 사용하는 아이콘을 한곳에서 import/export 하도록 정리했다. 이후 TypeIcon에서는 그 파일만 바라보도록 바꾸었다.
이렇게 바꾸고 나니 구조도 정리됐고, 아이콘 변경이나 추가도 훨씬 수월해졌다. 작은 차이처럼 보여도, 공통 리소스의 진입점을 만드는 것이 관리 비용을 꽤 줄여준다는 걸 느꼈다.

처음에는 전역 types/Types.tsx 파일 하나를 만들어 전체 타입을 넣어두었다. 초반에는 양이 많지 않아서, 이슈용 interface 하나, 주식용 interface 하나 정도를 한 파일에 모아도 크게 불편하지 않았다.
문제는 시간이 지나면서였다. 기능이 늘어나자 타입 정의도 함께 늘었고, 결국 한 파일 안에 여러 도메인의 타입이 뒤섞이기 시작했다. 그 순간부터는 “한곳에 모아두었기 때문에 편한 것”보다, “찾을 때마다 문맥 전환이 필요한 것”이 더 크게 느껴졌다.
그래서 전역 types 폴더를 없애고, Issues / Stocks / News 각 feature 안에 types.ts 파일을 따로 두는 방식으로 바꿨다. 이렇게 정리하고 나니 각 기능에서 필요한 타입이 훨씬 빠르게 눈에 들어왔다.
전역으로 모은다고 항상 좋은 것이 아니라, 관련 있는 것끼리 가까이 두는 편이 더 읽기 쉬울 때가 많다는 것을 느꼈다.

지금 가장 손이 가는 부분은 layout 폴더다. layout이라는 이름 자체가 꽤 넓고 추상적이다 보니, 여러 성격의 컴포넌트가 한 폴더 안에 함께 들어가 있다.
지금 들어 있는 것들을 역할별로 나눠 보면 대략 이렇다.
이 정도로 역할이 갈리면, 이미 layout이라는 이름 하나로 묶기엔 범위가 넓어진 것 같다는 생각이 든다. 아직은 정답 구조를 찾았다기보다 다음 리팩토링 후보가 무엇인지 보이기 시작한 상태에 가깝다.
그래서 이 부분은 지금 당장 결론을 내리기보다, 파일이 더 늘어나는 시점에 역할별 하위 폴더로 나누거나 위치를 다시 조정하는 방향을 고려하고 있다.
이번 경험에서 중요했던 건, 내가 FSD를 얼마나 정확하게 재현했느냐가 아니었다. 오히려 더 중요했던 건 폴더만 봐도 “이 안에 어떤 파일이 있겠구나”를 예측할 수 있는 구조인가였다.
폴더를 여러 번 갈아엎으며 얻은 기준은 아래와 같다.
결국 이번 글은 “FSD를 이렇게 적용하는 것이 정답이다”라는 이야기가 아니다. 오히려 작은 프로젝트에서 정석을 그대로 들여오기보다, 내가 감당할 수 있는 규칙의 무게로 시작하는 편이 더 현실적이었다는 기록에 가깝다.
이 구조도 아직 진행형이다. 프로젝트를 완성할 즈음에는 또 다른 기준이 생길 수 있다. 그때는 처음 왜 이렇게 나눴고, 나중에는 왜 바꾸었는지까지 포함해 최종 구조를 다시 정리해보려고 한다.
다음 글에서는 CSS, SCSS, styled-components, Tailwind 등 여러 스타일링 방식을 직접 바꿔가며 비교한 기록을 남겨보려고 한다. 어떤 방식이 더 편한지 정도가 아니라, 실제 프로젝트에서 무엇이 유지보수하기 좋았고 무엇이 오히려 불편했는지까지 정리해볼 생각이다.
퍼블리셔로 일하며 가장 오래 다뤄온 영역이라, 이번 작업에서도 유난히 재미있게 붙잡고 있었던 주제이기도 하다. 최종적으로 어떤 방식을 선택했는지 궁금하다면 다음 글에서 이어서 적어보겠다.
프로젝트 진행 과정이 궁금하다면 아래 저장소에 놀러와주어도 좋다. (❤️)