소잃고 폴더 구조 고치기

오늘처음해요·2025년 8월 5일
1
post-thumbnail

오늘 시간을 갈아넣어서 기존의 프론트엔드 폴더 구조를 전부 개선하는 작업을 했습니다.
프로젝트 중간에 폴더 구조를 싹다 바꾸는 게 굉장히 부담스럽고 시간이 많이 드는 작업이지만, 과감히 결정하게 된 과정을 공유드리겠습니다

기존 폴더 구조

기존 프로젝트는 역할 중심으로 폴더를 나누는 방식으로 구성되어 있었습니다. UI 컴포넌트는 components/, 페이지는 pages/, 전역 상태는 stores/, 타입은 customTypes/관심사의 분리에 초점을 둔 구조였습니다.

📦 src
├── 📁 api                  // API 호출 함수들
├── 📁 assets               // 정적 리소스 (이미지, 아이콘 등)
├── 📁 components           // 페이지에 공통적으로 사용되는 UI 컴포넌트
│   ├── 📁 common           // Header, Router 등 공용 컴포넌트
│   ├── 📁 homePage         // 홈 페이지 전용 컴포넌트
│   ├── 📁 loginPage        // 로그인 페이지 전용 컴포넌트
│   └── 📁 practicePage     // 실습 페이지 전용 컴포넌트
├── 📁 constants            // 상수 정의
├── 📁 customTypes          // 타입스크립트 타입 정의
├── 📁 hooks                // 커스텀 훅 정의
├── 📁 layouts              // 공용 레이아웃 컴포넌트
├── 📁 mocks                // 테스트용 목 데이터
├── 📁 pages                // 라우팅 단위의 실제 페이지 컴포넌트
│   ├── 📁 homePage
│   ├── 📁 loginPage
│   ├── 📁 myPage
│   ├── 📁 pairPracticePage
│   ├── 📁 practicePage
│   └── 📁 soloPracticePage
├── 📁 stores               // Zustand 기반 전역 상태 관리
├── 📄 App.tsx              // 루트 컴포넌트
├── 📄 main.tsx             // 앱 진입점
└── 📄 index.css            // 글로벌 스타일

기존 코드는 관심사별로 코드를 분리하는 데에는 좋았지만, 문제는 관심사로 분리하다 보니 하나의 기능에 필요한 코드가 여러 폴더에 흩어지는 문제가 있었습니다.

그래서 리팩토링하거나, 기능을 추가하게 될 경우 너무 많은 폴더를 이동해서 수정해야하는 불편함이 있었고 생산성도 떨어졌습니다.

예를 들어 practicePage에 간단한 버튼 컴포넌트를 수정한다고 가정해보겠습니다.

📦 src
├── 📁 components/practicePage/
│   └── 📄 FeedbackButton.tsx          ← UI 컴포넌트 정의
├── 📁 hooks/practicePage/
│   └── 📄 useFeedbackHandler.ts       ← 버튼 클릭에 사용할 커스텀 훅
├── 📁 customTypes/practicePage/
│   └── 📄 FeedbackButtonProps.ts      ← 버튼의 Props 타입
├── 📁 constants/
│   └── 📄 feedbackConstants.ts        ← 버튼에서 사용할 상수 정의
├── 📁 stores/
│   └── 📄 useFeedbackStore.tsx        ← 상태 저장 및 전역 관리
├── 📁 pages/practicePage/
│   └── 📄 index.tsx                   ← 페이지에서 버튼 컴포넌트 사용

단 하나의 기능을 구현하기 위해 6개의 폴더를 이동해야 했고, 폴더 내에서 해당 파일을 찾기도 힘들었습니다.

그래서 폴더 구조를 변경해야겠다는 필요성을 느끼고 여러 자료를 공부하던 중 하나의 키워드를 찾았습니다

토스 모닥불 - 디렉토리 관리
토스 - 프론트엔드 지침서

응집도

토스에서는 수정하기 쉬운 코드좋은 코드라고 합니다.

여기서 수정은 무엇을 의미할까요?

객체지향을 공부하셨던 분들은 OCP에서는 확장에는 열려있고, 수정에는 닫힘이라고 했는데, 왜 수정하는 게 좋은 코드이지 의아해 하실 수도 있습니다.

OCP에서 말하는 수정은, 새로운 요구사항이 생겼을 때 기존 코드를 변경하지 않고 새로운 코드를 추가하여 확장할 수 있어야 한다는 의미입니다.

반면 토스에서 말하는 수정하기 쉬운 코드는, 어떤 기능을 변경하거나 개선할 때 관련된 코드들이 한 곳에 모여 있어 쉽게 파악하고 수정할 수 있는 코드를 말합니다.

그것이 바로 응집도가 높은 코드입니다.

응집도가 높아지려면 어떻게 해야할까요?

응집도가 높아지려면 역할 기반이 아닌 기능 기반으로 코드를 모으면 됩니다.

예를 들어, 아래처럼 하나의 기능을 하는 코드들이 멀리 떨어져 있으면 수정하기 불편할 겁니다

before

📦 src
├── 📁 components/practicePage/
│   └── 📄 FeedbackButton.tsx          ← UI 컴포넌트 정의
	...(수 없이 많은 파일들)
    ...
    ...
├── 📁 hooks/practicePage/
│   └── 📄 useFeedbackHandler.ts       ← 버튼 클릭에 사용할 커스텀 훅
	...
    ...
    ...
├── 📁 customTypes/practicePage/
│   └── 📄 FeedbackButtonProps.ts      ← 버튼의 Props 타입
	...
    ...
    ...
├── 📁 constants/
│   └── 📄 feedbackConstants.ts        ← 버튼에서 사용할 상수 정의
	...
    ...
    ...
├── 📁 stores/
│   └── 📄 useFeedbackStore.tsx        ← 상태 저장 및 전역 관리
	...
    ...
    ...
├── 📁 pages/practicePage/
│   └── 📄 index.tsx                   ← 페이지에서 버튼 컴포넌트 사용
	...
    ...
    ...

after

하지만 이렇게 기능 단위로 묶이게 되면, 응집성이 높아 기능 수정이나 추가가 수월해집니다.

한 눈에 봐도 기능으로 묶이는 게 편해보이죠?


📦 src
├── 📁 features
│   └── 📁 feedbackButton
│       ├── 📄 index.ts               // 컴포넌트 export
│       ├── 📄 FeedbackButton.tsx     // UI 컴포넌트
│       ├── 📄 useFeedback.ts         // 관련 커스텀 훅
│       ├── 📄 feedbackStore.ts       // Zustand 전역 상태
│       ├── 📄 types.ts               // 타입 정의
│       └── 📄 constants.ts           // 상수 정의
├── 📁 pages
│   └── 📁 practicePage
│       ├── 📄 index.tsx              // 피드백 버튼 import 및 사용

FSD (Feature-sliced Design)

FSD 문서

FSD는 프론트엔드에서 쓰이는 기능 기반 폴더 구조의 대표적인 예시입니다.

FSD를 설명하는 글은 너무나 많으니 이번 글에서는 저희 프로젝트에 어떤 기준으로 적용했는지 설명드리겠습니다.

Layer

우선 FSD에서는 계층적인 구조를 가집니다.

📦 src
├── 📁 app            # 앱의 진입점
├── 📁 pages          # 페이지 단위
├── 📁 widgets        # 페이지를 구성하는 컴포넌트
├── 📁 features       # 사용자 중심의 기능 단위 (ex. 로그인, 채팅)
├── 📁 entities       # 모델 단위 (ex. User, Room 등)
├── 📁 shared         # 프로젝트 전반에서 재사용되는 코드들

막상 적용하려보면 무엇을 어디에 넣어야 할지 감 잡기가 어렵습니다.

실제 저희 프로젝트의 홈페이지를 보면서 설명드리겠습니다.

📦 HomePage
├── 📁 헤더
│   ├── 📄 로고
│   ├── 📄 네비게이션 메뉴 (홈, 마이페이지, 면접 질문)
│   └── 📄 로그인 버튼
├── 📁 좌측
│   ├── 📄 히어로 텍스트
│   │   ├── "꼬리에 꼬리를 무는 면접 질문"
│   │   └── "이제는 꼬리🦊 와 함께 AI로 연습하세요"
│   ├── 📄 혼자 연습하기 버튼
│   └── 📄 같이 연습하기 버튼
└── 📁 우측
    └── 📄 여우 캐릭터 일러스트 (정장+책상)

우선 헤더의 경우 공통적으로 쓰이는 컴포넌트입니다.

실제로는 MainLayout.tsx 내부에서 관리되고, React Router의 <Outlet />을 통해 <HomePage />가 보여지는 구조입니다.

이때 "공통적으로 사용된다"는 이유만으로 shared에 넣을지, 아니면 widgets에 둘지를 고민하게 됩니다.

<Header />의 경우 공통적으로 쓰이긴 하지만, 어떤 도메인과 직접적으로 연결되어 사용되는 재사용 UI가 아니고 그냥 페이지를 구성하는 하나의 기능 블록이라는 점에서 widgets이라고 판단했습니다.

. 📂 widgets
└── 📂 header/
│  ├── 📄 index.tsx
│  └── 📂 model/
│    ├── 📄 constants.ts
│  └── 📂 test/
│    ├── 📄 Header.test.tsx
│    ├── 📄 LogoLink.test.tsx
│  └── 📂 ui/
│    ├── 📄 LogoLink.tsx

다만 <Header />를 구성하는 요소 중 로그인 버튼은 다르게 관점에서 고려해야 합니다.

<LoginButton />은 단순한 UI 요소처럼 보일 수 있지만, 실제로는 인증 로직과 연결된 사용자 중심 기능을 포함하고 있기 때문입니다.

소셜 로그인 API 요청, 로그인 상태 판단, 리디렉션 등 인증 도메인과 밀접하게 연결된 로직이 포함되어 있다면, 이 컴포넌트는 단순한 UI가 아닌 기능 단위(feature)로 봐야 합니다.

따라서 저희는 로그인 버튼을 features/auth/ui로 분리하였습니다.

. 📂 features
└── 📂 auth/
│  └── 📂 model/
│    ├── 📄 constants.ts
│  └── 📂 test/
│    ├── 📄 GoToLoginButton.test.tsx
│  └── 📂 ui/
│    └── 📄 GoToLoginButton.tsx

그러면 나머지 좌측, 우측에 해당하는 컴포넌트들은 어디에 넣을 수 있을까요?

<HomePage/>에서만 사용되므로, /pages/homePage/ui 에 위치시키는 것이 적절합니다.

. 📂 homePage
│  ├── 📄 index.tsx
└── 📂 test/
│  ├── 📄 BackgroundShadow.test.tsx
│  ├── 📄 HeroText.test.tsx
│  ├── 📄 LeftSection.test.tsx
│  ├── 📄 PairPracticeButton.test.tsx
│  ├── 📄 PracticeButton.test.tsx
│  ├── 📄 SoloPracticeButton.test.tsx
│  ├── 📄 ThumbnailContainer.test.tsx
│  ├── 📄 index.test.tsx
└── 📂 ui/
│  ├── 📄 BackgroundShadow.tsx
│  ├── 📄 HeroText.tsx
│  ├── 📄 LeftSection.tsx
│  ├── 📄 PairPracticeButton.tsx
│  ├── 📄 PracticeButton.tsx
│  ├── 📄 SoloPracticeButton.tsx
│  └── 📄 ThumbnailContainer.tsx

이렇게 인증 도메인과 관련된 로직은 feature/auth 안에서 응집되고, <Header />는 여전히 레이아웃 구성 UI로써의 역할만 담당하게 되어 역할이 명확하게 분리됩니다.

또한, 하나의 페이지에서만 사용되는 컴포넌트들은 pages/page/ui에서 응집된 구조를 가지게 됐습니다.

이러한 구조 덕분에 컴포넌트의 역할과 책임이 명확히 분리되었고,
FSD가 지향하는 응집도 높은 구성과 명확한 책임 분리를 적용할 수 있었습니다.


맺음말

이 글을 읽으시다 의문이 드셨을 수도 있습니다

처음부터 기능 기반으로 폴더 구조를 잘 짰으면 되는 거 아닌가요?

맞습니다.

사실 오늘의 리팩토링 과정은 프로젝트 초반부터 예상된 일이기도 했습니다.

역할 기반 폴더 구조 의사결정 과정

처음 폴더 구조를 설계할 때, 여러 자료를 찾아보면서 FSD가 근 1~2년 사이에 프론트엔드에서 많이 쓰인다는 걸 알게 됐습니다.

다만, 단순히 트렌드라서 적용하기 보다는 실제로 내가 기능 중심으로 묶어야 할 필요성을 느끼고, 구조를 바꿨을 때 얻는 장점을 체감하며 적용하고 싶었습니다.

따라서 역할 기반 설계를 먼저 적용하고, 불편함이 명확해지면 기능 기반 구조로 변경하기로 결정했습니다.

이 의사결정을 하는데 큰 근거가 되었던 건 TDD였습니다.

테스트 코드가 존재하니 구조를 변경해도 기존 기능이 깨지지 않았는지 빠르게 확인할 수 있다는 믿음이 있었습니다.

그리고 폴더 구조 리팩토링을 겪으면 TDD의 가장 큰 장점인 안전한 리팩토링을 실제로 체감할 수 있으니 일석이조라는 생각으로 결정했습니다.

실제 적용 후기

폴더 구조를 기능 기반으로 변경하고, 연관된 코드들을 리팩토링하니 총 105개의 파일 변경이 있었습니다.

컴포넌트를 옮기고 경로를 수정하고, import 경로를 재정리하는 과정에서 수많은 에러가 발생했습니다.

처음에는 쌓여가는 에러에 겁이 났지만, 테스트 코드 있으니까 괜찮을 거야라고 동료와 대화를 나누며 걱정을 떨쳐낼 수 있었습니다.

실제로 테스트를 실행하면 어떤 컴포넌트에서 문제가 발생했는지 즉시 확인할 수 있었고,
UI나 로직을 수정해도 기능이 여전히 정상 동작하는지 자동으로 검증할 수 있었습니다.

이 과정을 겪으면서 두 가지를 깨달았습니다.

  1. 응집도가 높은 구조는 유지보수가 쉬워진다.
  2. 테스트 코드 함께라면 리팩토링이 즐겁다

0개의 댓글