본문 바로가기

React

React 앱에서 폴더 구조 자동화와 절대 경로(alias) 설정하기

규모가 커지는 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 규칙을 린하게 유지하세요.