React & TypeScript/React

[React] 프론트엔드 폴더 구조 방법론 정리 [펌 + 사견]

yoonjong Park 2022. 12. 14.

이런저런 자료들을 찾아보았다. 
결론부터 말하면,

정답은 없다. 정답이 되게 하기 위한 정답에 가까운 구조만이 있을 뿐이다.

오픈소스들 보면서 폴더구조를 참조하는 것도 좋은 방법이다.

Ant design Pro https://github.com/ant-design/ant-design-pro

주요 원칙 (결론)

그래도 원칙은 있는 것 같다.

1. 재사용

2. 중복 방지

3. 팀 시너지 증대

위 3가지 원칙을 지키기 위해 나온 것이 multi-Layerd Architecture (다층화 구조) 라고 보면 된다.

multi-layerd architecture 적용 예시

 

 

리액트에서 추천하는 분류법 (공식문서)

1. 파일의 기능이나 라우트에 의한 분류

최초에 src폴더를 이렇게 만들어봤는데, 폴더별로 가지는 파일 수가 너무 많이 늘어나는 경향이 있었다.

가벼운 프로젝트에서 사용할만한 방법이다. (실무에서는 되도록 pass하자...)

common/
  Avatar.js
  Avatar.css
  APIUtils.js
  APIUtils.test.js
feed/
  index.js
  Feed.js
  Feed.css
  FeedStory.js
  FeedStory.test.js
  FeedAPI.js
profile/
  index.js
  Profile.js
  ProfileHeader.js
  ProfileHeader.css
  ProfileAPI.js

2. 파일 유형에 의한 분류

Atomic Design이 이와 같은 분류에 속한다.

api/
  APIUtils.js
  APIUtils.test.js
  ProfileAPI.js
  UserAPI.js
components/
  Avatar.js
  Avatar.css
  Feed.js
  Feed.css
  FeedStory.js
  FeedStory.test.js
  Profile.js
  ProfileHeader.js
  ProfileHeader.css

위와 같은 글들과 너무 많은 중첩을 피하세요, 너무 깊게 생각하지 마세요. 같은 말을 페북에서도 한다. 일단 코드를 시작하고 코드를 작성하면서 구조를 만들어나가라고 조언하고 있다.
프로젝트마다 성격이 다르니까, 자기들도 정의하기 어렵다. 결국 모르겠다는 말을 하고 있다ㅋㅋㅋㅋ (당연한 말이긴 하지만...😆)

 

가장 좋았던 사례 #

바쁘면 이 것만 보는 거다. 사례 보다가 사례에 깔려 죽는다 🙃

나의 사견은 보라색으로 넣어두었다.

 

0. 전체 구조

1. API 계층

axios와 같은 Promise기반 http 클라이언트를 사용하면, 각 API에 대한 함수를 정의해서 호출에 필요한 모든 로직을 캡슐화할 수 있는데요. 다음 코드와 같이 API요청이 실제 사용되는 위치와 정의된 위치를 나눌 수 있습니다.

이런식으로, 모든 API요청을 정의해놓은 모든 파일들을 API계층의 폴더에 넣어놓습니다.

이렇게 까지 분할할 필요가 있을까? 싶기도 했었는데, 필요할 것 같다는 생각이 든다.
이렇게 분할 해놓고 전체 api를 import 하는 파일을 하나 만들어 두고, 그 파일을 import 해서 사용하면 좋겠다 싶다.

 

더보기
// 코드는 나중에 보자

const axiosInstance = axios.create({
   baseURL: 'https://yourdomain.org'
});

export const UserAPI = {
    getAll: function() {
        return axiosInstance.request({
            method: "GET",
            url: `/api/v1/users`
        });
    },
    getById: function(userId) {
        return axiosInstance.request({
            method: "GET",
            url: `/api/v1/users/${userId}`
        });
    },
    create: function(user) {
        return axiosInstance.request({
            method: "POST",
            url: `/api/v1/users`,
            data: user
        });
    },
    update: function(userId, user) {
        return axiosInstance.request({
            method: "PUT",
            url: `/api/v1/users/${userId}`,
            data: user,
        });
    },
}

이렇게 API계층에 정의해놓으면

import React, {useEffect, useState} from "react";
import {UserAPI} from "../../api/user";
//...

function UserComponent(props) {
    const { userId } = props;

    // ...

    const [user, setUser] = useState(undefined)

    // ...

    useEffect(() => {
        UserAPI.getById(userId).then(
            function (response) {
                // response handling
                setUser(response.data.user)
            }
        ).catch(function (error) {
            // error handling
        });
    }, []);

    // ...
}

export default UserComponent;

이런식으로 갖다 씁니다.

2. Asset 계층

멀티미디어 파일과 같이 소스코드가 아닌 파일이 필요할 수도 있는데요. 이런 파일들은 소스코드들과 명확하게 구분이 되어있어야 하기 때문에 전용 폴더를 가지고 있는 것이 좋습니다. 그래서 assets 폴더를 만들어서 해당 파일들을 여기에 위치시키는데요. 이 안에서도 파일의 확장자에 따라 하위 폴더로 나누어서 저장할 수도 있습니다.

파일 확장자에 따른 구분

 

그리고 만약 다국어지원 어플리케이션을 만들고 있다면 모든 번역파일들을 저장할 공간이 되기도 합니다.

다국어처리를 위해 i18next 파일만을 담은 폴더를 만들 수도!

3. component 계층

모든 컴포넌트 파일들이 저장되는 곳입니다. 컴포넌트 하나당 하나의 폴더가 필요합니다. 보통 index.js와 index.css파일이 같이 들어가야 하기 때문이죠...
만약 컴포넌트들의 크기가 너무 클 경우 하위폴더를 만드는 방법도 좋습니다. 하지만, 컴포넌트를 저장하는 구조를 언제든지 바꿀 수 있다는 것도 항상 생각하고 있어야합니다. 

이 부분이 가장 중요하다. 
여기서는 css를 나누었는데, 사실 component는 파일의 규모가 작기 때문에
styled-components 같은 css in js 방식 같은 경우, 1개 파일로 만들어도 무방하다.
그러나, index.js 는 필요하다.
여기서 공용 component는 /block(모듈), /atomic (3단계 계층) 으로 뽑아서 관리하는 것도 좋을 것이다.
특히, User Page의 경우 더 그렇다. Admin은 구조가 매우 간단해서 사실 좀 불필요한 부분이 있는 것도 사실이다.

 

4. constant 계층

상수를 저장하는 곳입니다. 흔한 방법으로는 constans.js파일을 만들어서 모든 상수들을 그 안에 정의할 수도있지만, 역시 프로젝트가 커진다면 해당 파일은 더러워지게되겠죠... 그래서 상수를 여러 파일로 분할해줘야합니다. 

예를들어서, 따로 파일을 만들어서 다국어를 지원하는 어플리케이션에서 모든 i18n과 관련된 모든 상수들만 넣어준다면 관리하기가 더욱 좋습니다!

상수들을 각자 다른 파일에 나누어 담아줬습니다.

/config 같은 형태로 만드는 것 같기도 하다.
환경설정에 관련된 상수들을 모아둔다.
api 주소 or 상태명 같은 그런 것들을 모아두면 좋을 것이다.

5. redux 계층

redux나 vuex와 같이 상태관리 라이브러리를 사용할 경우에만 필요한 곳인데요. 수많은 reducer와 actions 안에서 갈팡질팡해본 경험이 있는 사람만 조용히 손을 들어봅니다.. 게다가 actions는 bolierplate 코드를 포함하기도 합니다. 그래서 모든 actions를 한번에 관리하기 위해 redux-actions와 같은 라이브러리를 사용하기도 합니다. 그러면 아래처럼 정리된 파일 구조를 간단히 얻을 수 있게 됩니다.

하나의 index.js파일과 와 두개의 폴더(actions, reducers)

이처럼 모든 reducer를 "reducers"라는 폴더에 넣고 actions는 "actions"라는 폴더에 각각 담아줬습니다. 그리고 그 상위 레벨 redux 에 index.js를 만들어서 store나 반복해서 사용되는 로직들을 넣을 수 있습니다. 만약 필요하다면, 위의 폴더(redux/reducers, redux/actions)안에서 하위 폴더를 나누어서 정리해둘 수도 있습니다. 

/store 라고 만들어두었다.
redux는 라이브러리명인데, redux를 계속 사용하는 것도 아닌 것 같다.
나머지 세부 규칙은 크게 다르지 않다.

6. route 계층

프로젝트에 이미 모든 routes를 포함하는 routes.js파일이 있을 수도 있는데요. 아마 그 파일 안에다 모든 routes들을 하나로 모아 놓고 실제 사용할 때와 구분하고 있을 것입니다. 작은 프로젝트에서는 이런 방식도 괜찮지만, 문제는 우리의 프로젝트가 얼마나 더 커질지 모른다는 것이죠^^... 이것이 바로 모든 라우팅 파일과 로직들을 포함하는 Routes레이어를 따로 만들어줘야 하는 이유입니다. route.js를 여러 파일로 나누는 쉬운 방법은 다음과 같이 프로젝트에서 사용되는 각 기능의 라우팅으로 각각의 파일을 생성해주는 것입니다 :

각 기능에 따라 4개의 파일로 나누어준 routes들

좋은 생각 같다. 각 domain 별로 관리하는 것은 굉장히 좋은 방법이다.
매번 하던대로 걍 하나의 router.tsx파일에다 한번에 관리했었는데, path별로 분리해봐야겠다.

7. utility 계층

여기는 프로젝트 어디에서든 사용할 수 있도록 따로 만들어놓은 사용자정의 유틸리티 함수를 저장하는 곳입니다. 하나의 파일에 모두 몰아넣을 수도 있지만, 재차 말하지만 프로젝트의 크기가 커지면 강제로 여러개의 파일로 나눠줘야하는 일이 생기면 더 복잡해지때문에, 전용 폴더(계층)를 만들어주는 것이 좋습니다. 

8. view 계층

여기는 구성된 모든 페이지를 저장하는 최상위의 계층입니다. 여태까지 설명했던 다른 계층의 파일들을 import해서 쓸 수 있는 유일한 계층이라는 점을 잊지마세요! 프로젝트의 크기가 커져서 페이지의 양이 많아질 때를 대비해서 하위 폴더로 나누어 놓는것이 좋습니다. 나누는 기준은 라우팅 구조를 그대로 따라가면 좋습니다. 

/Pages에 해당하는 부분이다. URL 별로 유지하자.

나머지 파일들은?

지금까지 말한 폴더(계층)로는 프로젝트에 필요한 모든 소스코드들을 다 커버할 수는 없을 것입니다. 때문에 어디에 위치해야할지 길을 잃은 파일들이 있을수도 있는데요, 만약 3개 이하의 파일이 있다면 그냥 최상위 경로에 놔두는게 좋을 수도 있고, 언제든지 새로운 폴더(계층)을 만들어서 사용해도 됩니다. 

위 구조로 대부분 분류된다.
만약, 분류가 안된다면, 쓸데없이 세분화했을 가능성이 더 높다.
스스로 한번 더 돌아보자.

결론

이 글에서 리액트 프로젝트를 어떻게 정리하고 왜 정리해야 하는지 알아봤습니다.
다층화구조(multi-layered architecture)를 이용해 나누어준 폴더들에 각각의 파일들을 위치시키면서,
우리의 프로젝트는 좀 더 예쁘고 잘 정리된 프로젝트가 되었을 것입니다.
또한 개발팀은 잘 정립된 아키텍처 표준과, 유지보수하기 편한 코드와 간소화된 개발 프로세스를 얻게 되었습니다!

 

다른 사람들의 사례

사례1  #

최근들어 아래와 같이 만든다고 한다.  나도 이 방법에 크게 공감하는데, 중첩이 너무 과해도 문제이기 때문에 Component 별로 모아주고, Pages 에서 Data를 불러주는 형태로 하는 것이 맞지 않을까 싶다.

물론 Test 파일은 Page 단위로 작성하도록 해서, 페이지별 Test Coverage에 달성하도록 한다.

 

사례 2 #

출처 : https://medium.com/geekculture/clean-architecture-in-the-front-end-6e162894acb9

/src
  /assets
  /components
  /context
  /hooks
  /services
  /styles
  /ts
  /utilities
  /views

결론 : 위와 같이 폴더구조가 만들어지는데, 세부 내용은 다음과 같다. (파란색 글씨를 위 Clean Architecture 이미지와 매핑해서 보자)

 

Models / State

Models : 클래스, 인터페이스 등의 어플리케이션 내의 엔티티들을 대표하는 값
State : 프로젝트의 라이프 사이클과 시간의 흐름에 따라 엔티티가 저장되는 곳

 

Context API, Redux, global state handle

 props를 전달하지 않고 컴포넌트 트리 내에서 데이터를 전달하기 위한 방법을 제공한다. 이러한 데이터 상태를 나타내는 자료들은  / context  디렉토리에 포함될 수 있다. 만약, 엔티티(authors, books)를 가지고 있다면 아래와 같이 디렉토리 구조를 짤 수 있다.

types & interfaces

 

Components

Component : 앞서 말한 model과 state를 작동하는 로직을 구성하는 것들

Components

디렉토리에서는 타입(forms, tables, buttons, 등)에 따라 그룹화할 수 있다. 세부사항은 어플리케이션의 특징에 따라 달라진다. 

위 forms 폴더 안의 index.ts 파일은 아래와 같이 구성되어 있다. 이는 다른 컴포넌트에서 forms 내의 컴포넌트들을 간편하게 import할 수 있게 한다. 

// index.ts
import { TextField } from './TextField/TextField'
import { Select } from './Select/Select'

export { TextField, Select}

// 다른 컴포넌트에서 import 할 때
import { TextField, Select} from '@components/forms'

Custom Hooks  (보통 /store로 만드는 전역 상태관리)

subscription 설정 및 현재 값 기억과 같은 상태 로직을 재사용하는 메커니즘이지만, 커스텀 훅을 사용할 때마다 상태 로직 내의 모든 상태 및 효과가 완전히 isolated 된다.

styled-components

어플리케이션의 시각적 요소를 일컷는다.

Utilities / Helpers

프로젝트 로직 흐름에서 종종 사용되는 짧고 특정한 함수들을 넣는다. 만약, 프로젝트에 utility 폴더가 필요하던 말던 프로젝트 내에는 validation이나 conversation 등 다양한 부분에서 공통적으로 사용되는 함수들이 존재할 것이다. 이러한 함수들을 utils 에 넣어두면 된다.

 

Adaptors

Adaptors: API 호출이나 웹 소켓 등 어플리케이션과 외부(클라이언트, 서비스)의 데이터를 공유하는 연결통로이다. 

Interceptors

특정 URI로 요청시 Controller로 가는 요청을 가로채 수정할 수 있게 한다.

 

External Services

External Services : 어플리케이션 작동에 필요한 데이터를 얻기 위한 API, 웹 서비스 호출

 

Etc

/ assets

어플리케이션에서 필요한 정적인 멀티미디어 파일들 (images, icons, videos, 등)

/styles

컴포넌트에 공용적으로 사용되는 스타일들 (styled-components, css, scss)

/views

Views/Pages

어플리케이션에 있는 페이지들, 각각의 view는 components, context, services 등을 가진다.

 

자, 이제 준나 코딩이다ㅋㅋ

 

참고

가장 도움되었던 글 1,2

 

프론트엔드 아키텍처 다층화구조(layered architecture) (+실제 폴더 구성)

해당 글은 아래 원문을 번역하여 다시 작성한 글입니다. (https://blog.logrocket.com/optimize-react-apps-using-a-multi-layered-structure/) 리액트로 사이드프로젝트를 만들면서 리액트의 구조를 어떻게 만들어야

imagineu.tistory.com

 

Optimize React apps using a multi-layered structure - LogRocket Blog

React projects can quickly become a mess. Here, you can learn how to use a multi-layered structure to optimize React apps.

blog.logrocket.com

 

파일 구조 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

[React] 프로젝트 디렉토리 구조 도대체 어떻게 잡아야 되나요? - 고민하지 마세요🙃

지난 시간동안 다수의 React 프로젝트를 진행하면서 프로젝트를 빌드해 갈때마다 디렉토리 구조에 대해서 고민했습니다. 어떤 방법으로 디렉토리 구조를 잡을지는 언제나 어려웠으면 최선, 최고

xtring-dev.tistory.com

 

 

프론트엔드 개발 프로젝트 폴더 구조

지금까지 2개의 프로젝트를 해 보았다. 첫 번째 프로젝트의 구조는 지금 보면 한숨만 나올 정도로 darty하고 confusing하다. 구조를 바꿀 생각도 못한다. 프로젝트 중간에 몇번 바꿀까 같이 개발하는

sennieworld.tistory.com

 

 

Clean Architecture on Frontend

In this post, I describe what the clean architecture is, how to use it with JS/TS code bases and if it's even worth it.

bespoyasov.me

 

댓글