본문 바로가기

javascript/nodejs

[nodejs] esbuild 전체 폴더 감시하기

https://esbuild.github.io/

 

esbuild - An extremely fast bundler for the web

esbuild An extremely fast bundler for the web Above: the time to do a production bundle of 10 copies of the three.js library from scratch using default settings, including minification and source maps. More info here. Our current build tools for the web ar

esbuild.github.io

 esbuild는 많은 번들링 툴(여러 코드 베이스를 하나로 뭉치기) 중 하나로, 공식 홈페이지에 따르면 다른 번들링 툴에 비해 월등한 성능을 보이고 있다고 한다. 개인적으로 번들러를 그렇게까지 많이 사용해보지는 않았지만 공식 문서 및 설정이 상당히 깔끔하다고 느껴서 express + ejs 환경에서 사용해보고 있다.

 번들링을 하는 경우 진입점(엔트리포인트)이 되는 파일만 지정해두면, 해당 파일을 기준으로 연결된 종속성을 고려하여 하나의 파일로 변환해준다. 이때 나는 전체 폴더의 계층 구조는 유지하면서, 각각의 타입스크립트 파일을 자바스크립트 파일로 번들링하고 싶다는 생각이 들었다. 사실 이렇게 사용할 일이 거의 없기는 하지만, 클래스처럼 의도적으로 구분해둔 코드는 그대로 구분하여 사용하고 싶다는 마음이 들 수도 있으며, 나도 이런 발상에서 시작했다.


esbuild의 진입점

 esbuild는 전체 폴더의 파일을 감시하기 위한 기능을 가지고 있지는 않다. 감시할 파일은 entrypoints 배열에 지정되어야 하므로, 특정 폴더 내 모든 타입스크립트 파일을 감시하고 싶다면 해당 모든 파일의 이름을 entrypoints에 제공해야 한다.

파일 이름 얻기

 파일의 이름은 fs 모듈의 readdir 메서드를 이용하여 얻을 수 있다. 이때 읽은 파일이 정확히 타입스크립트 파일인지 판단하기 위해서는 정규식을 통한 필터링이 필요하다. 여기서 사용된 정규식은 다음과 같다.

/^(?!.*\.d\.ts$)(.+\.ts|[^.]+)$/

 현재 정규식에서는 다음과 같은 규칙이 적용된다.

  • .d.ts 로 끝나는 파일을 제외한다. (타입 관련 파일은 번들링할 필요 없음)
  • ~.ts 파일 또는 확장자가 없는 파일을 선택한다. 현재 확장자가 없는 경우 폴더로 간주하고 있다.

위 정규식은 chatgpt의 도움을 조금 받았다. 앞의 ?!이 포함되는 부분은 Negative Lookahead assertion이라고 하며, 특정 패턴을 제외할 때 사용된다고 한다. 자세한 설명은 아래 경로를 참고하라.

https://www.regular-expressions.info/lookaround.html

 

Regex Tutorial - Lookahead and Lookbehind Zero-Length Assertions

Lookahead and Lookbehind Zero-Length Assertions Lookahead and lookbehind, collectively called “lookaround”, are zero-length assertions just like the start and end of line, and start and end of word anchors explained earlier in this tutorial. The differ

www.regular-expressions.info

 정규식을 통해 획득한 타입스크립트 파일 및 폴더는 stat 메서드를 통해 대상이 파일 또는 폴더인지 검사된다. 만약 파일이라면 엔트리 포인트에 넣으면 되지만, 아니라면 폴더인 상황이므로 해당 폴더에 대해 다시 파일을 검사할 필요가 있다.


 결과적으로 작성한 코드는 다음과 같다.

import { readdir, stat } from 'fs/promises';
import * as esbuild from 'esbuild';
import { join } from 'path';
import { argv } from 'process';

const source = 'pub-develop/js';
const outdir = 'public/js';
// const fileNames = [];

async function getFiles(path, deep = true) {
    let fileNames = [];
    const targets = (await readdir(path)).filter(it => it.match(/^(?!.*\.d\.ts$)(.+\.ts|[^.]+)$/));
    // console.log(targets);
    // thanks for chatgpt! -> exclude .*.d.ts and include .*.d.ts
    // ^: Matches the start of the string.
    // (?!.*\.d\.ts$): Negative lookahead assertion. It ensures that the string does not end with .d.ts.
    // [^.]+\.ts: Matches one or more characters that are not a dot, followed by .ts (for files).
    // |: Alternation operator.
    // [^.]+: Matches one or more characters that are not a dot
    // $: Matches the end of the string.
    for (const target of targets) {
        const targetName = join(path, target);
        // console.log(targetName);
        const info = await stat(targetName);
        if (info.isDirectory() && deep) // 폴더 + 깊게 볼거면
        {
            fileNames = fileNames.concat(await getFiles(targetName)); // 깊게 한번 더 보자.
        }
        else
            fileNames.push(targetName); // 아님 조건 맞는 파일만 추가.
    }
    return fileNames;
}


async function bundleFiles() {
    let watch = false;
    if (argv.length > 2 && argv[2] === '--watch') {
        watch = true;
    }
    try {
        const fileNames = await getFiles(source);
       
        const config = {
            entryPoints: fileNames,
            platform: 'browser',
            bundle: true,
            minify: true,
            outdir: outdir,
            format: 'esm'
        }; // esbuild config
       
        if (watch) {
            const ctx = await esbuild.context(config);
            await ctx.watch();
        }
        else {
            await esbuild.build(config);
        }
    } catch (err) {
        console.log("error occured");
        console.error(err);
    }
}
// "esbuild pub-develop/js/main.ts --bundle --minify --platform=browser --format=esm --outdir=public/js --watch"

await bundleFiles();

 getFiles 메서드는 폴더를 재귀적으로 순회하며 엔트리 포인트 목록을 지정한다. 정규식을 통과한 것이 파일이라면 fileNames에 넣고, 아니라면 해당 폴더에 대해 getFiles을 다시 적용한다. 이런 과정을 통해 지정된 하위 디렉토리 전체를 순회하며 파일 목록을 얻는다.

 이때 readdir로 얻은 이름에는 전체 경로가 명시되지 않는다. 따라서 파일의 실제 경로를 지정하기 위해서는 파일의 위치 정보를 파일 이름과 합쳐야 한다. 이를 위해 join을 통해 실제 파일 경로인 targetName을 사용하고 있다. 재귀 호출할 때도 getFiles에 targetName을 넘겨 일관된 방식으로 파일 경로를 구할 수 있게 만든다.

 획득한 fileNames는 esbuild의 config에 포함되어 esbuild.context 또는 esbuild.build 메서드 호출에 파라미터로 제공된다. 이때 각 옵션을 설명하면 다음과 같다.

  • entryPoints: 번들링을 위한 진입점
  • platform: 현재 번들이 타겟으로 하는 환경
  • bundle: 번들링 할지 여부
  • minify: 코드를 작게 축소한다
  • outDir: 번들링된 파일을 출력할 위치
  • format: 번들링에 적용할 포맷

 이 외에도 많은 옵션들이 존재하므로, 필요한 경우 공식 문서를 참고하자.

 아무튼 위와 같은 설정을 기반으로 esbuild.context 또는 esbuild.build을 수행한다. context의 경우 감시나 재시작 같은 동작이 필요할 때 사용되며, build는 단순히 파일을 빌드할 때 사용된다. 둘을 구분하기 위해 process 모듈의 argv를 이용하여 --watch 옵션이 추가적으로 제공되었는지 확인한다.

 위 코드의 실행 결과는 다음과 같다.

번들링 전 코드(타입스크립트)
번들링 후 코드. 폴더 구조를 유지한다.

 폴더 및 파일의 계층 구조를 유지하면서 정상적으로 코드가 컴파일된 모습을 볼 수 있다.


 이번 설정의 흐름을 요약하면 아래와 같다.

  1. 재귀적으로 폴더 내 모든 파일을 순회, 엔트리 포인트로 지정할 파일 목록을 생성한다.
  2. 정규표현식을 이용하여 타입스크립트 파일 및 폴더만 필터링한다.
  3. 필터링 된 결과 이름을 실제 경로에 대응한다.
  4. 타겟의 실제 경로를 기반으로 파일인지 여부를 검사한다.
  5. 파일이라면 엔트리 포인트에 포함하고, 폴더라면 재귀적으로 엔트리포인트를 찾는다.
  6. 얻은 엔트리 포인트를 기반으로 빌드 / 감시를 진행한다.

 개인적으로 좋은 경험이었던 것 같다. vite 정도는 react 설정을 위해 사용해봤지만 본격적인 번들링 툴은 이용해본 적이 없었는데, 이렇게 사용해보니 번들링 툴이 정확히 어떤 일을 하는지, 어떻게 설정을 해야 하는지 대략적인 감이 잡혔다.

 현재 코드의 원문은 아래 주소를 참고하자.

https://github.com/blaxsior/socket-example/blob/socio-based/esbuild.js

 

GitHub - blaxsior/socket-example: 웹 소켓을 이용하여 만든 예제 (니콜라스 줌 코딩 참고)

웹 소켓을 이용하여 만든 예제 (니콜라스 줌 코딩 참고). Contribute to blaxsior/socket-example development by creating an account on GitHub.

github.com

 

'javascript > nodejs' 카테고리의 다른 글

[nodejs] thread pool  (0) 2023.08.16
[nodejs] libuv  (0) 2023.08.16
[nodejs] express-session typescript와 사용하기  (0) 2023.03.21
[nodejs] prisma ORM 라이브러리  (0) 2023.02.22
[nodejs] mysql2 라이브러리  (0) 2023.02.14