너굴 개발 일지

[Next.js] 기능 중심의 프로젝트 폴더 구조 본문

Next.js

[Next.js] 기능 중심의 프로젝트 폴더 구조

너굴냥 2024. 1. 1. 18:32

사이드 프로젝트를 진행하며 프로젝트 폴더 구조를 구성하는 방법에 대해 고민한 과정을 정리한 글입니다.

목차

서론

본론

결론


 

서론

프로젝트 구조를 잡는 기준은 크게 2가지로 역할 중심, 기능 중심으로 볼 수 있다. 대부분의 프로젝트 구조는 역할 중심이지만 이번 프로젝트에서는 기능 중심으로 적용하면서 겪은 고민들을 작성한 글이다.

 

기능이란?

기능은 서로 관련된 파일 그룹이며 더 나아가 프로젝트의 영역이나 주제를 대표한다. 예를 들면 인증, 랜딩 페이지, 대시보드가 있다.

프로젝트를 여러 기능으로 나누면 관심 분리 디자인 원칙에 대해 생각해 볼 수 있다.

기능을 만들기 전에 지금 작업하고 있는 것이 기능으로 그룹화 할 수 있는지 고민한다면 다음과 같은 질문을 할 수 있다.

  • 적어도 두 개의 관련 파일이 있는가?
  • 이들을 그룹화하면 코드베이스의 유지보수가 개선될 것인가?
  • 이 파일들은 프로젝트에서 공통된 영역이나 주제를 공유하는가?
  • 이 파일들은 프로젝트에서 공통된 책임을 공유하는가?
  • 기존의 기능으로는 작업을 수행할 수 없는가?

기능은 경로 폴더에 해당할 수도 있고 그렇지 않을 수도 있다.

어떤 폴더에 넣을지 고민한다면 사실 먼저 코딩한 다음 최적화를 생각하는 것이 더 좋을 수도 있다. 최적화가 필요하다 생각되는 시점에 큰 그림을 갖게 되고 더 쉽게 그룹화할 수 있기 때문이다.

 

기능 중심 구조의 이점

Next Right Now 문서에 따르면 기능 중심 구조는 다음과 같은 이점을 제공한다고 한다.

MVC 디자인 패턴은 각 파일의 "역할"에 따라 코드를 그룹화하지만, 우리의 접근 방식은 다르며 관련된 코드를 함께 그룹화한다.
MVC는 처음에 이해하기 쉽기 때문에 매우 직관적이다. 그래서 초보자 친화적이다.

하지만 확장이 어렵다.

수십 가지 다른 기능에 도달하면 모든 코드가 함께 그룹화된다. (예 : 컴포넌트)
반면에 개발자가 어떤 작업을 할 경우, 보통 "기능"과 관련이 있다. 그러나 MVC 패턴 때문에 기능과 관련된 코드는 많은 폴더와 하위 폴더로 흩어져 있다.
이로 인해 코드를 찾기가 훨씬 어려워지며 관련된 부분 전체를 볼 수 없다.

 

따라서 기능 중심 구조는 개발자가 코드베이스를 탐색하는 방식과 더 잘 호환된다고 생각한다.

 

기능과 역할의 차이는 무엇인가?

기능은 어떤 행위의 결과로 나타나는 현상과 관련된 것을 의미한다.
역할은 특정한 결과를 기대하는 행위 주체와 관련된 것이다.

예를 들어 useAuth라는 인증 관련 커스텀 훅 파일은 인증과 관련된 작업을 수행하므로 auth라는 기능으로 분류될 수 있다.
역할 기준으로 본다면 useAuth hook 자체는 React 생명 주기 기능을 연동하는 행위 주체이므로 hook이라는 역할로 분류될 수 있다.

 

아래는 기존의 프로젝트 구조로 역할 중심이었으며 기능 중심의 구조로 변경하며 구체적인 규칙을 정의해보자.

.
└── app/
    ├── (route) // 라우터별 폴더
    │ ├── news	
    │ │   └── page.tsx
    │ ├── markets
    │ │   └── page.tsx
    │ ├── stocks
    │ │   └── page.tsx
    │ ├── layout.tsx
    │ └── page.tsx
    ├── components // 컴포넌트 폴더
    │ ├── layout // 레이아웃 관련 컴포넌트 폴더
    │ │   ├── Header.tsx	// header 컴포넌트
    │ │   ├── NavigationBar.tsx	// header 내부 카테고리별 라우팅 처리
    │ │   └── Topbar.tsx	// header 내부 로고, 다크모드 변환 등 아이콘 모음 
    │ ├── NavItem.tsx	// 공통 css 적용 li 태그 공통 컴포넌트
    │ └── ThemeSwitcher.tsx	// 다크모드 변환 기능 컴포넌트
    │    
    ├── hooks
    │   └── useLocalStorage.ts	// 로컬스토리지 값을 다루는 커스텀 훅
    ├── images // 이미지 파일 관리 폴더
    └── ReactQueryProvider.tsx	// react-query provider 컴포넌트

 

경로 구조화

기능 중심으로 경로를 구조화했을 때 크게 랜딩 페이지, 뉴스, 시장과 같은 경로로 분리할 수 있다. Next.js 13은 app router를 사용하므로 app에 있는 route 폴더를 기능 중심으로 분리했다.

 전역에서 사용되는 레이아웃으로 header, footer 컴포넌트도 기능적으로 봤을 때 home을 담당하며 다른 route에서는 import하지 않으므로 components 폴더가 아닌 (route) 폴더 내부에 있는 것이 적합하다고 생각하여 (route) 폴더 내부에 private folders를 적용해 _home이라는 폴더를 추가했다.

next.js의 private folder를 통해 라우팅 시스템에 고려되지 않으며 UI로직과 라우팅 로직을 분리하고 내부 파일들을 일관성 있게 구성한다는 이점을 제공한다.

private folder를 적용하지 않아도 되지만 _home 폴더 내부에는 홈페이지에 사용되는 componenets, hook을 관리할 예정으로 라우팅 시스템에 고려되지 않는다는 의미를 부여하기 위해 private folder를 적용했다.

.
└── app/
    ├── (route) 
    │ ├── _home // 새로 추가
    │ ├── news	
    │ │   └── page.tsx
    │ ├── markets
    │ │   └── page.tsx
    │ ├── stocks
    │ │   └── page.tsx
    │ ├── layout.tsx
    │ └── page.tsx
    ├── components
    │ ├── layout 
    │ │   ├── Header.tsx	
    │ │   ├── NavigationBar.tsx	
    │ │   └── Topbar.tsx	
    │ ├── NavItem.tsx	
    │ └── ThemeSwitcher.tsx	
    │    
    ├── hooks
    │   └── useLocalStorage.ts	
    ├── images
    └── ReactQueryProvider.tsx

 

공통 파일 정리

프로젝트에는 기본 버튼 요소와 같은 전역 파일이 필요하기에 이를 그룹화할 수 있는 역할 기반의 폴더도 필요하다.

현재 프로젝트에서 역할 기반 폴더는 components, css, hooks, types, image, constants, provider가 있으며 root level에 위치한다. 또한 필요하다면 나중에 더 추가될 수 있다.

현재 프로젝트는 라우터 폴더를 기능 기반으로 분리하였기에 어떤 기능과도 관련이 없거나 2개 이상의 서로 다른 route 폴더에서 사용할 경우 공통 파일을 역할 기반 폴더로 이동시키는 규칙을 만들었다.

따라서 기존 components 폴더에 있는 컴포넌트들은 home이라는 기능만을 담당하기에 _home 라우터 폴더 내부로 이동했다.

기존 hooks 폴더에 있는 커스텀 훅도 다크모드 변환시에만 사용되고 다크모드 변경 아이콘 컴포넌트도 home을 담당하기에 마찬가지로 _home 라우터 폴더 내부로 이동했다.

ReactQueryProvider 컴포넌트는 어떤 기능에도 관련이 없기에 root에 있는 provider 폴더로 옮겼다.

 

├── app
│   ├── (route)
│   │   ├── _home
│   │   │   ├── Header.tsx
│   │   │   ├── NavItem.tsx
│   │   │   ├── NavigationBar.tsx
│   │   │   ├── ThemeSwitcher.tsx
│   │   │   ├── TopBar.tsx
│   │   │   └── useLocalStorage.ts
│   │   ├── markets
│   │   │   └── page.tsx
│   │   ├── news
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components
│   ├── hooks
│   ├── image
│   ├── provider
│   │   └── ReactQueryProvider.tsx
│   └── types

 

기능 구성

기능 중심 프로젝트 구조로 파일을 분리해놔서 특정 기능에만 사용되는지 전역적으로 사용되는지 구분하기 쉬워졌다. 하지만 단점으로는 기능별 route 폴더 내부에 많은 파일들이 존재해 지저분해 보일 수 있다.

그래서 나는 기능별 route 폴더 내부에 역할별로 폴더를 다시 분리했다. depth가 너무 많아지지 않을까 고민되었지만 역할 별로 분리함으로써 쉽게 구분할 수 있다는 장점이 크다고 판단되어 route나 sub route 기준 1~2 depth까지만 허용하기로 규칙을 정의했다.

따라서 _home 폴더 내부에 역할 별 폴더를 생성해 _home/componenets, _home/hooks 폴더를 생성했다.

NavagationBar 컴포넌트 내부에는 이동할 수 있는 url 관련 정보 상수가 존재했기에 상수라는 역할의 constants라는 폴더를 만들어 별도로 관리했다.

├── app
│   ├── (route)
│   │   ├── _home
│   │   │   ├── components
│   │   │   │   ├── Header.tsx
│   │   │   │   ├── NavItem.tsx
│   │   │   │   ├── NavigationBar.tsx
│   │   │   │   ├── ThemeSwitcher.tsx
│   │   │   │   └── TopBar.tsx
│   │   │   ├── constants
│   │   │   │   └── routes.ts
│   │   │   └── hooks
│   │   │       └── useLocalStorage.ts
│   │   ├── markets
│   │   │   └── page.tsx
│   │   ├── news
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components
│   ├── css
│   ├── hooks
│   ├── image
│   ├── provider
│   │   └── ReactQueryProvider.tsx
│   └── types

 

Q) Topbar, NavagationBar도 Header 컴포넌트 내부 구성요소니까 Header와 같은 별도 폴더를 만들어야 하지 않을까?

그럴 수도 있겠지만 componenets 폴더 내부에서 그룹핑을 한다면 3 depth 이상으로 깊어지므로 일관성 있게 구성하는 장점보다는 중첩으로 인한 복잡성이라는 단점이 커진다고 생각했다. 따라서 그룹핑을 하지 않기로 했다.

Q) root에 있는 componenets 폴더에 파일이 추가된다면 서브 폴더로 그룹화를 해야 할까? 그렇다면 무슨 기준으로 분리해야 할까?

현재 프로젝트는 기능별 route를 분리했으며 2개 이상의 서로 다른 route에 사용될 경우 역할 기반 폴더에 저장하는 규칙을 정의했기에 기능 기준으로는 불가능하다. 따라서 역할별로 서브 폴더를 분리할 필요성이 있다.

처음에는 공통 컴포넌트를 바로 componenets 폴더에 추가해 사용하며 만약 같은 역할의 컴포넌트가 많아진다면 역할 별로 다시 분리한다는 규칙을 정의했다. 예를 들어 componenents/CreateButton, DeleteButton이 존재한다면 componenets/button/CreateButton, componenets/button/DeleteButton와 같이 분리할 수 있다.

 

결론

위 과정을 통해 다음과 같은 규칙을 정의했다.

  • (route)라는 라우터 담당 폴더 내부는 기능별로 폴더를 분리한다.
  • 역할 기준 폴더는 다음과 같다 - componenets, hooks, types, image, constants, provider
  • 각 route 폴더 내부는 역할 기준으로 서브 폴더를 분리한다.
  • 새로운 파일을 추가할 경우 해당 route > 역할 기준 폴더 중 적절한 역할 폴더 하위에 추가한다.
  • 2개 이상의 route 폴더 내부에서 사용되는 파일이라면 root level의 역할 기준 폴더로 이동한다.
  • root level의 역할 폴더 내부에서 공통되는 역할이 존재한다면 서브 폴더를 추가해 그룹화 한다. (예 : components/CreateButton, DeleteButtoncomponenets/Button/CreateButton, componenets/Button/DeleteButton )   

기능 중심 구조로 변경하면서 관련된 요소들이 하나의 폴더로 묶여있어 탐색하기 쉽다는 장점이 큰 것 같다.

하지만 이 방법이 모든 프로젝트에 다 적합한 것은 아니기에 요구 사항에 맞는 방식으로 프로젝트를 구성하는 방법에 대해 알아야 하며 각 구조마다 어떤 장단점이 존재하는지 알아야 한다.


참고 자료

https://betterprogramming.pub/how-to-structure-your-next-js-app-with-the-new-app-router-61bf2bf5a20d

 

How to Structure Your Next.js App With the New App Router

Learn how to organize your Next.js project using a feature-driven structure with the new App Router, allowing for greater flexibility and…

betterprogramming.pub

https://github.com/alan2207/bulletproof-react/tree/master

 

GitHub - alan2207/bulletproof-react: 🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready

🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications. - GitHub - alan2207/bulletproof-react: 🛡️ ⚛️ A simple, scalable, and powerful architecture for...

github.com

https://unlyed.github.io/next-right-now/reference/folder-structure

 

Folder structure

Flexible production-grade boilerplate with Next.js 11, Vercel and TypeScript. Includes multiple opt-in presets using Storybook, Airtable, GraphQL, Analytics, CSS-in-JS, Monitoring, End-to-end testing, Internationalization, CI/CD and SaaS B2B multi single-t

unlyed.github.io

https://www.youtube.com/watch?v=UUga4-z7b6s