본문 바로가기
개발개발/Date-project

배럴 파일 자동 생성 스크립트

by yelimu 2025. 4. 9.

패키지 진입점 index.ts 생성하기 위해 배럴 파일 생성 자동화

아래 블로그에서 공유해준 코드로 배럴 파일을 생성하였다.

https://lasbe.tistory.com/187

 

  "scripts": {
	...
	"barrel": "node generateBarrel.js"
  },

안내해주신대로 처리 대상 디렉토리와 예외 파일 목록을 작성했고, script에 추가하여 편리하게 배럴파일을 생성할 수 있었다.

만약 내용을 수정하고 스크립트를 재호출하면 이전의 결과물을 덮어씌우게 된다. 

 

그런데 나는 export default 를 사용해서 컴포넌트를 export 해주었고

+ 컴포넌트 파일명의 경우 파스칼 컨벤션을 사용하므로 아래와 같이 코드를 일부 수정했다. 

 

export * from './components' 이런 식의 코드는 default export 된 컴포넌트를 가져오지 않기 때문이다

function toPascalCase(str) {
  return str
    .replace(/[-_](.)/g, (_, g1) => g1.toUpperCase())
    .replace(/^(.)/, (_, g1) => g1.toUpperCase());
}
//...
targetFolderList.forEach((folder) => {
//...

const exportName = toPascalCase(fileNameWithoutExtension);
//...
      exportStatementList.push(
        `export {${exportName}} from './${fileNameWithoutExtension}';`
      );

 

생성된 배럴파일

export { Button } from "./button";
export { CheckBox } from "./checkBox";
export { FileUpload } from "./fileUpload";
export { Radio } from "./radio";
export { Select } from "./select";
export { Slider } from "./slider";
export { Switch } from "./switch";
export { Tag } from "./tag";
export { Textarea, TextInput } from "./textfield"; // 이건 수동으로 적어줬음..

  • named export vs default export 

Default export

const Component = () => {...}

export default Component

 

import할때 아무 이름으로 import가 가능하다 

변수는 export할 수 없다

 

나는 컴포넌트에 default export 를 사용하고있다. 


Named export 

export function Component1(){...}

export function Component2(){...}

한 파일 내에서 여러 변수, 클래스 등을 export 할 수 있다

나는 유틸함수나 훅에서 named export를 사용했다.

 

import할때 동일한 이름으로만 설정 가능하다

import { Component1, Component2 } from './MyComponents'

 

또는 as 를 사용하여 다른 이름으로 import 

* as 를 사용하여 한 파일에 있는 클래스/변수를 한번에 import 할 수 있다 

import * as Hello from './MyComponents'

// Hello.Component1 로 호출

위에서 언급했듯, 이 방법은 default export된 컴포넌트를 가져올 수 없다 

 

https://medium.com/@_diana_lee/default-export%EC%99%80-named-export-%EC%B0%A8%EC%9D%B4%EC%A0%90-38fa5d7f57d4


근데 이렇게 했더니 빌드할때 에러가 발생한다 ㅠㅠ ㅠ 

src/components/button/index.ts:1:9 - error TS2614: 
Module '"./Button"' has no exported member 'Button'. 
Did you mean to use 'import Button from "./Button"' instead?

1 export {Button} from './Button';

 

이유는  

components/하위 폴더 마다 생성할 배럴파일과, components/index.ts 전체 배럴 파일의 형식이 달라야 하기 때문이다 

 

개별 컴포넌트가 존재하는 폴더에서 index.ts 를 만들때 => default export 컴포넌트들에 대한 배럴파일 생성

export {default as MyComponent} from './MyComponent'

 

전체 배럴파일 생성 시에는 named export 형식으로 작성

export * from './button';
export * from './checkBox';
export * from './fileUpload';
export * from './radio';
export * from './select';
export * from './slider';
export * from './switch';
export * from './tag';
export * from './textfield';

 

이 부분에 해당하는 내용이

    const isDir = fs.statSync(filePath).isDirectory();
    const isTargetFile =
      fs.statSync(filePath).isFile() &&
      (file.endsWith(`.${extension}`) || file.endsWith(`.${extension}x`)) &&
      file !== `index.${extension}`;

    if (isTargetFile) {
      exportStatementList.push(
        `export { default as ${exportName} } from './${fileNameWithoutExtension}';`
      );
    } else if (isDir) {
      const nestedIndexPath = path.join(filePath, `index.${extension}`);
      if (fs.existsSync(nestedIndexPath)) {
        // 디렉토리에 index.ts(x)가 있는 경우 → export * from './folder'
        exportStatementList.push(
          `export * from './${fileNameWithoutExtension}';`
        );
      } else {
        // index.ts(x) 없는 경우 → default export 라고 가정
        exportStatementList.push(
          `export { default as ${exportName} } from './${fileNameWithoutExtension}';`
        );
      }
      generateIndexFile(filePath); // 하위 디렉토리도 재귀 처리
    }
  });

 

친절하게 주석 달아주신 대로.. 

내부에 index.ts가 있으므로 named export  (isDir : true)

내부에 index.ts가 없는 경우 default export 

 

요렇게 작성하면 되시겠다~~

빌드 성공

 

▼ 수정된 코드 전문

더보기
const fs = require("fs");
const path = require("path");

function toPascalCase(str) {
  return str
    .replace(/[-_](.)/g, (_, g1) => g1.toUpperCase())
    .replace(/^(.)/, (_, g1) => g1.toUpperCase());
}

// 소스 코드 디렉토리 경로 설정
const srcFolderPath = path.join(__dirname, "src");
let count = 0;

// (직접 수정) index.js = true, index.ts = false
const isJavaScript = false;
const extension = isJavaScript ? "js" : "ts";

// (직접 수정) 처리 대상 디렉토리 목록
const targetFolderList = ["components"];

// (직접 수정) 예외 파일 목록
const exceptionFileList = [
  "style.css.ts",
  "type.ts",
  "stories",
  "ImageUploader",
  "ImageInput",
  "ImagePreview",
  "ButtonSample",
  "sample",
  "OptionList",
];

targetFolderList.forEach((folder) => {
  const folderPath = path.join(srcFolderPath, folder);
  if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
    generateIndexFile(folderPath);
  }
});

function generateIndexFile(directoryPath) {
  const indexFilePath = path.join(directoryPath, `index.${extension}`);
  const relativePath = path.relative(__dirname, indexFilePath);

  const files = fs.readdirSync(directoryPath);
  const exportStatementList = [];

  files.forEach((file) => {
    const filePath = path.join(directoryPath, file);
    const fileNameWithoutExtension = path.basename(file, path.extname(file));
    const exportName = toPascalCase(fileNameWithoutExtension);

    const isException = exceptionFileList.some((exceptionFileName) =>
      file.toLowerCase().includes(exceptionFileName.toLowerCase())
    );
    if (isException) return;

    const isDir = fs.statSync(filePath).isDirectory();
    const isTargetFile =
      fs.statSync(filePath).isFile() &&
      (file.endsWith(`.${extension}`) || file.endsWith(`.${extension}x`)) &&
      file !== `index.${extension}`;

    if (isTargetFile) {
      exportStatementList.push(
        `export { default as ${exportName} } from './${fileNameWithoutExtension}';`
      );
    } else if (isDir) {
      const nestedIndexPath = path.join(filePath, `index.${extension}`);
      if (fs.existsSync(nestedIndexPath)) {
        // 디렉토리에 index.ts(x)가 있는 경우 → export * from './folder'
        exportStatementList.push(
          `export * from './${fileNameWithoutExtension}';`
        );
      } else {
        // index.ts(x) 없는 경우 → default export 라고 가정
        exportStatementList.push(
          `export { default as ${exportName} } from './${fileNameWithoutExtension}';`
        );
      }
      generateIndexFile(filePath); // 하위 디렉토리도 재귀 처리
    }
  });

  const content = exportStatementList.join("\n") + "\n";
  fs.writeFileSync(indexFilePath, content);
  console.log(`${++count} \t ${relativePath}`);
}