리액트를 처음 접하는 분들께 “폴더/파일을 어떻게 나눌지”를 설명할 때, 용어보다 왜 그렇게 나누는가를 먼저 잡아주면 이해가 빠릅니다. 본 글은 아주 단순한 출발점에서 시작해, 프로젝트가 커질 때 흔들리지 않도록 점진 확장 전략까지 한 번에 정리합니다.
처음에는 src를 작은 작업실이라 생각하고, 그 안에 세 가지 상자만 둡니다.
App : 전체 화면의 뼈대(레이아웃/라우팅의 시작점)components : 재사용 가능한 UI 블록pages : 라우터로 진입하는 “완성된 화면”my-app/
index.html
package.json
src/
main.tsx // 엔트리
App.tsx // 앱의 뼈대
pages/
HomePage.tsx
components/
Greeting.tsx
핵심은 폴더가 역할을 말한다는 감각입니다.
처음에는 스타일도 컴포넌트 파일 옆에 두어 “모양+동작”을 한눈에 보게 하십시오.
왜 이렇게 단순하게 출발할까?
초반에는 “빨리 그려보고 피드백 받는 경험”이 중요합니다. 구조가 복잡하면 속도가 느려집니다.
컴포넌트가 늘고 API 호출이 생기면 서랍(폴더)을 기능별로 하나씩 추가합니다. 이때 기준은 단순합니다.
1) 같은 성격끼리 모은다 (hooks, services, utils, types…)
2) 재사용 범위로 깊이를 나눈다 (components/common vs components/home)
components/home은HomePage.tsx에서만 사용되는 컴포넌트를 모아두는곳
src/
main.tsx
App.tsx
pages/
HomePage.tsx
AboutPage.tsx
components/
common/
Button.tsx
Card.tsx
home/
Hero.tsx
FeatureList.tsx
hooks/
useToggle.ts
useFetch.ts
services/
api/
http.ts // axios/fetch 래퍼
users.ts // /users API
articles.ts // /articles API
styles/
global.css // reset/전역 스타일
assets/
logo.svg
utils/
formatDate.ts
number.ts
types/
article.ts
user.ts
이 단계의 목적은 “파일이 늘어도 찾기 쉬운 위치가 유지되도록” 만드는 것입니다.
규모가 커지면 기능(도메인) 단위로 방을 나누는 편이 유지보수에 유리합니다. 각 방은 그 기능에 필요한 것을 스스로 갖습니다(컴포넌트, 훅, API, 타입 등).
src/
app/
routes.tsx // 라우팅 정의
providers.tsx // 전역 Provider(QueryClient, Theme 등)
store/ // 전역 상태(redux/zustand 등)
shared/
components/ // 진짜 범용: 버튼, 다이얼로그
hooks/
utils/
types/
features/
auth/
components/
LoginForm.tsx
api/
auth.api.ts
hooks/
useLogin.ts
types/
auth.types.ts
index.ts // 외부에 공개할 것만 배럴(export)
articles/
components/
ArticleList.tsx
ArticleItem.tsx
api/
articles.api.ts
hooks/
useArticles.ts
types/
article.types.ts
index.ts
pages/
HomePage.tsx
ArticlesPage.tsx
설명 포인트는 간단합니다.
이전 단계의
components/home처럼 “특정 페이지 전용 컴포넌트”는 커질 때 해당 도메인 방(예:features/articles)으로 점진적 이동하면 됩니다. 전면 개편 없이도 리팩터링이 가능합니다.
① 페이지 vs 컴포넌트
② shared vs features
③ Barrel 파일(index.ts)
features/articles만 임포트해도 필요한 공개 항목을 한 번에 가져올 수 있습니다.④ 경로 별칭(예: @/shared, @/features)
../../../ 지옥을 피하기 위해 tsconfig.json/번들러에 절대경로 별칭을 둡니다. shared/styles로 올려 일관성을 확보하고, 개별 컴포넌트는 여전히 co-location을 유지합니다.간단 예시(초기: 파일 내부에 스타일):
// src/components/common/Button.tsx
import styled from 'styled-components';
const Root = styled.button`
padding: 8px 12px;
border-radius: 8px;
font-weight: 600;
`;
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'ghost';
};
export default function Button({ variant = 'primary', ...rest }: Props) {
return <Root className={`btn ${variant}`} {...rest} />;
}
확장 예시(도메인 방으로 이동 + 공용 토큰 사용):
// src/features/articles/components/ArticleItem.tsx
import styled from 'styled-components';
import { tokens } from '@/shared/styles/tokens';
const Box = styled.div`
padding: ${tokens.spacing.md};
border: 1px solid ${tokens.color.border};
border-radius: ${tokens.radius.lg};
`;
export function ArticleItem({ title, excerpt }: { title: string; excerpt: string }) {
return (
<Box>
<h3>{title}</h3>
<p>{excerpt}</p>
</Box>
);
}
components/에 비슷한 이름이 20개 이상 쌓여 서로 찾기 어려워졌다면 → features로 분리 utils/가 잡동사니 창고가 되었다면 → 도메인별 utils로 쪼개거나 shared/ 내부로 재배치 이 신호를 느낄 때마다 전면 개편 대신 오늘 다루는 코드 범위부터 작게 옮기는 전략이 안전합니다.
이 흐름을 체득하면, 초심자도 “왜 → 언제 → 어떻게”의 축을 잃지 않고 구조를 점진적으로 확장할 수 있습니다.