searchgithubemail

svg 파일을 rollup 으로 번들링하기

svg 파일을 rollup 으로 번들링해 React 컴포넌트로 사용하기

2025-05-26

개발 진행중 다양한 svg 파일을 컴포넌트로 변환하여 사용해야할 필요가 있다. 매번 svg 파일을 컴포넌트로 변환하는 작업도 번거롭고, 변환 과정에서 실수로 svg 파일을 잘못 수정하는 경우도 있다. 또, 아이콘은 상황에따라 색상, 크기, 회전 등을 변경해야하는 경우가 많다.


이런 문제를 해결하기 위해 rollup 을 사용하여 svg 파일을 React 컴포넌트로 사용할 수 있게끔 패키지하여 사용하였다.

rollup-config.mjs

아이콘들은 /icons/*.svg 경로에 위치했고, svg 파일을 컴포넌트로 변환해 export + 타입을 자동으로 정의되도록 했다.

먼저 아이콘들의 각 경로들을 수집했다.

const iconBasePath = new URL('./icons', import.meta.url).pathname;
const iconFileNames = fs.readdirSync(iconBasePath, 'utf-8');

아이콘의 이름을 search-icon.svg 형태로 저장했고, 이름을 PascalCase 형태로 변환하는 함수를 작성했다.

iconFileNames 는

[
'date-picker.svg',
'info.svg',
'link.svg',
'logout.svg',
'upload-trash.svg',
'upload.svg',
...
]

이런형태로 잘 불러와졌다.

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

iconFileNames 를 순회하며, 각 아이콘의 이름을 PascalCase 형태로 변환하고, import/export 문을 별도 배열에 저장했다.

    iconImportLines.push(`import ${iconModuleName} from '../icons/${iconFileName}'`);
    iconComponentTypes.push(`export declare const ${iconModuleName}: SVGIconType`);

svg -> 컴포넌트 변환

const jsCode = await transform(
  rawSvg,
  {
    plugins: ['@svgr/plugin-jsx'],
    icon: true,
    jsxRuntime: 'classic',
    template: reactIconTemplate,
  }
);

여기서 reactIconTemplate 은 아래와 같이 작성했다.

    function reactIconTemplate({ imports, componentName, props, jsx }, { tpl }) {
        return tpl`
      ${imports}
      function ${componentName}(${props}) {
        return (
          ${jsx}
        );
      }
      export default ${componentName};
    `;
    }

tpl 함수는 템플릿 문자열을 반환하는 함수로, 위의 코드를 통해 svg 파일을 React 컴포넌트로 변환할 수 있다.


타입 선언 자동 생성

빌드시 index.d.ts 파일을 생성해, 각 아이콘 별로 타입을 자동으로 생성되도록 했다.

    const entryTypesContent = `
    declare module '*.svg' {
      const content: string
      export default content
    }

    export declare type IconSource = React.FunctionComponent<React.SVGProps<SVGSVGElement>>
    export declare type SVGIconType = IconSource

    export declare type IconName = ${iconNames.join(' | ')}

    export declare const icons: Record<IconName, SVGIconType>

    ${iconComponentTypes.join('\n')}
    `.trim();


    emitFile({
        fileName: 'index.d.ts',
        source: entryTypesContent,
    }),

emitFile 함수를 만들어 index.d.ts 파일을 생성하고, 위에서 저장해둔 iconComponentTypes를 사용하여 각 아이콘의 타입을 자동으로 정의했다.


defineConfig

export default defineConfig({
    input: 'src/index.ts',
    output: [
        {
            dir: 'dist',
            format: 'cjs',
            entryFileNames: '[name].js',
            chunkFileNames: '[name].js',
            interop: 'auto',
        },
        {
            dir: 'dist',
            format: 'esm',
            entryFileNames: '[name].mjs',
            chunkFileNames: '[name].mjs',
        },
    ],
    external: ['react'],
    plugins: [
        virtual({ 'src/index.ts': entryModuleContent }),
        nodeResolve({ extensions }),
        svgBuild({ include: `${iconBasePath}/*.svg` }),
        babel({
            exclude: 'node_modules/**',
            extensions,
            babelHelpers: 'bundled',
        }),
        emitFile({
            fileName: 'index.d.ts',
            source: entryTypesContent,
        }),
        terser(),
    ],
});

@rollup/plugin-virtual 를 사용해 index.ts 파일을 가상으로 생성했다.

output: [
  {
    dir: 'dist',
    format: 'cjs',
    entryFileNames: '[name].js',
    chunkFileNames: '[name].js',
    interop: 'auto',
  },
  {
    dir: 'dist',
    format: 'esm',
    entryFileNames: '[name].mjs',
    chunkFileNames: '[name].mjs',
  },
],

결과물들을 dist 폴더에 저장되도록 하고, commonjs 와 esm 형식으로 각각 저장되도록 설정했다.

svgBuild({ include: `${iconBasePath}/*.svg` }),

svg 를 컴포넌트로 변환해주는 플러그인이다.

    babel({
        exclude: 'node_modules/**',
        extensions,
        babelHelpers: 'bundled',
    }),

바벨 플러그인을 사용해 js 코드로 변환해준다.

    emitFile({
        fileName: 'index.d.ts',
        source: entryTypesContent,
    }),

커스텀 emitFile 플러그인을 사용해 빌드가 끝난 시점에 index.d.ts 타입 선언 파일을 생성하여 타입을 자동으로 정의한다.

    terser(),

결과물의 크기를 줄이기 위해 terser 플러그인을 사용해 코드를 압축한다.


icons/*.svg에 svg 파일들을 위치시키고 빌드를 실행하면 자동으로 React 컴포넌트가 생성되고 dist/index.js, dist/index.mjs로 각각 리액트 컴포넌트가 생성되고 index.d.ts로 타입 선언까지 자동으로 생성되어 사용할 수 있게 된다.

import { UploadIcon, SearchIcon } from 'your-icon-package';

function App() {
  return (
    <div>
      <UploadIcon width={24} height={24} fill="blue" />
      <SearchIcon width={24} height={24} fill="red" />
    </div>
  );
}