2024.04.30

이짜젠·2025년 4월 30일

최근 많은 프로젝트들을 접하고, 처음에 불편하다고 생각했던 디렉토리구조에 익숙해지다보니 불편함을 잊고 일을하게된다.
다시한번 나만의 프로젝트 구조기준을 정하고, 사이드프로젝트나 혹은 지금 프로젝트를 개선할 수 있는 기회가생겼을때 이 기준으로 진행하면 좋을 것 같다. 물론 이러한 기준은 잘 정리해둔다면 rule 로 만들어 agent에 학습시켜 AI에게 시키기도 좋을 것 같다.

여러 고민들이 있었다.

  • componetns
    • atomic design pattern 을 쓰자
    • 공용컴포넌트들을 담아두자.
    • 과거에 팀동료중 한명이, 요즘은 components 하위에 디렉토리구조를 나누는게 의미가 점점 없어지는것같다는 이야기를 한적이 있다. 뭔가 찜찜하지만 그 당시엔 나또한 디렉토리구조를 탐새갛지않고 단순 IDE의 intellegence를 통해서 컴포넌트를 탐색하고 활용해왔기에 그렇게 생각할수도 있겠구나 했는데, 최근 다양한 프로젝트들을 경험해보면서 디렉토리의 구분은 필요하다는 확신이 생겼다.
      • 프로젝트를 처음 접하는 동료가 있을 경우, 어떤 공용컴포넌트가 있는지를 이름만 보고 유추하는것엔 한계가 있다.
      • 디렉토리를 통해 해당 디렉토리가 어떤 목적의 컴포넌트인지 힌트를 주어야 유추하고 필요한 컴포넌트들을 찾아 활용할 수 있다.
      • 디렉토리로 구분을 해둔다면 단순히 파일을 정리하는 것에 그치지않고, 테스트코드를 작성하거나 혹은 전역으로 어떤 작업을 수행할 때, 작업을 적용할 컴포넌트의 기준으로 삼을수도 있다.
  • hooks
    • 공통된 훅만을 담는게 좋을 것 같다.
    • 특정 도메인이나 서비스에 종속되어있는 hook의 경우 그 내부에서 진행하는게 좋아보인다.
    • store 또한 hook 형태로 제공하는게 좋을 것 같다. store에 직접 접근하는건 디버깅측면에서도 최대한 지양 하는게 좋을 것 같고, useAppStore 와 같은 hook 이름 말미에 Store 와 같은 키워드를 붙이는 방향으로 컨벤션을 정해서 사용하면 store라는 별도의 디렉토리를 추가하지 않고도 store를 이쁘게 분류할 수 있다.
  • pages
    • 최근 많은 프론트엔드 개발자가 nextjs 에 익숙하다보니 pages도 컴포넌트로 구현되지만, components 하위보다는 components와 동일한 depth에서 구성하는게 DX 적으로 좋다고 생각이 든다.
    • 사용자들이 실제로 보게되는 하나의 화면단위로, url path 하나가 page 하나라고 보면 된다.
    • 그리고 page를 구성하는 컴포넌트들은 page 하위로 구성하며, 관련된 hook 또한 하위로 구성하는게 좋아보인다.
    • pages/components, pages/_hooks, pages/...
  • apis
    • endpoint 별로 하위에 파일을 만든다.
      • getProductList.ts
      • getProductList.scheme.ts
    • 파일을 많이 만드는걸 좋아하진않는데, 도메인과 같은 특정한 기준으로 그룹화하게 되면 프로젝트가 커지면 커질수록 나누는 기준이 모호해지는 것 같다.
    • 파일을 따로 파게되면 prodlist.scheme.ts 처럼 스키마파일도 비슷한 이름으로 구성할 수 있고, 특정 api 가 변경되었을때 쉽게 수정도 가능해보인다.
    • 실제로 일을 하다보면 api가 추가되거나 수정되는 경우가 잦은데, 한 파일에 몰아넣었을 때 동료와 동시에 작업하다가 충돌이 발생할 위험도 있다.
  • utils
    • libs 와 공존하는 프로젝트들을 많이 봐왔는데, 개인적으로 차라리 구분하지않는게 나을 것 같다. 너무 성격이 비슷하고 오히려 구분할때 헷갈린다.
    • react 혹은 nextjs 와 같은 프레임워크 컨텍스트에 의존하지않는 순수한 함수들을 구성해둔다.
  • types
    • 항상 고민이 많이되는 디렉토리 구조다.
    • 최대한 도메인별로 맞추는게 좋을 것 같다는 생각을 하지만, 파일을 분리하는게 마냥 쉽지많은 않다.
    • 결국 프론트엔드에서 취급하는 데이터의 형태가 api response spec 만있다면 굳이 파일을 별도로 두지않고 api 에서 export 한 타입을 사용하는것도 괜찮을 것 같은데, 사실 그렇지않다, 프론트엔드에서만 사용하는 타입들도 많이 있다.
    • page 단위로 구성한다면 사실 page 내부로 넣어서 그룹화하는게 더 좋을 것 같다는 생각도 드는데... 고민이다.
  • themes or theme.ts
    • MUI나 tailwindcss 같은 UI/Style framework 를 쓴다면, 테마를 관리해야할 디렉토리도 필요하다.

이러한 생각과 고민들을 LLM 과 논의하며 readme를 작성해봤다.
꽤나 맘에 들고, 마스카라를 사용하여 최대한 AI 친화적인 프롬프트를 만들어서 cursor 나 windsurf 같은 에이전트에 학습시켜서, 작업중이 사이드프로젝트를 리펙토링 하고, 추후에 다른 멤버가 합류하거나, 기획자들또한 편리하게 프로젝트에 기능을 추가/유지보수할수있도록 개선해야겠다.

🗂 프로젝트 디렉토리 구조 가이드

이 문서는 프로젝트의 디렉토리 구조와 컴포넌트 설계 철학에 대해 설명합니다.
구조화된 디렉토리는 유지보수와 협업을 원활하게 하고, 확장성과 가독성을 높이는 데 기여합니다.

📦 디렉토리 개요

src/
├── components/  # Atomic Design 기반 공용 컴포넌트
├── pages/       # 실제 라우팅에 대응되는 화면
├── apis/        # API 요청 로직
├── hooks/       # 전역 공통 Hook
├── types/       # 전역 타입 정의
├── utils/       # 프레임워크에 독립적인 순수 유틸 함수
└── theme/       # 스타일 관련 테마 구성

🎯 Atomic Design Pattern

components/
├── atoms/
├── molecules/
├── organisms/
└── layout/

✅ atoms

  • 더 이상 쪼갤 수 없는 가장 기본 UI 단위
  • 예: Button, Input, Icon, Typography
  • 상태나 로직 없이 스타일과 구조만 담당
  • 테스트 코드 및 스토리북 필수
atoms/
└── Logo/
    ├── Logo.tsx
    ├── Logo.test.tsx
    └── Logo.stories.tsx

✅ molecules

  • 2개 이상의 atoms를 조합한 작은 단위 UI
  • Pure한 성격 유지 (단순 상태는 있어도 Side Effect 없음)
  • 예: SearchBar, FormField
  • 테스트 및 스토리북 작성 대상
molecules/
└── SearchBar/
    ├── SearchBar.tsx
    ├── SearchBar.test.tsx
    └── SearchBar.stories.tsx

💡 내부에 state가 있다고 하더라도, 외부 API나 Context에 의존하지 않는다면 molecule에 속합니다.

✅ organisms

  • atoms/molecules 조합 + 비즈니스 로직 포함 가능
  • 예: UserProfileCard, LoginForm, ProductList
  • 비동기 처리, API 호출, 상태 관리 등이 포함될 수 있음
  • 테스트/스토리북은 선택적 (Mocking 비용 고려)
organisms/
└── ProductList/
    ├── ProductList.tsx
    ├── ProductList.test.tsx
    └── ProductList.stories.tsx

✅ layout

  • Header, Footer, Sidebar 등 전체 레이아웃 관련 구성
  • 페이지에서 공통적으로 사용되는 고정 UI 구조
layout/
└── Header/
    ├── Header.tsx
    ├── Header.test.tsx
    └── Header.stories.tsx

📄 pages

  • 실제 사용자에게 노출되는 라우트 단위
  • 각 페이지에 필요한 컴포넌트, 훅, 타입은 해당 페이지 하위에서 관리
pages/
└── products/
    ├── index.tsx
    ├── _components/
    │   └── ProductItem.tsx
    ├── _hooks/
    │   └── useFetchProducts.ts
    └── _types.ts

_components, _hooks, _types처럼 언더스코어 접두어를 사용해 라우트와 혼동 방지

🌐 apis

  • 도메인별로 디렉토리를 구분
  • 각 endpoint는 하나의 파일로 관리
  • 요청/응답 타입은 <endpoint>.schema.ts로 분리
apis/
└── product/
    ├── getList.ts
    ├── getList.schema.ts
    ├── getDetail.ts
    ├── getDetail.schema.ts
    └── index.ts

💡 endpoint 기준 분리는 Git 충돌 최소화 및 추적 편의성에 유리

🧠 hooks

  • 전역에서 재사용 가능한 hook만 작성
  • 특정 페이지/기능에 종속된 hook은 해당 위치에 정의
  • 상태 관리(store)도 훅 형태로 구현하여 캡슐화 (useXxxStore.ts 형태로 명명)
hooks/
├── useDebounce.ts
├── useModal.ts
└── useAppStore.ts  # 상태 관리 스토어를 캡슐화한 훅

📊 상태 관리 패턴

  • 별도의 store/ 디렉토리를 만들지 않고, hooks/ 내에 useXxxStore.ts 형태로 관리
  • 네이밍 컨벤션: 반드시 Store 키워드를 포함 (예: useAppStore, useAuthStore)
  • 스토어 인스턴스는 훅 내부에서만 접근 가능하도록 캡슐화
  • 컴포넌트에서는 훅을 통해서만 상태 접근/변경 가능
// hooks/useAppStore.ts 예시
import create from 'zustand'; // 또는 다른 상태 관리 라이브러리

// 1. 스토어 타입 정의
type AppStore = {
  count: number;
  increment: () => void;
  decrement: () => void;
};

// 2. 스토어 내부 구현 (비공개)
const appStore = create<AppStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

// 3. 외부에 노출되는 훅 (공개 API)
export const useAppStore = () => {
  const { count, increment, decrement } = appStore();
  
  return {
    count,
    increment,
    decrement,
  };
};

이 패턴의 장점:

  • 스토어 직접 접근 방지로 예측 가능한 상태 변경
  • 컴포넌트에서 일관된 훅 API로 상태 관리
  • 테스트 용이성 및 격리성 향상

📐 types

  • 프론트에서 전역적으로 사용하는 타입 정의
  • 도메인 단위로 정리
  • API 응답 타입은 apis/*.schema.ts에서 export 가능
  • 작을 경우, 프로젝트 최상단에 통합 관리
types/
├── product.ts  # 도메인 타입
├── user.ts
└── global.ts   # 공통 타입

🔧 utils

  • 프레임워크에 의존하지 않는 순수 함수들
  • libs와 구분하지 않고 하나로 관리
utils/
├── date.ts
└── formatter.ts

🎨 theme

  • 스타일 프레임워크(MUI, Tailwind 등)의 설정
  • 테마 전역 구성 관리
theme/
├── palette.ts
├── typography.ts
└── theme.ts

💡 FAQ

Q. 내부 상태가 있는 컴포넌트는 무조건 organisms인가요?

아니요. 내부 상태가 있어도 외부에 의존하지 않고, 단순 UI 렌더링에 국한된다면 molecule입니다.
예: ToggleSwitch, TabPanel

Q. 테스트와 스토리북은 어디까지 작성하나요?

  • atoms와 molecules: 필수
  • organisms: 선택적 (복잡도와 외부 의존성 고려)
  • layout: 필요에 따라

이 가이드는 프로젝트 상황에 따라 유연하게 확장할 수 있으며,
명확한 기준을 통해 일관된 코드베이스를 유지하는 데 목적이 있습니다.

profile
오늘 먹은 음식도 기억이 안납니다. 그래서 모든걸 기록합니다.

0개의 댓글