이 글은 다음 글을 번역한 글입니다: React Folder Structure in 5 Steps

 

React Folder Structure in 5 Steps - RWieruch

How to structure large React apps into folders and files for a scalable React project ...

www.robinwieruch.de

 

 

 

1. 앞선 글

 

https://www.robinwieruch.de/react-folder-structure

 

큰 규모의 리액트 앱을 폴더와 파일로 구성하는 방법은 서로 다른 의견이 많은 주제입니다. 정답이 존재하지 않는 주제이기 때문에 이 주제에 대해 글을 쓰는 한동안 고생했습니다. 하지만 매주 사람들이 내게 자신의 리액트 프로젝트를 어떻게 구성해야 하는지 물어봅니다. 폴더 구조는 작은 규모의 리액트 프로젝트뿐 아니라 확장 가능한 리액트 애플리케이션에서 더 중요합니다. 수년간 개인 프로젝트, 고객의 프로젝트의 리액트 애플리케이션을 구현했고 이러한 문제에 내가 어떻게 접근했는지 설명하도록 하겠습니다. 5개의 단계만 거치면 여러분에게 의미가 있는 게 어떤 것인지 정할 수 있고 어느 정도까지 원하는지 알 수 있습니다.  그럼 시작해 보도록 하겠습니다.

 

"난 내가 좋다고 느껴질 때까지 파일을 옮깁니다"라고 말하는 사람은 아마 혼자 개발하는 사람이나 소규모 팀에서는 괜찮을 수도 있습니다. 하지만 한 기능을 4명이 개발하고 5개의 팀이 있는 회사에서 실제로 수행할 수 있을까요? 더 큰 규모의 팀에서는 "명확한 비전 없이 그냥 파일을 이동하는 것"은 더 까다롭습니다. 게다가 고객이 내게 이 문제에 대해 물어봤을 때 말할 수 있는 내용이 아닙니다. 따라서 이 연습은 좀 더 명확한 것을 찾는 모든 이들을 위한 참조 가이드입니다.

 

 

 

2. 단일 리액트 파일.

 

첫 번째 단계는 다음 규칙을 따릅니다: 하나의 파일로 모두를 규정짓습니다. 대부분의 리액트 프로젝트는 src 폴더와 App 컴포넌트가 있는 하나의 src/App.js 파일로 시작합니다. creatr-react-app을 사용했을 때 얻을 수 있는 최소한의 것입니다. 이 App 함수형 컴포넌트는 다음과 같은 것을 렌더링 합니다:

 

import React from 'react';
const App = () => {
  const title = 'React';
  return (
    <div>
      <h1>Hello {title}</h1>
    </div>
  );
}
export default App;

 

결국 이 컴포넌트에 더 많은 기능을 추가하고 자연스럽게 사이즈가 커지게 되며 일부를 독립된 리액트 컴포넌트로 추출해내야 합니다. 다음은 App 컴포넌트에서 자식 컴포넌트로 list 컴포넌트를 추출해 낸 것입니다:

 

import React from 'react';
const list = [
  {
    id: 'a',
    firstname: 'Robin',
    lastname: 'Wieruch',
    year: 1988,
  },
  {
    id: 'b',
    firstname: 'Dave',
    lastname: 'Davidds',
    year: 1990,
  },
];
const App = () => <List list={list} />;
const List = ({ list }) => (
  <ul>
    {list.map(item => (
      <ListItem key={item.id} item={item} />
    ))}
  </ul>
);
const ListItem = ({ item }) => (
  <li>
    <div>{item.id}</div>
    <div>{item.firstname}</div>
    <div>{item.lastname}</div>
    <div>{item.year}</div>
  </li>
);

 

새로운 리액트 프로젝트를 시작할 때마다 한 파일에 여러 컴포넌트를 포함시키는 것이 괜찮다고 말합니다. 심지어 큰 규모의 프로젝트에서 한 컴포넌트가 다른 컴포넌트에 긴밀히 묶이는 것도 괜찮습니다. 하지만 이 시나리오에서는 결국 리액트 프로젝트에서 한 파일만으로는 더 이상 충분하지 않습니다. 이제 2단계로 전환을 해야 할 때입니다.

 

 

 

3. 여러 리액트 파일.

 

두 번째 단계는 다음 규칙을 따릅니다: 여러 파일로 모두를 규정짓습니다. List와 ListItem 컴포넌트를 갖는 이전 App 컴포넌트를 예시로 들어보겠습니다. 모든 것을 가진 하나의 src/App.js 파일 대신 컴포넌트들을 여러 개의 파일로 분리할 수 있습니다. 여기서 여러분이 얼마나 멀리 갈지 정합니다. 예를 들어 전 다음 폴더구조 까지만 갈 겁니다:

 

- src/
--- App.js
--- List.js

 

src/List.js에는 List와 ListItem 컴포넌트의 자세한 구현을 갖고 있을 수도 있지만 List 컴포넌트를 분리해 공용 API로 다음과 같은 파일로 내보냅니다.

 

const List = ({ list }) => (
  <ul>
    {list.map(item => (
      <ListItem key={item.id} item={item} />
    ))}
  </ul>
);
const ListItem = ({ item }) => (
  <li>
    <div>{item.id}</div>
    <div>{item.firstname}</div>
    <div>{item.lastname}</div>
    <div>{item.year}</div>
  </li>
);
export default List;

 

그러면 src/App.js는 List 컴포넌트를 import해와 사용할 수 있습니다.

 

import React from 'react';
import List from './List';
const list = [
  {
    id: 'a',
    firstname: 'Robin',
    lastname: 'Wieruch',
    year: 1988,
  },
  {
    id: 'b',
    firstname: 'Dave',
    lastname: 'Davidds',
    year: 1990,
  },
];
const App = () => <List list={list} />;

 

만약 더 나아가길 원한다면 ListItem 컴포넌트도 별도 파일로 추출하고 List 컴포넌트가 ListItem 컴포넌트를 import 하도록 할 수 있습니다.

 

- src/
--- App.js
--- List.js
--- ListItem.js

 

하지만 앞서 말한 것처럼 ListItem 컴포넌트는 List 컴포넌트와 강하게 묶여 있기 때문에 좀 더 멀리 가야 할 수도 있습니다. 그래서 ListItem을 src/List.js 파일에 남겨두는 것도 괜찮습니다. 저는 리액트 컴포넌트가 재사용 가능한 리액트 컴포넌트가 될 때마다 다른 리액트 컴포넌트에서 액세스 할 수 있도록 마치 앞서 List 컴포넌트에서 했던 것처럼 독립적인 파일로 나누는 경험의 법칙을 따릅니다. 

 

 

 

4. 리액트 파일에서 리액트 폴더로.

 

여기부터 더 흥미로워지고 의견이 다양해집니다. 모든 리액트 컴포넌트는 결국 복잡해집니다. 더 많은 조건부 렌더링이나 리액트 훅과 같은 더 많은 로직이 더해지고 스타일링과 테스트 같은 더 많은 기술적 고려사항 해문입니다. 별다른 지식이 필요하지 않은 접근 방식은 리액트 컴포넌트 옆에 더 많은 파일을 추가하는 것입니다. 예를 들어 모든 리액트 컴포넌트에 테스트와 스타일 파일이 있다고 가정해 봅시다.

 

- src/
--- App.js
--- App.test.js
--- App.css
--- List.js
--- List.test.js
--- List.css

 

src 폴더 내의 모든 추가 컴포넌트에 대해 개별 컴포넌트를 보기 힘들어질 것이므로 잘 확장되지 않는다는 것을 이미 알 수 있습니다. 이로 인해 저는 각각의 리액트 컴포넌트에 하나의 폴더를 갖게 합니다.

 

- src/
--- App/
----- index.js
----- test.js
----- style.css
--- List/
----- index.js
----- test.js
----- style.css

 

이 파일들의 네이밍은 여러분에게 달려있습니다. 예를 들어 index.js, component.js, test.js, spec.js가 될 수 있습니다. 게다가 만약 여러분이 CSS를 사용하지 않고 Styled Components와 같은 것을 사용하고 싶다면 파일 확장자도 style.css에서 style.js가 될 수 있습니다. 여러분들의 네이밍 규칙에 익숙해졌다면 IDE에서 "List index" 혹은 "App test"를 검색해 파일을 열 수도 있습니다. 컴포넌트의 폴더를 모두 축소하면 여러분은 매우 간결하고 명확한 폴더 구조를 볼 수 있습니다.

 

- src/
--- App/
--- List/

 

예를 들어 특정 컴포넌트에 대한 커스컴 훅을 파일로 추출해야 하는 것과 같이 컴포넌트에 기술적으로 고려해야 할 점이 많다면 컴포넌트 폴더 내에서 수평적으로 확장하는 방법으로 접근할 수 있습니다.

 

- src/
--- App/
----- index.js
----- test.js
----- style.css
--- List/
----- index.js
----- test.js
----- style.css
----- hooks.js

 

ListItem 컴포넌트를 별도의 파일로 추출해 List/Indes.js를 좀 더 가볍게 유지하기로 정했다면 다음과 같은 폴더구조를 가질 수 있습니다.

 

- src/
--- App/
----- index.js
----- test.js
----- style.css
--- List/
----- index.js
----- test.js
----- style.css
----- ListItem.js

 

여기에 다시 한 단계 더 나아가 컴포넌트 폴더에 테스트와 스타일과 같은 기술적 고려사항을 더할 수도 있습니다.

 

- src/
--- App/
----- index.js
----- test.js
----- style.css
--- List/
----- index.js
----- test.js
----- style.css
----- ListItem/
------- index.js
------- test.js
------- style.css

 

중요한 점은 여기부터 컴포넌트가 서로 너무 깊게 중첩되지 않도록 신경 써야 합니다. 제 경험에 따라 저는 컴포넌트가 두 레벨 이상 중첩되지 않도록 합니다. 따라서 List 폴더 내 ListItem 폴더는 괜찮지만 ListItem 폴더에 다른 중첩된 폴더가 있어서는 안 됩니다. 

 

중간 사이즈의 리액트 프로젝트 규모를 넘어설 것이 아니라면 이것이 여러분의 리액트 컴포넌트를 구조화하는 방법이라고 생각합니다. 하지만 제가 언급했듯이 이는 이견이 있을 수 있고 모든 이들의 입맛에 맞지는 않을 수 있습니다.

 

 

 

5. 기술적인 폴더를 분리.

 

다음 단계는 중대형 이상 규모의 리액트 애플리케이션을 구조화하는데 도움이 될 것입니다. 여러 컴포넌트에서 사용하는 기능을 컴포넌트로부터 분리합니다. 다음 폴더 구조에서 예를 들어보도록 하겠습니다.

 

- src/
--- components/
----- App/
------- index.js
------- test.js
------- style.css
----- List/
------- index.js
------- test.js
------- style.css

 

이전 리액트 컴포넌트를 새로운 component 폴더로 그룹화합니다. 이것은 다른 카테고리 폴더를 생성하기 위한 수직적 레이어를 제공해 줍니다. 예를 들어 어느 시점에 여러 컴포넌트에서 사용할 수 있는 리액트 훅이 있을 수도 있습니다. 이 훅을 컴포넌트에 강하게 결합하는 대신 모든 컴포넌트에서 사용할 수 있도록 전용 폴더에 훅의 구현을 위치시킬 수 있습니다.

 

- src/
--- components/
----- App/
------- index.js
------- test.js
------- style.css
----- List/
------- index.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside/
------- index.js
----- useData/
------- index.js

 

모든 훅이 폴더 내에 있어야 한다는 의미는 아닙니다. 한 컴포넌트에만 사용되는 리액트 훅은 여전히 컴포넌트 파일에 남아 있거나 별도의 hook.js 파일에 있어야 합니다. 오로지 모든 리액트 컴포넌트에서 사용될 수 있는 훅만이 hook 폴더에 있어야 합니다. 

 

만약 여러분이 프로젝트에서 다른 모든 파일에서 글로벌하게 액세스 되어야 하는 React Context를 사용하는 경우 같은 전략이 적용될 수 있습니다.

 

- src/
--- components/
----- App/
------- index.js
------- test.js
------- style.css
----- List/
------- index.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside/
------- index.js
----- useData/
------- index.js
--- context/
----- Session/
------- index.js

 

여기에 컴포넌트와 훅에 접근이 필요한 다른 유틸리티가 있을 수도 있습니다. 다양한 유틸리티의 경우 저는 주로 service 폴더를 생성합니다. 이름을 여러분에게 달려있지만 이 기술적인 분리를 이끄는 것은 우리 프로젝트의 다른 코드에서 로직을 사용할 수 있도록 한다는 원칙입니다. 

 

- src/
--- components/
----- App/
------- index.js
------- test.js
------- style.css
----- List/
------- index.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside/
------- index.js
----- useData/
------- index.js
--- context/
----- Session/
------- index.js
--- services/
----- ErrorTracking/
------- index.js
------- test.js
----- Format/
------- Date/
--------- index.js
--------- test.js
------- Currency/
--------- index.js
--------- test.js

 

예를 들어 Data/index.js 파일을 봅시다. 자세한 구현은 다음과 같습니다:

 

export const formatDateTime = (date) =>
  new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'numeric',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    second: 'numeric',
    hour12: false,
  }).format(date);
export const formatMonth = (date) =>
  new Intl.DateTimeFormat('en-US', {
    month: 'long',
  }).format(date);

 

다행히도 자바스크립트의 Intl API는 날짜 변환을 위한 훌륭한 도구들을 제공합니다. 하지만 리액트 컴포넌트에서 API를 직접적으로 사용하는 것 대신에 서비스를 제공하는 방식이 더 좋습니다. 이 방식을 사용하면 내 컴포넌트에 애플리케이션에서 사용할 수 있는 날짜 형식 옵션을 보장할 수 있기 때문입니다. 그렇지 않으면 애플리케이션이 커가면서 많은 다른 날짜 포맷이 있을 겁니다. 

 

이제 날짜 형식 함수를 개별적으로 import 해올 수 있습니다.

 

import { formatMonth } from '../../services/format/date';
const month = formatMonth(new Date());

 

하지만 제가 더 선호하는 서비스로서 캡슐화된 코드는 다음과 같습니다.

 

import * as dateService from '../../services/format/date';
const month = dateService.formatMonth(new Date());

 

상대 경로가 있는 항목을 가져오는 것은 어려울 수도 있습니다. 따라서 전 항상 별칭을 위한 Babel의 Module Resolver를 사용합니다. 이후 import는 다음과 같이 바꿔 쓸 수 있습니다.

 

import * as dateService from '@format/date';
const month = dateService.formatMonth(new Date());

 

저는 모든 폴더에 전용의 목적을 부여하고 애플리케이션에서 기능을 공유하도록 하기 때문에 이러한 기술적인 분리의 고려를 좋아합니다.

 

 

 

6. 도메인 폴더 분리.

 

마지막 단계는 대규모 리액트 애플리케이션을 구성하는데 도움이 됩니다. 기술적으로 분리된 폴더에 많은 서브 폴더가 있는 경우 발생할 수 있는 일입니다. 예시는 전체를 보여주진 않지만 요점을 이해하길 바랍니다.

 

- src/
--- components/
----- App/
----- List/
----- Input/
----- Button/
----- Checkbox/
----- Profile/
----- Avatar/
----- MessageItem/
----- MessageList/
----- PaymentForm/
----- PaymentWizard/
----- ErrorMessage/
----- ErrorBoundary/

 

여기부터는 UI 컴포넌트처럼 재사용 가능한 컴포넌트만 components 폴더를 사용합니다. 다른 모든 컴포넌트는 도메인 중심의 폴더로 이동시켜야 합니다. 폴더 이름은 여러분에게 달려있습니다.

 

- src/
--- domain/
----- User/
------- Profile/
------- Avatar/
----- Message/
------- MessageItem/
------- MessageList/
----- Payment/
------- PaymentForm/
------- PaymentWizard/
----- Error/
------- ErrorMessage/
------- ErrorBoundary/
--- components/
----- App/
----- List/
----- Input/
----- Button/
----- Checkbox/

 

PaymentForm이 Button이나 Input에 액세스 해야 하는 경우 재사용 가능한 UI 컴포넌트 폴더에서 가져와 사용하면 됩니다. MessageList 컴포넌트에 추상 List 컴포넌트가 필요하면 List 컴포넌트를 가져와 사용하면 됩니다. 더 나아가 이전 단계의 서비스가 도메인에 강하게 결합되어 있으면 서비스를 특정 도메인 폴더로 이동시킵니다. 앞서 기술적으로 분리된 폴더에도 동일하게 적용될 수 있습니다.

 

- src/
--- domain/
----- User/
------- Profile/
------- Avatar/
----- Message/
------- MessageItem/
------- MessageList/
----- Payment/
------- PaymentForm/
------- PaymentWizard/
------- services/
--------- Currency/
----------- index.js
----------- test.js
----- Error/
------- ErrorMessage/
------- ErrorBoundary/
------- services/
--------- ErrorTracking/
----------- index.js
----------- test.js
--- components/
--- hooks/
--- context/
--- services/
----- Format/
------- Date/
--------- index.js
--------- test.js

 

각 도메인 폴더에 중간중간 services 폴더가 필요한지 여부는 여러분에게 달려있습니다. services 폴더를 제외하고 ErrorTracking폴더의 내용을 Error폴더에 직접 넣을 수도 있습니다만 ErrorTracking은 리액트 컴포넌트가 아닌 서비스로 표시되어야 하기 때문에 혼란스러울 수도 있습니다. 대안으로 ErrorTracking대신 error-tracking처럼 PascalCase보다 kebab-case를 사용해 명명할 수 있습니다.

 

여기에는 개인적인 공간이 존재합니다. 경국 이 단계는 도메인을 하나로 모아 회사의 여러 팀이 프로젝트 전체에서 특정 도메인에서 작업할 수 있도록 하는 것입니다.

 

 

 

7. 맺는 글

 

모든 글이 끝났습니다. 여러분 또는 다른 팀이 리액트 프로젝트를 구성하는데 도움이 되기를 바랍니다. 표시된 접근 방식 중 어느 것도 바꾸기 어렵거나 불가능한 것이 아닙니다. 오히려 전 여러분들이 이것들을 개인에 맞게 적용해 사용하기를 바랍니다. 모든 리액트 프로젝트는 시간이 지남에 따라 규모가 커지므로 대부분의 폴더 구조도 그에 맞게 자연스레 변합니다. 따라서 이 5단계의 과정은 문제가 발생할 경우 몇 가지 지침을 제공하는 가이드일 뿐입니다.

 

 

 

 

 

반응형

+ Recent posts