2023년 01월 03일
6

React 프로젝트 구조

Frontend
KKingmo

Changmo Oh

@KKingmo

전체 글 보기

소스 폴더 구조

대부분의 코드는 src 폴더에 있으며 다음과 같은 구조를 가진다.

src
|
+-- app               # 애플리케이션 레이어:
|   |
|   +-- routes        # 애플리케이션 라우트 / 페이지
    +-- app.tsx       # 메인 애플리케이션 컴포넌트
    +-- app-provider  # 애플리케이션 제공자, 전체 애플리케이션을 글로벌 제공자로 감쌈
+-- assets            # 정적 파일(이미지, 폰트 등)을 포함한 리소스 폴더
|
+-- components        # 애플리케이션 전체에서 사용되는 공유 컴포넌트
|
+-- config            # 글로벌 구성, 환경 변수 등
|
+-- features          # 기능별 모듈
|
+-- hooks             # 애플리케이션 전체에서 사용되는 공유 훅
|
+-- lib               # 애플리케이션을 위해 사전 구성된 재사용 가능한 라이브러리
|
+-- stores            # 글로벌 상태 저장소
|
+-- test              # 테스트 유틸리티 및 Mock
|
+-- types             # 애플리케이션 전체에서 사용되는 공유 타입
|
+-- utils             # 공유 유틸리티 함수

확장성과 유지 보수성을 위해 대부분의 코드를 features 폴더 내에 구성한다. 각 기능 폴더는 해당 기능에 특정한 코드를 포함해 잘 분리된 상태를 유지한다. 이 접근 방식은 공유 컴포넌트와 기능 관련 코드를 섞지 않아, 코드베이스를 관리하고 유지하기 쉽게 만든다. 이를 통해 애플리케이션 아키텍처에서 협업, 가독성 및 확장성을 높일 수 있다.

features 폴더 구조

features 폴더는 다음고 같은 구조를 따를 수 있다.

src/features/awesome-feature
|
+-- api         # 특정 기능과 관련된 API 요청 선언 및 API 훅
|
+-- assets      # 특정 기능을 위한 정적 파일을 포함한 자산 폴더
|
+-- components  # 특정 기능에 한정된 컴포넌트
|
+-- hooks       # 특정 기능에 한정된 훅
|
+-- stores      # 특정 기능을 위한 상태 저장소
|
+-- types       # 기능 내에서 사용되는 타입스크립트 타입
|
+-- utils       # 특정 기능을 위한 유틸리티 함수

모든 기능에 이 모든 폴더가 필요한 것은 아니다. 필요한 폴더만 포함하면 된다.

과거에는 barrel 파일을 사용해 기능에서 모든 파일을 내보내도록 권장했지만, 이는 Vite가 트리 셰이킹을 수행하는 데 문제를 일으켜 성능 문제로 이어질 수 있다. 따라서 파일을 직접 임포트하는 것이 좋다.

기능 간의 임포트는 피하는 것이 좋다. 대신 애플리케이션 레벨에서 다양한 기능을 조합한다. 이를 통해 각 기능이 독립적으로 유지되어 코드베이스가 덜 복잡해진다.

기능 간의 임포트를 금지하려면 다음과 같이 ESLint를 사용할 수 있다.

'import/no-restricted-paths': [
    'error',
    {
        zones: [
            // 기능 간 임포트를 비활성화:
            // 예: src/features/discussions는 src/features/comments에서 임포트하지 않음
            {
                target: './src/features/auth',
                from: './src/features',
                except: ['./auth'],
            },
            {
                target: './src/features/comments',
                from: './src/features',
                except: ['./comments'],
            },
            {
                target: './src/features/discussions',
                from: './src/features',
                except: ['./discussions'],
            },
            {
                target: './src/features/teams',
                from: './src/features',
                except: ['./teams'],
            },
            {
                target: './src/features/users',
                from: './src/features',
                except: ['./users'],
            },
            // 등등...
        ],
    },
],

단방향 코드베이스 아키텍처를 적용하는 것도 좋다. 이는 코드가 공유된 부분에서 애플리케이션으로 한 방향으로 흐르도록 하는 것이다(공유 -> 기능 -> 애플리케이션). 이는 코드베이스를 더 예측 가능하고 이해하기 쉽게 만든다.

단방향 코드베이스

공유된 부분은 코드베이스의 어느 부분에서도 사용할 수 있지만, 기능은 공유된 부분에서만 임포트할 수 있고, 애플리케이션은 기능과 공유된 부분에서 임포트할 수 있다.

다음과 같이 ESlint로 제한할 수 있다.

'import/no-restricted-paths': [
    'error',
    {
        zones: [
            // 단방향 코드베이스 강제:
            // 예: src/app은 src/features에서 임포트할 수 있지만 반대는 안 됨
            {
                target: './src/features',
                from: './src/app',
            },
 
            // 예: src/features와 src/app은 공유 모듈에서 임포트할 수 있지만 반대는 안 됨
            {
                target: [
                    './src/components',
                    './src/hooks',
                    './src/lib',
                    './src/types',
                    './src/utils',
                ],
                from: ['./src/features', './src/app'],
            },
        ],
    },
],

마무리

이러한 구성을 따르면 코드베이스를 잘 조직하고, 확장 가능하며, 유지 보수하기 쉽게 만들 수 있다. 이는 팀이 프로젝트에서 더 효율적이고 효과적으로 작업할 수 있도록 돕는다. 이 접근 방식은 Next.js, Remix 또는 React Native로 구축된 앱에 유사한 아키텍처를 적용하는 것도 더 쉽게 만든다.