과제를 풀며 했던 폴더구조에 대한 고민

2025-11-30
  • architecture

🏃‍♀️ 들어가며

최근에 과제를 풀면서 코드 베이스에 대한 퀄리티 고민을 많이 하다보니 자연스레 폴더구조를 어떻게 구성해야 할까란 생각도 많이 하게 됐다. 그 고민에 대한 나름대로의 해답을 내리게된 사고의 과정을 정리해보려고 한다. 고민은 Feature-Sliced Design(이하 FSD)부터 시작되었기 때문에 중점적으로 이에 대해 다뤄볼 예정이다.

⚙️ 역할 중심 분리

components, utils, hooks와 같이 코드의 역할 중심으로 폴더구조를 구성하는 것도 큰 문제가 되진 않았었다. 사이드 프로젝트를 주로 했었는데 프로젝트의 규모도 규모지만 유지보수 기간도 길지 않았었기 때문에 완성된 상태에서 수정할 일이 많지 않아 불편함을 크게 느끼지 않았던 것 같다.

이 구조에 대해 고민이 생겼던 것은 실무에서 규모가 큰 프로젝트를 유지보수하게 되었을 때다. 하나의 폴더에 파일이 많아지면서 도메인을 기준으로 폴더를 나눠서 구성해두었지만, 유지보수 시 관련 있는 코드를 보며 흐름을 따라가야 할 때면 파일이 너무 많아 추적하기가 불편했다. components/domain/ 정도에서 분리가 끝나는 게 아니라 그 하위에도 폴더가 많았기 때문에 관련 있는 코드를 각각의 util, store, constants 등등의 폴더에서 찾아가야 하는 번거로움이 있었다.

폴더를 훑어 봤을 때 A 도메인과 관련된 코드는 이게 전부구나 라는 확신이 점차 들지 않는다는 것과 복잡하다보니 이해하기 어려워진다는 게 내게 문제가 되었던 것 같다. 문제 인식은 했지만, 이 문제를 명쾌하게 해결할만한 해법이 떠오르지 않아 답답했었다.

🪄 기능 중심 분리

그러다가 알게 된 게 FSD였는데 ‘어떻게 하면 관련 코드들을 가까이에 두고 로직의 흐름을 이해하기 수월하게 할 수 있을까?‘에 대한 방식을 제시해주었다고 생각했다. FSD로 과제도 풀어보고 실제 회사 프로젝트에 팀원들과의 상의 끝에 도입도 해봤었다. 그 경험에 대해 얘기해보려고 한다.

도입했을 때 발생하는 문제점

FSD 아키텍처에 대해 가장 많이 나오는 얘기가 이 파일을 feature, widget, entitiy 중 어디에 위치시켜야 하는가다. 나도 처음 FSD에 대해 접했을 때 코드를 어느 레이어에, 어느 슬라이스에, 어느 세그먼트에 둬야할지에 대한 고민이 너무 깊어졌었다. 가이드라인은 있지만, FSD 문서에서도 명확한 예시랄 게 없었고(예시 프로젝트 모음이 있지만 프로젝트끼리 기준이 달랐다) 경계가 생각하기 나름인 것 같아서 대체 어디에 넣어야 하는가에 대해 많이 몰두하게 됐다.

팀원들과도 그 부분에 대해서 가장 얘기를 많이 했다. 내가 했던 고민을 해결해줬지만, 더 많은 고민 덩어리가 앞에 놓였던 것 같기도 하다. FSD가 도입된 프로젝트는 혼자 유지보수하게 되어서 팀원들과의 이해도를 일치시키는 과정이랄 게 없었지만, 만약 팀이라면 코드 리뷰가 FSD 관련으로 도배가 될 수도 있을 거란 생각을 했다.

폴더구조에 대해 이야기할 시간이 있다면 괜찮겠지만, 바쁘게 비즈니스적인 요구사항을 쳐내야 하는 상황에서 어려워질 것이다. 그러다보면 코드베이스를 예측하기 어려워지는 상황이 만들어지기 쉬울 것 같다.

FSD에 대해서 ‘코드가 파편화된다’라는 이야기도 많다. 코드가 레이어, 슬라이스, 세그먼트라는 기준으로 분리가 되어 여기저기 나눠지게 된다.

// 예시 TODO APP
// pages/TodoListPage.tsx
function TodoListPage() {
  const [input, setInput] = useState('');
  const queryClient = useQueryClient();

  const { data: todos = [], isLoading } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  const addMutation = useMutation({
    mutationFn: createTodo,
    onSuccess: (data) => {
      queryClient.setQueryData(['todos'], (old) => [...old, { ...data, id: Date.now() }]);
      setInput('');
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>TODO List</h1>
      
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add todo..."
        />
        <button onClick={() => input && addMutation.mutate(input)}>
          Add
        </button>
      </div>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

할일을 조회하고, 추가를 할 수 있는 아주 간단한 TODO APP의 코드다. 여기에 FSD를 도입하게 되면 어떻게 될까?

src/
├── entities/
│   └── todo/
│       ├── api/
│       │   └── todoApi.ts          # API 함수들
│       └── model/
│           └── todoQueries.ts      # React Query 훅
│
├── features/
│   └── todo/
│       ├── ui/
│       │   └── AddTodoForm.tsx     # UI 컴포넌트
│       └── model/
│           └── useAddTodo.ts       # 비즈니스 로직
│
└── pages/
    └── TodoListPage.tsx             # 페이지 조합

아주 간단한 프로젝트에 FSD를 도입하는 것은 추천되고 있지 않지만, 예시를 들고자 한다. TodoListPage 한 파일에서 관리되었던 코드는 FSD 규칙에 따라 여기저기 흩어지게 된다.

// FSD 도입 후 
// pages/TodoListPage.tsx
function TodoListPage() {
  return (
    <div>
      <h1>TODO List</h1>
      <AddTodoForm />
      <TodoList />
    </div>
  );
}

이렇게 분리하고 나서 어느 날 다시 TODO 관련 기능을 수정하기 위해 TodoListPage.tsx 파일을 열면 이런 코드를 보게 된다. 이 케이스를 두고 보자면 아예 코드를 분리하지 않았을 때보다 유지보수가 힘들어졌다. 관련 로직들이 파편화 되어서 GOTO 비용이 높아졌고, 관련 코드를 열심히 추적해가며 로직 흐름을 이해해야 한다.

따라서 분리와 응집에 대한 적당한 기준이 좋을 것 같다는 생각을 하게 됐다. ‘UI와 비즈니스 로직은 분리하는 게 좋다’, ‘관련 코드는 같이 있는 게 좋다’라는 상충되는 의견들이 있는데 이 또한 적당함을 찾아가는 게 좋을 것 같다. 컴포넌트를 이해하기 어려워졌을 때 하나씩 트레이드 오프에 대해 고민을 하며 응집과 분리에 대한 고민을 하는 게 좋을 것 같다.

도입 후 얻을 수 있었던 인사이트

FSD는 좋은 설계의 원칙을 위해 어떻게 구조를 그리면 좋은지 방식을 제안해주었다고 생각했고, 개인적으로 이를 통해 코드를 어떻게 관리하는 게 좋은가에 대한 힌트를 얻을 수 있었다.

보통 프로젝트 로직 중에서 가장 중요한 건 수익과 연계된 비즈니스 로직이고, 요구사항에 따라 많이 변하게 될 부분이다. 이들을 안전하게 관리하는 방식을 FSD가 제안하는 것 같다.

도메인 로직끼리 얽히면 로직의 흐름을 이해하기 어려워지고, 그런 코드는 건드리기 어려워진다. A 도메인만 건드렸는데 B, C, D까지 영향을 받고 있다면 변하는 요구사항에 대응하기 쉽지 않아진다. 따라서 도메인 A, B, C, D 각각의 로직이 서로에게 주는 영향을 최소화하기 위해서는 분리 가능한 구조가 필요하다.이에 대한 필요성과 이를 실현할 수 있는 구조를 FSD를 통해 알 수 있었다.

└─ src
   │  // 도메인 맥락이 들어가지 않는 코드
   ├─ components
   ├─ constants
   ├─ hooks
   ├─ utils
   ├─ ...
   │
   └─ domains
      │  // 도메인 A 관련 코드
      ├─ A
      │     ├─ components
      │     ├─ constants
      │     ├─ hooks
      │     ├─ utils
      │     └─ ...
      │
      │  // 도메인 B 관련 코드
      └─ B
            ├─ components
            ├─ constants
            ├─ hooks
            ├─ utils
            └─ ...

또한 entitiy, feature, widget이라는 분리 방식을 제안하면서 복잡하게 관리되고 있는 로직들을 어떻게 분리하면 좋을지에 대한 기준을 제안해주었다.

도메인 로직을 서로 분리시켜야 한다라는 생각으로 이번에 과제를 풀면서 폴더구조를 고민하다 이런 구조를 선택했다. 그러나, ‘장기적인 확장성’이라는 것이 이번 과제의 요구사항이었기 때문에 다양한 케이스에 대해 고민을 해보다가 프로젝트가 커지게 된다면 각각의 domains/A, domains/B 폴더 내부도 예전과 같이 복잡해질 수 있을 거란 생각이 들었다.

문제가 있으니 그 케이스에는 또 다른 구조를 고민해야 할 것 같은데 그러면 이 때 어떤 기준으로 분리하면 좋을까 생각하던 중에 FSD에서 제시한 다음의 레이어가 필요할 수 있을 것 같다는 생각을 하게 됐다.

Widgets - 독립적으로 동작하는 대형 UI·기능 블록

Features - 제품 전반에서 재사용되는 비즈니스 기능

Entities - user, product 같은 핵심 도메인 Entity

Shared - 프로젝트 전반에서 재사용되는 일반 유틸리티

아마 이런 경우라면 하나의 도메인 자체의 크기가 서비스만큼 큰 경우가 아닐까 싶고, 많은 경우는 아닐지 싶다. 완전히 FSD의 레이어, 슬라이스, 세그먼트에 대한 개념을 완전히 도입할 필요성이 있는 것은 아니고 FSD의 개념을 빌려 비슷한 구조를 만들어볼 수 있을 것 같다.

의존성 방향이 정의되어 있는 레이어 계층 구조 또한 차용하기 좋은 개념인 것 같다. 하위 계층은 여러 레이어에 의한 참조가 많이 일어나고, 범용적으로 사용되기 때문에 변경이 자주 일어나는 상위 계층에 의존하면 불안정해진다. 이에 대해 FSD는 레이어가 각각의 하위 계층만 참조할 수 있다다는 규칙이 있어서 계층이 낮은 레이어의 안정성을 유지할 수가 있다. 이런 개념을 코드를 작성할 때 염두에 둔다면 보다 안정적으로 모듈 간의 의존성을 관리할 수 있을 것 같다.

과제를 하면서 FSD를 사용하지 않지 말자라는 생각으로 고민을 시작했는데, FSD의 개념에서 코드를 작성할 때 어떤 기준을 세우면 좋을지 힌트를 얻을 수 있었던 것 같다.

💻 마무리하며

이런 저런 생각을 거듭하면서 결국은 상황에 따라 적합한 선택을 하는 것이 가장 중요하다라는 생각을 다시금 하게 되는 것 같다. 프로젝트의 규모, 성격, 도메인, 유지보수 기간 등등에 대한 고민을 통해 차근차근 정해가면 좋을 것 같다.

어쨌거나 구조라는 것에 너무 몰두하다보면 정해놓은 구조에 맞게 코드를 관리하려고 하면서 가장 중요했던 문제는 간과되는 상황이 발생하는 것 같다. 어떤 방법을 사용할 것인가도 고민할 문제지만, 궁극적으로는 FSD가 구조를 통해 이루고자 했던 목적과 같은 것들에 집중하다 보면 유연하게 상황에 따라 대처할 수 있을 거라 생각한다.

Profile picture

박세리

Frontend Developer