출처(번역) React Folder Structure in 5 Steps
대규모 리액트 어플리케이션의 폴더 및 파일 구조를 어떻게 설계하는가는 핫한 주제 중 하나입니다. 사람들이 소규모부터 대규모 프로젝트에 이르기까지, 저에게 리액트의 폴더 구조를 설계하는 방법에 대해서 질문합니다. 이 주제에 대한 정해진 정답이 없기 때문에, 작성하는 데에 꽤 오랜 시간이 걸렸습니다.
지난 몇년동안 리액트를 구현하면서, 제 개인 프로젝트(freelance, React workshops)를 통해 어떻게 제가 이 문제에 접근하는지 설명해드리겠습니다. 5 단계를 통해서 자신에게 얼마나 필요한 것인지 아닌지 결정할 수 있습니다. 시작하겠습니다.
누군가는 "나는 파일이 이동될 때까지 옮기는걸 반복해" 라고 말합니다. 이건 1인 개발자로써는 괜찮을지 모릅니다. 하지만 서로 다른 기능을 개발하는 4인 혹은 5인이 있는 회사에서는 어떨까요? 규모가 큰 팀에서는 명확한 이유 없이 파일을 이동하는 것이 까다로워집니다. 또한, 고객이 이 문제에 대해 질문할 때 답할 수 있는 사항이 아닙니다. 따라서,
이 주제에 대한 답안을 찾는 분이라면 참조 가이드로 사용하세요.
1. 단일 리액트 파일
첫번째 단계는 하나의 파일로 모든걸 관리하는 겁니다. 대부분의 리액트 프로젝트는 src/
그리고 src/App.js
로 시작합니다. create-react-app
을 사용할 때 얻는 것들이고, JSX를 렌더링하는 함수 구성 요소입니다.
import * as React from 'react';
const App = () => {
const title = 'React';
return (
<div>
<h1>Hello {title}</h1>
</div>
);
}
export default App;
갑자기 이 컴포넌트에 더 많은 기능을 추가한다면 자연스럽게 사이즈가 커지고, 결국 독립적인 리액트 컴포넌트로 분리해야합니다. 아래 예시에서 App component
로부터 ListItem 컴포넌트와 List 컴포넌트를 추출했습니다.
import * as 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단계를 적용해야할 때입니다.
2. 다중 리액트 파일
두번째 단계는 여러 파일을 모두 관리하는 겁니다. List 및 ListItem이 있는 이전 예시로 설명하겠습니다. src/App.js
에 모두 담기보다, 여러개의 파일로 컴포넌트를 분할할 수 있습니다. 여기에서, 두 컴포넌트간 얼마나 멀어질지 결정해야합니다. 아래 예시 폴더 구조를 보면,
src/
--- App.js
--- List.js
src/List.js
에는 List 및 ListItem의 세부적인 구현정보를 갖고있지만, List 컴포넌트만 export 합니다.
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 { List };
다음으로 src/App.js
에서 List 를 import 합니다.
import * as React from 'react';
import { List } from './List';
const list = [ ... ];
const App = () => <List list={list} />;
더 나아가서, 만약 ListItem도 분리하여 독립적인 파일로 만든다면, src/List.js
에서 ListItem을 import 합니다.
src/
--- App.js
--- List.js
--- ListItem.js
그러나 전에 말했듯이, 컴포넌트간 거리가 멀어지면 이해하는데 너무 오래 걸릴 수 있습니다. ListItem은 List와 밀접하게 연결되어 있으므로 src/List.js
에 그대로 두어도 됩니다.
저는 List처럼 재사용 가능한 컴포넌트를 독립적인 파일로 분리합니다.
이 방법은 다른 리액트 컴포넌트에서도 List에 접근할 수 있도록 만듭니다.
3. 리액트 파일에서 리액트 폴더로
여기서부터 흥미로울겁니다. 모든 리액트 컴포넌트는 결국 복잡성을 증가시킵니다. 꼭 로직(ex: 조건부 렌더링, React Hooks, Event Handler, JSX)을 추가해서 더 복잡해지는 것이 아닙니다. CSS, test와 같은 더 많은 문제들이 있기 때문입니다. 순수하게 계속해서 폴더에 리액트 컴포넌트를 추가해보겠습니다. 예시처럼 리액트 컴포넌트는 CSS와 test 파일을 가지게됩니다.
src/
--- App.js
--- App.test.js
--- App.css
--- List.js
--- List.test.js
--- List.css
src/
에 추가된 많은 구성요소들로 인해 개별 구성요소를 볼수 없습니다.
따라서 이 방법은 좋게 확장되었다고 할 수 없습니다. 그래서 저는 One Folder - One Component를 좋아합니다.
src/
--- App/
------ index.js
------ component.js
------ test.js
------ style.css
--- List/
------ index.js
------ component.js
------ test.js
------ style.css
새로운 컴포넌트가 생길때마다 CSS와 test가 생성되고, component.js
에서 로직을 확인할 수 있습니다.
새로운 index.js
파일에서는 폴더 외부와의 인터페이스를 작성합니다.
예시처럼, List 컴포넌트는 보통 아래처럼 작성합니다.
export * from './List';
App 컴포넌트에서 List 컴포넌트를 import할때는 아래처럼 작성할겁니다.
import { List } from '../List/index.js';
하지만 자바스크립트에서는 /index.js가 기본값이기 때문에 아래처럼 작성할 수 있습니다.
import { List } from '../List';
이 파일의 이름은 이미 명시적입니다. 예를 들어, 파일이 다중화되어도, test.js
는 spec.js
로, style.css
는 styles.css
가 될 수 있습니다.
만약, CSS를 사용하지 않고 Styled Components를 사용한다면, style.css
를 style.js
로 만들면 됩니다.
폴더 이름 작성 규칙이 익숙해지면, IDE에서 List component 혹은 App test로 검색하여 각 파일을 열 수 있습니다. 저는 개인적으로 간결한 파일 이름을 선호하지만, 다른 사람들은 긴 파일 이름을 선호한다는건 인정합니다.
src/
--- App/
------ index.js
------ App.js
------ App.test.js
------ App.style.css
--- List/
------ index.js
------ List.js
------ List.test.js
------ List.style.css
어쨋든 파일 이름에 관계없이 모든 폴더를 축소하면 매우 간결하고 명확한 구조를 갖게 됩니다.
src/
--- App/
--- List/
예를 들어 컴포넌트에 더 많은 기술적인 문제(ex: Typescript types, storybook, helper functions, Javascript Constants)가 있는 경우 전용 폴더 내에서 수평으로 확장하며 해결할 수 있습니다.
src/
--- App/
------ index.js
------ component.js
------ test.js
------ style.css
------ types.js
--- List/
------ index.js
------ component.js
------ test.js
------ style.css
------ hooks.js
------ story.js
------ types.js
------ utils.js
------ constants.js
만약 List/component.js
를 더 가볍게 만드려고 ListItem을 하나의 파일로 만들기로 결정했다면 아래 폴더구조처럼 만들수도 있습니다.
src/
--- App/
------ index.js
------ component.js
------ test.js
------ style.css
--- List/
------ index.js
------ component.js
------ test.js
------ style.css
------ ListItem.js
여기서, 폴더 구조를 중첩시켜 한단계 더 깊게 만들수도 있습니다.
src/
--- App/
------ index.js
------ component.js
------ test.js
------ style.css
--- List/
------ index.js
------ component.js
------ test.js
------ style.css
------ ListItem/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
중요: 여기서 컴포넌트가 너무 깊게 가지 않도록 주의해야합니다. 경험적으로 컴포넌트가 2개 이상 중첩하면 안됩니다. 따라서 현재 List 및 ListItem은 괜찮지만, ListItem 폴더 내에서 또 다른 폴더가 있어서는 안됩니다.
중간 규모의 리액트 프로젝트까지 리액트 컴포넌트를 구조화하는 방법이라고 생각합니다. 리액트 프리랜서로 활동하는 제 경험상, 많은 리액트 프로젝트가 이 구조를 따릅니다.
기술적(범주) 폴더
이 단계는 중간 혹은 대규모 리액트 프로젝트를 구조화하는데 도움이 될겁니다. 리액트 컴포넌트에서 재사용 가능한 hooks나 context를 분리합니다. 아래 폴더 구조를 예로 들어보겠습니다.
src/
--- components/
------ App/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
------ List/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
모든 리액트 컴포넌트는 components/
에서 그룹화되었습니다. 이건 또다른 범주에 대한 폴더를 구성할 수 있는 수직 레이어를 제공합니다. 예를 들어, 어느 시점에서 여러 컴포넌트가 사용하는 React Hooks가 존재할 수 있습니다. 그래서, 컴포넌트와 custom Hooks를 강하게 결합하는 대신에, 모든 리액트 컴포넌트가 사용할 수 있는 전용 폴더에 Hooks를 담을 수 있습니다.
src/
--- components/
------ App/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
------- List/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
--- hooks/
------- useClickOutside.js
------- useScrollDetect.js
그렇다고 모든 Hooks가 hooks/
폴더 내에 있어야 하는건 아닙니다. 하나의 컴포넌트에서만 사용되는 전용 Hooks는 components.js
옆 hooks.js
파일로 사용할 수 있습니다. 재사용 가능한 Hooks만 hooks/
폴더에 저장하세요. 만약 하나의 Hooks에 더 많은 파일이 필요할 경우 아래처럼 다시 폴더로 구조화할 수 있습니다.
src/
--- components/
------ App/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
------- List/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
--- hooks/
------ useClickOutside/
--------- index.js
--------- hook.js
--------- test.js
------ useScrollDetect/
--------- index.js
--------- hook.js
--------- test.js
리액트 프로젝트에서 React Context를 사용할 경우 동일한 전략을 사용할 수 있습니다. Context는 어딘가에서 인스턴스화되어야 하기 때문에 전용폴더/파일
방식이 모범 사례입니다. 수많은 리액트 컴포넌트에서 Context에 접근할 수 있어야 하기 때문입니다.
src/
--- components/
------ App/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
------- List/
--------- index.js
--------- component.js
--------- test.js
--------- style.css
--- hooks/
------- useClickOutside.js
------- useScrollDetect.js
--- context/
------- Session.js
여기서 components/
뿐만 아니라 hooks/
및 context/
에 접근하고 싶은 다른 유틸리티가 있을수도 있습니다. 저는 보통 services/
를 생성합니다. 이름은 꼭 services/
가 아니어도 됩니다. utils/
로 지어도 상관없습니다. 그러나 services/
가 utils/
에 비해 더 많은 의미를 내포하고 범용적입니다.
src/
--- components/
------ App/
---------- index.js
---------- component.js
---------- test.js
---------- style.css
------- List/
---------- index.js
---------- component.js
---------- test.js
---------- style.css
--- hooks/
------- useClickOutside.js
------- useScrollDetect.js
--- context/
------- Session.js
--- services/
------- ErrorTracking/
---------- index.js
---------- service.js
---------- test.js
------- Format/
---------- Date/
------------- index.js
------------- service.js
------------- test.js
---------- Currency/
------------- index.js
------------- service.js
------------- test.js
Date/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를 즉시 사용하는 것 대신, 서비스를 제공하는 것이 좋습니다. 서비스만 제공하면 컴포넌트 내에 날짜 형식 관련 로직이 없다는 것을 보장할 수 있기 때문입니다.
Date/index.js
를 통해 각 날짜에 대해 개별적으로 import할 수 있음과 동시에 다음과 같은 방식으로 응용할 수도 있습니다.
import { formatMonth } from '../../services/format/date';
const month = formatMonth(new Date());
또한 services/
는 캡슐화된 모듈로써 제가 좋아하는 것입니다.
import * as dateService from '../../services/format/date';
const month = dateService.formatMonth(new Date());
슬슬 상대경로로 import하기 어려워질 수 있습니다. 그러므로 저는 동료들을 위해 항상 Babel's Module Resolver 별칭 방식을 선택합니다. 적용하면 다음과 같이 import하게 될 겁니다.
import * as dateService from 'format/date';
const month = dateService.formatMonth(new Date());
저는 이러한 범주 방식의 분리가 마음에 듭니다. 모든 폴더에 전용 목적을 부여하고, 리액트 어플리케이션 전체에서 기능 공유를 할 수 있기 때문입니다.
기능 폴더
마지막 단계는 대규모 리액트 어플리케이션 구조를 짜는데 도움이 될겁니다. 일반적인 UI 구조에서 특정 기능들을 분리합니다. 한번 사용되는 UI가 아닌 여러번 사용되는 UI에 대한 컴포넌트입니다.
예제에서는 컴포넌트에 중점을 두지만 이전 단계의 범주 폴더도 동일하게 적용할 수 있습니다. 아래 폴더구조를 예로 들면 전체가 표시되지 않지만, 요점을 이해하시기 바랍니다.
src/
--- components/
------ App/
------ List/
------ Input/
------ Button/
------ Checkbox/
------ Radio/
------ Dropdown/
------ Profile/
------ Avatar/
------ MessageItem/
------ MessageList/
------ PaymentForm/
------ PaymentWizard/
------ ErrorMessage/
------ ErrorBoundary/
결국 components/
에 너무 많은 컴포넌트가 있게 됩니다. 일부는 재사용이 가능하지만 다른 것들은 기능과 더 관련있습니다.(ex: Message)
여기서 재사용 가능한 컴포넌트만 components/
에 사용합니다. 모든 다른 컴포넌트는 feature/
에 옮겨 사용합니다. 폴더의 이름은 사용자에게 달렸습니다.
src/
--- feature/
------ User/
--------- Profile/
--------- Avatar/
------ Message/
--------- MessageItem/
--------- MessageList/
------ Payment/
--------- PaymentForm/
--------- PaymentWizard/
------ Error/
--------- ErrorMessage/
--------- ErrorBoundary/
--- components/
------ App/
------ List/
------ Input/
------ Button/
------ Checkbox/
------ Radio/
------ Dropdown/
만약 기능 컴포넌트(ex: MessageItem, PaymentForm)가 Checkbox, Radio 혹은 Dropdown 컴포넌트에 접근해야 한다면, components/
에서 import 합니다. 만약 특정 도메인에서 feature/MessageList
컴포넌트가 components/List
컴포넌트를 필요로 한다면 import 하면 됩니다.
뿐만 아니라, feature
와 services
가 강하게 결합된 경우, services/
폴더를 feature/
내 컴포넌트의 폴더에 위치시킵니다. 이전에 기술적(범주) 폴더 방식으로 분리했던 모든 폴더를 동일하게 적용할 수 있습니다.
src/
--- feature/
------ User/
--------- Profile/
--------- Avatar/
------ Message/
--------- MessageItem/
--------- MessageList/
------ Payment/
--------- PaymentForm/
--------- PaymentWizard/
--------- services/
------------ Currency/
--------------- index.js
--------------- service.js
--------------- test.js
------ Error/
--------- ErrorMessage/
--------- ErrorBoundary/
--------- services/
------------ ErrorTracking/
--------------- index.js
--------------- service.js
--------------- test.js
--- components/
--- hooks/
--- context/
--- services/
------ Format/
--------- Date/
------------ index.js
------------ service.js
------------ test.js
각 기능 폴더에 services/
의 존재 여부는 사용자에게 달려있습니다. ErrorTracking
의 경우 services/
를 생략하고 Error/
폴더에 넣을 수도 있습니다. 그러나 ErrorTracking
은 컴포넌트가 아닌 서비스로 표현되어야하기 때문에 혼란스러울 겁니다.
여기에는 당신의 개인적인 소통을 위한 많은 공간이 있습니다. 결국, 이 단계는 회사의 팀이 프로젝트 전반에 걸쳐 파일을 건드리지 않고 작업할 수 있도록 기능을 통합하는 것입니다.
보너스: 폴더/파일 네이밍 컨벤션
리액트같은 컴포넌트 기반 UI 라이브러리 이전에는, 모든 폴더와 파일 이름을 kebab-case 네이밍 컨벤션 방식을 사용했습니다. Node.js에서 여전히 유효한 네이밍 컨벤션입니다. 그러나 컴포넌트 기반 UI 라이브러리에서는 PascalCase 방식으로 변경 되었습니다. 컴포넌트를 선언할 때 PascalCase 방식을 사용하기 때문입니다.
src/
--- feature/
------ user/
--------- profile/
--------- avatar/
------ message/
--------- message-item/
--------- message-list/
------ payment/
--------- payment-form/
--------- payment-wizard/
------ error/
--------- error-message/
--------- error-boundary/
--- components/
------ app/
------ list/
------ input/
------ button/
------ checkbox/
------ radio/
------ dropdown/
위의 예시처럼 완벽한 환경에서는 모든 폴더와 파일에 _kebab-case_를 사용합니다. _PascalCase_의 이름이 운영체제에 따라 다르게 처리되므로 다른 OS를 사용하는 팀과 버그가 발생할 수 있기 때문입니다.
보너스: Next.js 프로젝트 구조
Next.js의 프로젝트는 pages/
로 시작합니다.
질문: src/
는 어디에 둘까요??
api/
pages/
src/
--- feature/
--- components/
일반적으로 src/
는 pages/
옆에 생성합니다. 그리고 src/
에서 이전의 폴더구조 5단계를 따를 수 있습니다. 저는 src/
에 pages/
를 넣는 Next.js의 방식에 대해서도 들었습니다.
api/
src/
--- pages/
--- feature/
--- components/
그러나 이경우 더 많은 pages/
폴더를 가질 수 없습니다.
마치며...
이 글을 작성하면서, 리액트 프로젝트 구조화를 하는데 도움이 됐기를 바랍니다. 설명한 단계 중 불변하지 않는 것은 없습니다. 오히려 개인적인 방법을 적용해 보는 것이 좋습니다. 모든 리액트 프로젝트는 시간이 지남에 따라 규모가 커지므로, 대부분 폴더 구조도 자연스럽게 발전합니다. 따라서 문제가 해결되지 않는 경우 시도할 수 있는 5단계 프로세스에 대해 소개드렸습니다.
추천 포스트
[Javascript] 신입, 취준생을 위한 현업에서 코드 짜는법(클린코드)
이제 막 취업에 성공하신 분, 대학생 혹은 프로그래머로 취업을 준비중이신 분들은 현업에서 코드를 작성하는 방법에 대해서 궁금해 하시는 분들이 많으실 겁니다. 특히, 코드의 가독성은 신입
aierse.tistory.com
[프로그래밍] 2024년에 새롭게 추가되는 자바스크립트 신규 기능
프로그래밍 언어의 최신 기능을 탐색하는 것은 휴일이나 생일을 간절히 기대하는 것과 비슷합니다. 이는 새로운 선물을 탐구하는 설렘과 스릴로 가득 찬 시간입니다. ES2024®에 포함될 예정인 향
aierse.tistory.com
리액트 프리랜서 체크리스트
출처 Freelance React Developer Checklist 리액트 프리랜서로서, 저는 요즘 리액트 프로젝트에서 많은 고객들과 함께 일합니다. 이메일로 요청을 받을 때마다, 저는 보통 리액트 프리렌서 개발자 체크리
aierse.tistory.com
'웹 > React' 카테고리의 다른 글
리액트 프리랜서 체크리스트 (3) | 2024.01.29 |
---|---|
리액트(React.js)를 배우는 방법 (0) | 2024.01.29 |