규모가 커지는 React 프로젝트에서 폴더 구조를 일관되게 유지하고, 절대 경로(alias)로 import를 간결하게 관리하면 유지보수와 온보딩이 크게 쉬워집니다. 이 글은 실무에서 바로 적용할 수 있는 폴더 구조 자동화 스크립트와, Vite/CRA/Next/Webpack 환경별 절대 경로(alias) 설정을 빠르게 안내합니다.
1. 추천 폴더 구조(예시)
도메인 중심(feature-first)을 기본으로 하되, 공용 계층(lib, hooks, components)을 분리합니다.
// src/ 기준 추천 구조(예시)
src/
app/
providers/
routes/
features/
auth/
cart/
components/
ui/
layout/
hooks/
lib/
services/
store/
styles/
assets/
tests/
App.tsx
main.tsx // Vite 기준
index.tsx // CRA 기준
핵심은 feature 디렉터리로 도메인 경계를 만들고, 공용 유틸/컴포넌트는 lib/components/hooks로 모으는 것입니다.
2. 폴더 구조 자동화 스크립트(Node, 크로스플랫폼)
OS에 상관없이 동작하도록 Node 스크립트로 디렉터리/파일을 한 번에 생성합니다.
// scripts/scaffold.js
const fs = require('fs/promises');
const path = require('path');
const root = path.resolve(process.cwd(), 'src');
const dirs = [
'app/providers',
'app/routes',
'features/auth',
'features/cart',
'components/ui',
'components/layout',
'hooks',
'lib',
'services',
'store',
'styles',
'assets',
'tests'
];
const files = [
{ file: 'hooks/useToggle.ts', content: "import { useState } from 'react';\nexport function useToggle(initial = false){const [v,setV]=useState(initial);return [v,()=>setV(x=>!x)] as const;}\n" },
{ file: 'lib/index.ts', content: '' },
{ file: 'store/index.ts', content: "export * from './store';\n" },
{ file: 'features/auth/index.ts', content: '' },
{ file: 'features/cart/index.ts', content: '' },
{ file: 'components/ui/Button.tsx', content: "import React from 'react';\nexport const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>>=({children,...props})=>(<button {...props}>{children}</button>);\n" }
];
async function ensureDir(p){ await fs.mkdir(p, { recursive: true }); }
async function ensureFile(p, content){ try { await fs.access(p); } catch { await fs.mkdir(path.dirname(p), { recursive: true }); await fs.writeFile(p, content); } }
(async () => {
await ensureDir(root);
await Promise.all(dirs.map(d => ensureDir(path.join(root, d))));
await Promise.all(files.map(f => ensureFile(path.join(root, f.file), f.content)));
console.log('Scaffold complete');
})();
package.json에 명령어를 추가합니다.
// package.json
{
"scripts": {
"scaffold": "node scripts/scaffold.js"
}
}
실행: npm run scaffold 또는 pnpm run scaffold
3. 절대 경로(alias) 기본 설정(js/ts 공통)
가장 단순하고 안전한 방법은 jsconfig.json 또는 tsconfig.json에 baseUrl과 paths를 선언하는 것입니다. baseUrl을 src로 두면 src 기준 절대 경로가 활성화됩니다.
// jsconfig.json (JS 프로젝트)
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"],
"@features/*": ["features/*"]
}
},
"exclude": ["node_modules", "dist", "build"]
}
// tsconfig.json (TS 프로젝트)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"],
"@features/*": ["features/*"]
},
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"]
}
이후 import는 다음처럼 간결해집니다.
import { Button } from '@components/ui/Button';
import { useToggle } from '@hooks/useToggle'; // paths에 @hooks/* 를 추가했다면 가능
4. Vite에서 alias 설정
Vite는 두 가지 접근이 있습니다. 1) vite-tsconfig-paths 플러그인으로 ts/jsconfig paths를 그대로 사용 2) vite.config에서 직접 alias 선언.
// 1) 플러그인 사용(권장)
// 설치: npm i -D vite-tsconfig-paths
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()]
});
// 2) 직접 alias 선언
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
}
}
});
5. CRA(Create React App)에서 alias 설정
CRA는 webpack 설정 노출이 제한됩니다. 가장 간단한 방법은 jsconfig/tsconfig에서 baseUrl을 src로 설정하여 절대 경로를 사용하는 것입니다. 커스텀 별칭(@components 등)을 추가하려면 craco/react-app-rewired 같은 툴이 필요합니다.
// 1) 기본: jsconfig.json에 baseUrl만 설정
{
"compilerOptions": { "baseUrl": "src" }
}
// 사용: import Button from 'components/ui/Button';
// 2) 커스텀 별칭 필요 시(craco 예시)
// 설치: npm i -D @craco/craco
// craco.config.js
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
}
}
};
// package.json scripts: "start": "craco start", "build": "craco build", "test": "craco test"
주의: CRA 내 Jest는 baseUrl=src로는 동작하지만, 커스텀 별칭은 jest moduleNameMapper를 추가해야 할 수 있습니다(아래 8장 참고).
6. Next.js에서 alias 설정
Next.js는 jsconfig/tsconfig 경로 매핑을 바로 인식합니다. next.config 수정 없이 js/tsconfig만으로 충분합니다.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"]
}
}
}
프로젝트 루트에 src 디렉터리를 쓰지 않는다면 baseUrl을 "."로 조정하고 paths를 실제 경로로 맞추세요.
7. Webpack 커스텀 프로젝트에서 alias 설정
// webpack.config.js
const path = require('path');
module.exports = {
// ...
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
}
}
};
8. Babel/Jest/ESLint와 alias 정합성 맞추기
툴마다 해석기가 달라 동기화가 필요합니다.
// Babel(module-resolver) - 필요할 때만
// 설치: npm i -D babel-plugin-module-resolver
// .babelrc or babel config
{
"plugins": [
[
"module-resolver",
{
"root": ["./src"],
"alias": {
"@": "./src",
"@components": "./src/components"
}
}
]
]
}
// Jest - 경로 매핑
// jest.config.js or package.json "jest"
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1'
}
};
// ESLint(import resolver)
// 설치: npm i -D eslint-import-resolver-typescript
// .eslintrc.js
module.exports = {
settings: {
'import/resolver': {
typescript: {},
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] }
}
}
};
9. Storybook에서 alias 동기화
Storybook을 사용한다면 빌더에 맞춰 alias를 적용합니다.
// .storybook/main.ts (Vite 빌더)
import tsconfigPaths from 'vite-tsconfig-paths';
const config = {
viteFinal: async (config) => {
config.plugins = [...(config.plugins || []), tsconfigPaths()];
return config;
}
};
export default config;
// .storybook/main.js (Webpack 빌더)
const path = require('path');
module.exports = {
webpackFinal: async (config) => {
config.resolve.alias = {
...(config.resolve.alias || {}),
'@': path.resolve(__dirname, '../src')
};
return config;
}
};
10. VS Code 자동완성과 타입 서버 갱신
jsconfig/tsconfig를 수정한 뒤 VS Code의 TypeScript 서버를 재시작해야 자동완성이 반영됩니다. 명령 팔레트에서 “TypeScript: Restart TS server”를 실행하세요. Path Intellisense 확장(Christian Kohler) 사용 시 alias 자동완성에 도움됩니다.
11. 빠른 체크리스트
- 폴더 자동화: scripts/scaffold.js 작성 후 npm run scaffold로 초기 레이아웃 생성합니다.
- js/tsconfig: baseUrl과 paths를 src 기준으로 설정합니다.
- 번들러: Vite는 vite-tsconfig-paths 또는 resolve.alias, CRA는 baseUrl 우선, Webpack/Next는 설정에 맞게 alias를 적용합니다.
- 툴 연동: Jest moduleNameMapper, ESLint import resolver, Storybook builder에 동일 별칭을 반영합니다.
- 에디터: TS 서버 재시작, Path Intellisense 확장 확인합니다.
12. 트러블슈팅
- VS Code는 경로를 인식하는데 빌드가 실패한다면: 번들러(Vite/Webpack) alias 설정이 누락된 것입니다.
- 테스트에서만 경로 에러가 난다면: Jest moduleNameMapper를 확인하세요.
- CRA에서 @components가 동작하지 않는다면: craco 등으로 webpack alias를 추가하거나 baseUrl=src로 절대 경로(components/...)만 사용하세요.
- TS가 paths를 무시하면: tsconfig 경로가 프로젝트 루트에 있는지, include에 src가 포함되는지 확인하고, 하위 tsconfig가 중첩되어 덮어쓰지 않는지 점검하세요.
위 설정을 한 번 정리해두면 신규 페이지/피처 추가가 빨라지고, PR 리뷰에서 경로/구조 논쟁을 줄일 수 있습니다. 팀 합의된 스캐폴딩과 alias 규칙을 린하게 유지하세요.
'React' 카테고리의 다른 글
| React 앱에서 SEO 메타 태그 동적 관리하기 (0) | 2026.07.28 |
|---|---|
| React에서 날짜 및 시간대(Timezone) 처리 최적화하기 (0) | 2026.07.28 |
| React에서 실시간 협업 에디터 구현하기 (0) | 2026.07.27 |
| React 앱에서 브라우저 Wake Lock API 활용하기 (0) | 2026.07.26 |
| React에서 OpenAPI 기반 API 클라이언트 자동 생성하기 (1) | 2026.07.26 |