Ayden's journal

sicilian으로 알아보는 패키지 실행 기능

본 포스트의 모든 코드와 예제는 GitHub의 @ilokesto/sicilian 레포지토리에서 확인 가능합니다.

 

좋은 라이브러리를 만들었다 한들 쓰는 게 귀찮으면 아무짝에도 쓸모가 없다. 가령 sicilian이 그렇다. 다른 라이브러리들도 아마 비슷한 면이 있겠지만, sicilian의 경우 특히 form을 구성하는 요소들에 대한 옵트인 옵션이나, 리턴되는 객체에 포함된 메서드와 값을 일일이 선언해야 하는 번거로움이 있었다.

문제는 이런 반복적인 선언 과정이 단순히 개발자의 귀찮음을 넘어, 실제로는 생산성을 저해하고 진입 장벽을 높이는 요인이라는 점이다. 즉, 라이브러리의 기능이 아무리 훌륭해도 "시작하기 힘들다"라는 인상이 남는 순간, 대부분의 사용자는 대안을 찾아 떠나게 된다. 나 스스로도 매번 boilerplate 코드를 복붙하고 수정하는 과정에서 점점 불편함을 크게 체감하게 되었다.

 

그때 떠오른 것은 Nest.js였다. npx nest -g처럼 단 한 줄로 모듈이나 컨트롤러 클래스를 자동 생성해주다 보니, 백엔드를 구성할 때 반복적인 설정이나 보일러플레이트 작성에 시간을 낭비할 필요 없이 바로 핵심 기능에 집중할 수 있다는 점이 인상적이었다. 이러한 작은 자동화는 단순한 편의 기능을 넘어, 라이브러리를 보다 적극적으로 활용하도록 만드는 중요한 진입 장치가 된다. Sicilian에도 이러한 기능이 제공된다면, 사용자는 반복적인 코드 작성에서 벗어나 라이브러리의 본래 목적에 곧바로 몰입할 수 있을 거라 생각했다.

나는 -g <path>와 같은 명령어를 받으면, 지정된 경로에 자동으로 파일을 생성하고 필요한 코드 스니펫을 작성하도록 CLI를 설계하고자 했다. 또한, 사용자가 CLI에서 제공하는 명령어와 옵션을 쉽게 확인할 수 있도록 -help 플래그도 제공하면 좋겠다고 생각했다. 실제 사용 예시는 다음과 같다.

npx sicilian -g src/shared/sicilian/signIn.tsx
npx sicilian -help

 

 

bin 필드 설정

패키지를 실행하기 위해서는 package.json의 bin 필드를 설정해주어야 한다. 이 설정을 통해 npm은 패키지를 설치하거나 npx로 실행할 때, sicilian이라는 명령어가 dist/cli/index.js를 가리키도록 연결한다. 즉, 사용자는 별도로 글로벌 설치를 하지 않아도 다음과 같이 CLI를 실행할 수 있다.

"bin": {
  "sicilian": "dist/cli/index.js"
},

bin 필드를 정의할 때 중요한 점은 크게 두 가지다. 첫째, 실행할 파일에 해시뱅(#!/usr/bin/env node)이 포함되어 있어야 한다. 그래야 Unix 계열 OS에서 바로 실행 가능한 스크립트로 인식된다. 둘째, 패키지를 배포할 때 해당 파일이 빌드 산출물(dist)에 포함되어야 한다.

 

index.ts

index.ts를 실행하면 main 함수가 호출되어 CLI가 동작을 시작한다. 사용자가 터미널에 입력한 명령어는 Node.js에서 제공하는 process.argv라는 배열을 통해 조회할 수 있다. 예를 들어, 다음과 같이 명령을 실행했다고 가정해보자.

npx sicilian -g src/shared/sicilian/signIn.tsx

이때 process.argv 배열에는 Node.js 실행 경로, 실행 중인 스크립트 경로, 그리고 사용자가 입력한 플래그와 인자가 순서대로 담기게 된다. Sicilian CLI에서는 process.argv.slice(2)를 사용하여 앞의 두 항목을 제외하고 실제 사용자 입력만을 추출한다. 결과적으로 main 함수가 처리하는 배열은 다음과 같다.

['-g', 'src/shared/sicilian/signIn.tsx']

이 배열을 기반으로 Sicilian CLI의 main 함수는 사용자가 입력한 명령어와 플래그를 확인하고, 그에 따라 적절한 기능을 호출한다. 만약 사용자가 -g 플래그를 입력하면 조건문이 이를 감지하고, 플래그 바로 다음 인자를 파일 경로로 간주하여 handleGenerate 함수를 호출한다. 이 함수는 지정된 경로에 코드 스니펫을 생성하거나 기존 파일에 추가하는 역할을 수행한다.

const generateFlagIndex = args.indexOf('-g');
if (generateFlagIndex !== -1) {
  const filePathArg = args[generateFlagIndex + 1];
  handleGenerate(filePathArg, args);
  return;
}

즉, 조건문 내에서 각각의 함수를 호출함으로써, main 함수는 CLI 실행 흐름을 제어하고, 사용자가 요청한 기능을 정확하게 수행하도록 한다.

 

handleGenerate 함수

handleGenerate 함수는 -g 플래그와 함께 전달된 경로를 기반으로 파일을 생성하거나 기존 파일에 코드 스니펫을 추가하는 핵심 기능을 수행한다. 이 함수를 통해 반복적인 boilerplate 코드를 작성할 필요 없이, 사용자는 CLI 한 번으로 원하는 코드 구조를 만들 수 있다.

함수는 먼저 파일 경로가 전달되었는지 확인한다. 만약 파일 경로가 없으면, 오류 메시지를 출력하고 showHelp를 호출하여 사용자가 올바른 명령어를 확인할 수 있도록 안내한다.

if (!filePathArg) {
  console.error('Error: File path is required after the -g flag.');
  showHelp();
  return;
}

 

다음으로 함수는 지정된 경로의 디렉터리가 존재하지 않으면 fs.mkdirSync를 사용해 재귀적으로 생성한다. 이후 파일이 이미 존재하면 스니펫을 덧붙이고(append), 존재하지 않으면 새 파일을 생성하고 스니펫을 작성한다. 기본적으로는 destructured form 스니펫을 사용하지만, -o 또는 --object 플래그가 포함되어 있다면 객체 형태의 스니펫을 사용하도록 설정한다.

  let snippetToUse = destructuredSnippet; // 기본값
  if (args.includes('-o') || args.includes('--object')) {
    snippetToUse = objectSnippet;
  }

  const targetPath = path.resolve(process.cwd(), filePathArg);
  const targetDir = path.dirname(targetPath);

  try {
    if (!fs.existsSync(targetDir)) {
      fs.mkdirSync(targetDir, { recursive: true });
    }

    if (fs.existsSync(targetPath)) {
      fs.appendFileSync(targetPath, `${snippetToUse.trim()}`);
      console.log(`Success: Code snippet appended to ${targetPath}`);
    } else {
      fs.writeFileSync(targetPath, `${snippetToUse.trim()}`);
      console.log(`Success: Created file and generated code at ${targetPath}`);
    }

 

결론

Sicilian CLI를 구현하며 알게 된 것은, 패키지 실행 기능을 추가하는 과정이 생각보다 복잡하지 않다는 점이다. 한 번 방법을 이해하면, bin 필드 설정부터 커맨드라인 인자 처리, 파일 생성 및 코드 스니펫 작성까지의 흐름을 차근차근 구현할 수 있다. 작은 자동화 기능 하나가 반복적인 작업을 줄이고, 라이브러리를 더 적극적으로 활용하도록 만드는 중요한 역할을 할 수 있음을 이번 경험을 통해 확인할 수 있었다.

 

아래는 실제로 Sicilian CLI를 사용해 미리 정의된 코드 스니펫을 정해진 path에 생성하는 모습이다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기