스크립트 툴의 장점만 모았다! zx로 업무 자동화하기

김준영

안녕하세요. 데브시스터즈에서 프론트엔드 엔지니어로 일하고있는 김준영입니다. 이번 글에서는 자바스크립트에 익숙한 개발자가 자동화 스크립트를 편하게 작성할 수 있게 해주는 zx 라이브러리를 소개하려고 합니다.

들어가기에 앞서

저는 업무 자동화 작업을 참 좋아합니다. 아래와 같은 여러 장점이 있기 때문입니다.

  • 지루한 반복 업무를 더 이상 하지 않아도 된다는 편안함
  • 개발자 다운 방식으로 문제를 해결했다는 뿌듯함
  • 더 중요한 문제에 집중할 수 있음
  • 오타 등 단순 실수에 신경쓰지 않아도 됨

스크립트를 통해 어떤 작업을 자동화 할 수 있고, zx 는 다른 선택지와 비교하여 어떤 장점이 있는지 살펴보겠습니다.

이 글은 아래와 같은 분들을 대상으로 작성하였습니다. 🙍‍♀️🙎‍♂️

- 반복 업무를 자동화하고 핵심 로직 개선에 집중하고 싶은 분
- 스크립트 작성은 처음이지만 새롭게 도입하고 싶은 분
- 쉘 스크립트 사용 경험이 있으나 더 편한 도구를 찾고 있는 분

쉘 & 쉘 스크립트

쉘(shell)은 사용자가 OS 의 기능을 사용할 수 있도록 하는 사용자 인터페이스입니다. 사용자가 작성한 명령어를 커널이 이해할 수 있는 형태로 변환해 줍니다. 대표적인 쉘로는 bash, csh, zsh 등이 있습니다.

쉘 스크립트(shell script)는 쉘의 명령어들을 이용해 작성하는 프로그래밍 스크립트 언어입니다. 이를 통해 반복적인 작업을 자동화하거나 복잡한 작업을 단순화할 수 있습니다. 그러나 다소 복잡하고 생소한 문법을 가지고 있다는 단점도 있습니다. 아래 내용에서 zx 는 이러한 단점을 어떻게 보완하였는지 소개하려고 합니다.

linux shell

Linux Shell

zx란?

zx는 자바스크립트를 기반으로 쉘 명령어를 동시에 사용할 수 있는 라이브러리입니다. (ZX 공식 페이지) 그렇기에 자바스크립트와 쉘 스크립트가 각각 가지고 있는 장점을 동시에 누릴 수 있는 유용한 도구 입니다. bash 또는 node.js 를 단독으로 사용하여 스크립트를 작성하는 상황과 비교해서 zx 의 장점을 보여드리도록 하겠습니다.

logos

장점 1) JSON 조작의 편의성

JSON은 개발 영역 전반에서 쓰이고 있는 데이터 포맷입니다. 그러나 bash 에서는 JSON 데이터를 다루기에는 불편한 부분이 있습니다. 예시를 통해 zx 를 사용할 때와 비교해 보겠습니다.

bash 에서는 다양한 자료구조를 지원하고 있지 않아 JSON 데이터도 문자열의 형태로 관리하게 됩니다.

DATA = '{
  "users": [
    {
      "id": 1,
      "name": "John",
      "contacts": {
        "email": "john@example.com",
        "phone": "123-456-7890"
      }
    }
  ]
}'

DATA에서 이름이 John인 객체를 찾아 email을 출력하기 위해서는 아래와 같은 코드가 필요합니다.

JOHN_EMAIL = $(echo "$DATA" | jq -r '.users[] | select(.name == "John") | .contacts.email')
echo 'John's Email: $JOHN_EMAIL'

예시처럼, 쉘 스크립트를 통해 JSON 데이터를 다룰 때에는 몇 가지 단점이 있습니다.

  • 데이터 구조가 다양하지 않아 jq 라는 별도의 도구 필요
  • 도구를 사용해도 여전히 복잡한 코드 작성 (여러 개의 | 사용 필요)
  • 문자열 로 명령어를 작성하는 방식의 불편함

반면에, zx 를 사용하는 경우 자바스크립트 매서드를 활용하여 익숙하고 편리한 방식으로 작성할 수 있습니다.

const data = {
  users: [
    {
      id: 1,
      name: "John",
      contacts: {
        email: "john@example.com",
        phone: "123-456-7890",
      },
    },
  ],
};
const johnEmail = data.users.find((user) => user.name === "John").contacts
  .email;
console.log("John's Email:", johnEmail);

장점 2) $ 함수를 통한 쉘 스크립트 명령어 병행 사용

앞선 단락에서는 쉘 명령어를 사용할 때에 단점에 대해 설명드렸지만, 반대로 쉘 명령어가 강점을 발휘하는 순간도 있습니다. 많은 양의 처리가 필요한 작업에서는 쉘 명령어가 강한 성능을 발휘하기도 하고, 이미 쉘 스크립트로 작성 된 다양한 레퍼런스를 쉽게 참고할 수 있기 때문입니다.

필요한 경우에 zx 에서는 $ 함수를 사용해 쉘 명령어를 직접 호출할 수 있습니다. $ 함수를 불러와 문자열 형식으로 명령어를 작성해주면 자바스크립트와 쉘 명령어의 장점을 동시에 활용할 수 있게 됩니다.

ls *.js | wc -l 는 현재 디렉토리에 있는 .js 파일의 갯수를 세는 명령어입니다. $ 함수를 사용해 쉘 스크립트 구문을 실행하고, 자바스크립트의 console.log 를 통해 결과를 출력하고 있습니다.

import { $ } from "zx";

const files = await $`ls *.js | wc -l`;
console.log(files.stdout);

장점 3) Node.js 생태계를 활용한 작업 처리

zx 를 사용하면 스크립트 내에서 npm 패키지를 사용할 수 있습니다. HTTP 요청, 파일 압축, 이미지 처리 등 각 상황에 맞는 도구를 사용하여 원하는 작업을 쉽게 처리할 수 있게 됩니다.

예를 들어, HTTP 요청 시 자바스크립트 사용자에게 익숙한 axios 패키지를 동일하게 사용할 수 있습니다.

import axios from "axios";

const response = await axios.get("https://api.example.com/data");

그 외에도 zx 는 여러 가지 장점 을 가지고 있습니다.

  • Promise와 async/await를 통해 비동기 작업을 쉽게 처리할 수 있습니다.
  • 필요한 경우 타입 시스템(TypeScript)을 활용하여 안정적이고 정확한 코드 작성이 가능합니다.

프론트엔드 개발자가 zx를 활용하는 방법

그러면 zx 는 어떤 상황에서 유용하게 사용할 수 있을까요? Next.js 로 만들어진 게임 어드민에서 이미지 파일을 다루기 위해 스크립트를 사용하는 사례를 통해 알아보겠습니다.

게임 어드민에는 프로젝트 내에 많은 이미지 삽입이 필요합니다. 게임 클라이언트에서 보여주는 많은 이미지를 운영툴에서도 동일하게 보여 주어야 하기 때문입니다. 보안 관련 번거로운 부분이 있기에 원격 이미지 저장소를 활용하는 대신 프로젝트에 직접 커밋을 하는 형태를 가지고 있습니다.

이미지 추가는 아래와 같은 방식으로 이루어집니다.

image update process

이미지 업데이트 프로세스

이 과정에는 몇 가지 어려움이 있었습니다.

  • 수 만개의 이미지 파일과 수천 개의 폴더로 구성된 이미지 저장소의 크기
  • 많은 이미지 파일 중 어드민에 필요한 이미지의 저장 경로 파악
  • 게임 업데이트에 따라 주기적으로 새로운 이미지 추가 필요

이렇게 이미지를 옮기는 것은 지루하고 시간도 많이 소요됩니다. 다음 단락에서 귀찮은 작업을 어떻게 스크립트로 바꾸었는지 알아보겠습니다.

주요 스크립트 내용

아래 내용은 독자의 이해를 돕고 외부에 공개하기 위해 핵심 로직을 남기고 부가적인 부분은 추상화 하였습니다.

먼저, 스크립트 내에서는 prefix 규칙을 활용해 원하는 이미지를 검색합니다. 어드민에서 필요한 이미지들은 이름에서 공통적인 prefix 규칙을 사용하고 있기 때문입니다. prefix 를 기준으로 스크립트를 작성하면 새로운 아이템이 추가 되어도 동일한 로직를 활용해 이미지를 가져올 수 있습니다.

각 단계별 함수를 호출하는 main 함수를 통해 전체적인 흐름을 소개하겠습니다. (main 함수 내 들어가는 개별 함수의 내용은 설명으로 대체하였습니다. 세부 내용이 궁금하시다면 아래에 첨부한 ‘스크립트 전체 코드’를 통해 확인해 주세요!)

const main = async () => {
  const { targetPath, sourcePath, prefixes } = config;

  await fs.mkdir(targetPath, { recursive: true });

  const findPatterns = prefixes.map((prefix) => `-name ${prefix}`).join(" -o ");
  const files = await findFiles(sourcePath, findPatterns);

  await copyFiles(files, targetPath);
  console.log(`.png 파일들이 모두 ${targetPath}로 복사되었습니다.`);
};

먼저, Node.jsfs 모듈을 사용하여 파일을 복사할 디렉토리를 생성합니다.

// 'recursive: true' 는 targetPath 경로에 있는 디렉토리가 없을 경우 생성해 주는 옵션입니다.
await fs.mkdir(targetPath, { recursive: true });

파일을 찾을 규칙을 findPatterns 변수로 정의하고, 복사할 파일의 경로를 리스트업 해주는 함수 findFiles 를 호출합니다. (함수의 내부에서는 $ 연산자를 통해 쉘 스크립트 문법을 사용하여 파일을 검색합니다.)

const findPatterns = prefixes.map((prefix) => `-name ${prefix}`).join(" -o ");
const files = await findFiles(sourcePath, findPatterns);

다음으로는 copyFiles 함수를 사용해 FindFiles 에서 찾은 파일을 복사하는 동작을 수행합니다. (함수 내부에서 파일 복사는 fs 모듈의 copyFile 을 사용하였습니다. 또한, Promise.all() 을 사용하여 복사 작업을 병렬적으로 처리해 주었습니다.

await copyFiles(files, targetPath);

마지막으로 모든 작업이 포함 된 main 함수를 실행하고 또 에러 처리를 위해 후속 메서드 catch 를 추가해 주었습니다.

main().catch(console.error);
Appendix) 스크립트 전체 코드
import { promises as fs } from "fs";
import path from "path";
import { $ } from "zx";

const config = {
  targetPath: "./target",
  sourcePath: "../source",
  prefixes: ["prefix1_.*\\.png", "prefix2_.*\\.png", "prefix3_.*\\.png"],
};

const main = async () => {
  const { targetPath, sourcePath, prefixes } = config;

  await fs.mkdir(targetPath, { recursive: true });

  const findPatterns = prefixes.map((prefix) => `-name ${prefix}`).join(" -o ");
  const files = await findFiles(sourcePath, findPatterns);

  await copyFiles(files, targetPath);
  console.log(`.png 파일들이 모두 ${targetPath}로 복사되었습니다.`);
};

const findFiles = async (sourcePath, pattern) => {
  const command = ["find", sourcePath, "-type", "f"].concat(pattern.split(" "));
  let files = await $`${command}`;
  return files.stdout.split("\n").filter(Boolean);
};

const copyFiles = async (files, targetPath) => {
  const copyTasks = files.map((file) =>
    fs.copyFile(file, `${targetPath}/${path.basename(file)}`)
  );
  await Promise.all(copyTasks);
};

main().catch(console.error);

zx 사용을 통한 변화

zx 로 작성한 스크립트를 사용하면서 생긴 변화를 단계별로 표현해 보았습니다. 기존의 5단계의 과정이 필요했던 작업이 2단계로 줄어든 것을 알 수 있습니다. 또한, prefix 를 추가해야 되는 상황은 가끔 발생하는 일이기에 부담이 더욱 줄어들게 됩니다.

image update process renewal

이미지 업데이트 프로세스 비교

"zx를 사용해 이미지 변경 사항에 거의 신경쓰지 않고 프로젝트를 관리할 수 있게 되었습니다."

글을 마치며

단순 작업을 반복하게 되면 종종 ‘나 대신 작업을 해 줄 사람이 없을까?’ 하는 생각이 들 때가 있습니다. 그러나 업무 자동화 를 선뜻 시작하기에는 부담스러운 경우가 많은 것 같습니다. 대부분의 개발자들에게 스크립트 작업은 주요 업무가 아니기 때문에, 생소한 문법을 배우고 구현하는 과정에서 여러 시행착오가 있을 수 있기 때문입니다.

이런 점에서 많은 개발자에게 zx는 유용한 도구가 될 것이라 생각합니다. 자바스크립트에서 제공하는 편의성과 쉘 스크립트의 뛰어난 성능과 다양한 자료를 동시에 활용할 수 있어, 빠르게 원하는 자동화 작업을 구현할 수 있기 때문입니다.  

“지루한 반복 업무가 있다면 zx를 통한 업무 자동화를 적극 추천합니다.”

© 2024 Devsisters Corp. All Rights Reserved.