본문 바로가기

FrontEnd

Refactoring React(리팩토링 리액트) : Folder Structure(폴더 구조)

반응형

리액트 프로젝트를 위한 최적의 폴더 구조는 뭘까요?

react

모범 사례 탐구

커뮤니티에서 유명한 추천 사례들은 다음 게시물들에서 잘 소개되고 있습니다.

https://www.joshwcomeau.com/react/file-structure/

 

Delightful React File/Directory Structure

How should we structure components and other files in our React apps? I've iterated my way to a solution I'm really happy with. In this blog post, I'll share how it works, what the tradeoffs are, and how I mitigate them.

www.joshwcomeau.com

https://redux.js.org/tutorials/essentials/part-2-app-structure

 

Redux Essentials, Part 2: Redux App Structure | Redux

The official Redux Essentials tutorial: learn the structure of a typical React + Redux app

redux.js.org

https://itchallenger.tistory.com/693

 

리액트 쿼리 : 셀렉터와 폴더 구조

리액트 쿼리 셀렉터 실무 사용 경험, 폴더 구조에 대한 생각을 공유합니다. 리액트 쿼리 셀렉터 놀랍게도 리덕스처럼 리액트 쿼리에는 셀렉터가 있습니다. export const useTodosQuery = (select, notifyOnCha

itchallenger.tistory.com

(물론 지금까지 제가 경험한 프로젝트에서 위와 같은 폴더 구조를 사용한 적은 없으며, 제 짬이 이렇게 하자고 설득할 정도가 안됩니다.)


Bulletpoof-react

리액트의 여러 모범 사례들을 모아둔 레포지토리가 있습니다.

해당 레포지토리에는 폴더 구조에 대한 예시도 존재합니다.

https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md

 

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

 

해당 프로젝트는 기능 중심으로 폴더 구조를 구성할 것을 추천하고 있습니다. 그 이유를 살펴봅시다.


파일들을 기능 관점에서 그룹화해야 하는 이유

흔하게 볼수 있는 프로젝트 구조들은 다음과 같습니다.

  1. 파일 타입 별 그룹화
  2. 페이지 별로 그룹화
    • 관련된 모든 것을 최대한 페이지 폴더 안에 위치
    • 컨텍스트, 훅과 같은 공통 요소를 위한 별도 폴더를 글로벌하게 사용
  3. 기능별로 그룹화

번호가 커질수록 왜 더 좋은 구조인지 알아봅시다.

1. 파일 타입 별 그룹화

가장 쉽게 볼 수 있는 타입입니다.
파일 유형 별로 폴더에 정돈합니다.

└── src/
    ├── components/
    │   │   # I'm omitting the files inside most folders for readability
    │   ├── button/
    │   ├── card/
    │   ├── checkbox/
    │   ├── footer/
    │   ├── header/
    │   ├── todo-item/
    │   └── todo-list/
    │       ├── todo-list.component.js
    │       └── todo-list.test.js
    ├── contexts/
    │   │   # no idea what this does but I couldn't leave this folder empty
    │   └── todo-list.context.js
    └── hooks/
        │   # again no idea what this does but I couldn't leave this folder empty
        └── use-todo-list.js

 

 

해당 방식은 컴포넌트 갯수가 많아지면 금세 찾기 어려워진다는 단점이 있습니다.

만약 컴포넌트들이 모여 하나의 컴포넌트를 구성한다면 (ex : modal), 해당 컴포넌트를 위한 폴더를 따로 만듭니다.

그리고 여러 컴포넌트에서 공통적으로 사용할 수 있는 컴포넌트는 ui 폴더에 둡니다.

└── src/
    ├── components/
    │   ├── edit-todo-modal/
    │   │   ├── edit-todo-modal.component.js
    │   │   ├── edit-todo-modal.test.js
    │   │   │   # colocate -> todo-form is only used by edit-todo-modal
    │   │   ├── todo-form.component.js
    │   │   └── todo-form.test.js
    │   ├── todo-list/
    │   │   │   # colocate -> todo-item is only used by todo-list
    │   │   ├── todo-item.component.js
    │   │   ├── todo-list.component.js
    │   │   └── todo-list.test.js
    │   │   # group simple ui components in one folder
    │   └── ui/
    │       ├── button/
    │       ├── card/
    │       ├── checkbox/
    │       ├── footer/
    │       ├── header/
    │       ├── modal/
    │       └── text-field/
    ├── contexts/
    │   ├── modal.context.js
    │   └── todo-list.context.js
    └── hooks/
        ├── use-modal.js
        ├── use-todo-form.js
        └── use-todo-list.js

2. 페이지 별 그룹화

  1. 프로젝트에는 페이지라는 컨텍스트가 있습니다.
    • 페이지가 증가할수록 컴포넌트 갯수도 많아지며 어느 컴포넌트가 어떤 페이지에서 사용되는지 불분명해집니다.
    •  페이지 폴더는 이용해 컴포넌트 디렉터리의 중첩 단계를 추가할 수 있습니다. (좀 더 플랫함을 피할 수 있습니다.)
  2. 컴포넌트 폴더에는 사실 다양한 타입의 컴포넌트가 있습니다.
    • 실제 페이지 역할을 하는 컴포넌트
    • 버튼과 같은 단순한 UI
    • 양식과 같은 복잡한 컴포넌트
    • 부작용을 포함한 컴포넌트

다음과 같은 방법으로 1번의 프로젝트 구조를 리팩토링할 수 있습니다.

  1. components에는 일반적인, 단순 UI 컴포넌트만 남깁니다
  2. 페이지와 연관된 컴포넌트들은 페이지 별로 구성합니다.
└── src/
    ├── components/
    │   │   # the form is shown on the home and create todo page
    │   ├── todo-form/
    │   │   # we could also ungroup this folder to make the components folder flat
    │   └── ui/
    ├── contexts/
    │   ├── modal.context.js
    │   └── todo-list.context.js
    ├── hooks/
    │   ├── use-auth.js
    │   ├── use-modal.js
    │   ├── use-todo-form.js
    │   └── use-todo-list.js
    └── pages/
        ├── create-todo/
        ├── home/
        │   ├── home-page.js
        │   │   # colocate -> the edit modal is only used on the home page
        │   ├── edit-todo-modal/
        │   └── todo-list/
        │       ├── todo-item.component.js
        │       ├── todo-list.component.js
        │       └── todo-list.test.js
        ├── login/
        │   # don't forget the legal stuff :)
        ├── privacy/
        ├── signup/
        └── terms/

 

글로벌 폴더는 1번에서 파일 유형별 분류의 문제를 그대로 갖고 있습니다.

빠르게 파일이 많아지며, 구분하기 어려워 집니다.

따라서, 정말로 여러 곳에서 사용하게 되면 그 쪽으로 옮기는 것이 낫습니다.

그 전에는 pages 폴더에 같이 둡니다.

└── src/
    ├── components/
    │   ├── todo-form/
    │   └── ui/
    ├── hooks/
    │   │   # not much left in the global hooks folder
    │   └── use-auth.js
    └── pages/
        ├── create-todo/
        ├── home/
        │   ├── home-page.js
        │   ├── edit-todo-modal/
        │   └── todo-list/
        │       ├── todo-item.component.js
        │       ├── todo-list.component.js
        │       ├── todo-list.context.js
        │       ├── todo-list.test.js
        │       │   # colocate -> this hook is only used by the todo-list component
        │       └── use-todo-list.js
        ├── login/
        ├── privacy/
        ├── signup/
        └── terms/

지금까지 많이 개선했지만 남은 문제 2가지가 있습니다.

  • todo가 각 페이지에 존재한다면, todo 엔터티가 사용되는 곳을 찾기 위해 모든 페이지 폴더를 뒤져봐야 합니다.
  • 아래 구조를 보고 todo-list 컴포넌트가 home 페이지에서 사용됨을 코드베이스에서 쉽게 파악할 수 있나요?
└── src/
    ├── components/
    ├── hooks/
    └── pages/
        ├── create-todo/
        ├── home/
        ├── login/
        ├── privacy/
        ├── signup/
        └── terms/

3. 기능 별 그룹화

2번에서의 문제를 재정의하면 다음과 같습니다.

  • 여러 곳에서 사용된다고 파일 위치를 호이스팅 하는건 기만입니다.
    • 어디서 쓰는지는 페이지 폴더를 열어봐야 합니다.
  • 페이지 폴더 명만 보고 어떤 컴포넌트가 들어있는지 쉽게 파악하기 어렵습니다.
    • 물론 우리에게는 ctrl+P가 있지만, 컴포넌트 이름을 외우기 전 탐색은 어렵습니다.

해당 문제는 도메인 주도 설계에서 아이디어를 차용해서 해결할 수 있습니다.

도메인 주도 설계의 핵심은 해당 도메인의 핵심 엔터티가 중심이 되어야 한다는 것입니다.

이를 토대로 다음과 같은 기준에 따라 폴더를 구성합니다.

  • 특정 엔터티를 다루는 코드들은 엔터티 중심으로 코로케이션 합니다.
  • 데이터 의존성이 없는 presentation 컴포넌트와 같은 경우, 기능이 지원하는 대상을 엔터티 명으로 합니다.

따라서 다음과 같은 엔터티를 도출할 수 있습니다.

  • 앱의 도메인
    • todo
    • user
    • project
  • 프론트엔드 자체를 의미하는 도메인
    • ui

이를 토대로 프로젝트 구조를 다시 구성해 봅니다.

└── src/
    ├── features/
    │   │   # the todo "feature" contains everything related to todos
    │   ├── todos/
    │   │   │   # this is used to export the relevant modules aka the public API (more on that in a bit)
    │   │   ├── index.js
    │   │   ├── create-todo-form/
    │   │   ├── edit-todo-modal/
    │   │   ├── todo-form/
    │   │   └── todo-list/
    │   │       │   # the public API of the component (exports the todo-list component and hook)
    │   │       ├── index.js
    │   │       ├── todo-item.component.js
    │   │       ├── todo-list.component.js
    │   │       ├── todo-list.context.js
    │   │       ├── todo-list.test.js
    │   │       └── use-todo-list.js
    │   ├── projects/
    │   │   ├── index.js
    │   │   ├── create-project-form/
    │   │   └── project-list/
    │   ├── ui/
    │   │   ├── index.js
    │   │   ├── button/
    │   │   ├── card/
    │   │   ├── checkbox/
    │   │   ├── header/
    │   │   ├── footer/
    │   │   ├── modal/
    │   │   └── text-field/
    │   └── users/
    │       ├── index.js
    │       ├── login/
    │       ├── signup/
    │       └── use-auth.js
    └── pages/
        │   # all that's left in the pages folder are simple JS files
        │   # each file represents a page (like Next.js)
        ├── create-project.js
        ├── create-todo.js
        ├── index.js
        ├── login.js
        ├── privacy.js
        ├── project.js
        ├── signup.js
        └── terms.js

Feature-Driven Folder Structure and Screaming Architecture (기능 중심 폴더 구조와 스크리밍 아키텍처)

 

엉클 밥은 Screaming Architecture에서 다음과 같이 말했습니다.

아키텍처는 시스템에서 사용한 프레임워크를 설명하는게 아니라,
해당 코드가 구현하는 시스템을 이야기해야 합니다.
건강 관리 시스템을 구축하고 있다면
새로운 프로그래머가 폴더 구조를 보자마자 건강 관리 시스템 코드임을 알아야 합니다.

아래 폴더 구조를 보고 어떤 시스템인지 알기 쉽나요?

리액트 앱인지는 알 수 있습니다.

이는 프레임워크를 나타내는 구조지 시스템에 대해 설명하는 구조가 아닙니다.

└── src/
    ├── components/
    ├── contexts/
    └── hooks/

이제 아래 구조를 볼까요?

└── src/
    ├── features/
    │   ├── todos/
    │   ├── projects/
    │   ├── ui/
    │   └── users/
    └── pages/
        ├── create-project.js
        ├── create-todo.js
        ├── index.js
        ├── login.js
        ├── privacy.js
        ├── project.js
        ├── signup.js
        └── terms.js
  • pages폴더, features 폴더 둘 다 프로젝트 관리 앱임을 파일 명으로 나타내고 있습니다.
    • 컴포넌트가 어디에서 쓰이는지 몰라도 features 폴더를 열면 쉽게 알 수 있습니다.
    • home.js에서 사용되는 컴포넌트를 바꾸고 싶으면 pages/home.js 파일을 열면 됩니다.

좀 더 자세한 사항을 아래 문서를 참고하면 좋습니다.

 

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


기타 모범 사례

절대 경로

상대 경로 ../../ 갯수는 점차 늘어나며 몇개 필요한지 계산하기 귀찮아집니다.

import { Button } from "@features/ui/button";

CRA를 사용하는 경우 jsconfig.js나 tsconfig.js에 다음을 설정하면 끝납니다.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@features/*": ["src/features/*"],
    }
  }
}

참고 : React, Next.js

index.js를 퍼블릭 API로

index.js를 사용하면 import가 다음과 같이 깔끔해 집니다.

import { TodoList } from "@features/todo/todo-list/todo-list.component";

...
import { TodoList } from "@features/todo";

...
  1. 개발자는 컴포넌트를 사용하기 위해 폴더 구조에 대해 알 필요가 없습니다.
  2. index.js에서 노출하지 않는 파일은 private 모듈처럼 생각할 수 있습니다.
  3. 퍼블릭 API만 stable하면 나머지는 쉽게 리팩토링 할 수 있습니다.

파일과 폴더명에 케밥케이스(kebab-case) 사용하기

파일명을 myComponent에서 MyComponent로 바꿨다 가정합시다.
임포트 구문은 myComponent에서 MyComponent로 다음과 같이 변경될 것입니다.
import MyComponent from "./MyComponent";
MacOS는 기본적으로 대소문자를 구분하지 않는 파일 시스템을 사용합니다.
MyComponent.js와 myComponent.js는 같은 것입니다.
따라서 Git은 파일 이름의 변경 사항을 반영하지 않습니다.
 
GitHub의 CI는 Linux 이미지를 사용하며 이것은 대소문자를 구분합니다,
따라서 Git에서 myComponent.js를 가져오게 되면 문제가 발생합니다.
 
따라서 많은 사람들은 이미 kebab-case를 사용하고 있습니다.

Angular는 위 내용을 공식문서 coding styleguide(코딩 스타일가이드)에 포함하였습니다.

  • MyComponent.js 대신 my-component.js를 사용하세요
  • useMyHook.js 대신 use-my-hook.js를 사용하세요

 

반응형