커맨드라인을 활용한 TODO만들기(feat: yargs)

onezerokang·2021년 4월 13일
2
post-thumbnail

코딩을 하다보면 npm run start, npm --version 같은 커맨드를 사용하게 됩니다. 오늘은 yargs를 이용해서 나만의 커맨드라인, TODO 프로그램을 만들어보려고 합니다.

커맨드라인 인자 받기

Node.js에서는 보통 app.js를 메인파일로 사용합니다. 만약 app.js를 실행시키려고 하면 node app.js를 하면 됩니다. 아래는 간단한 예시입니다.

app.js

console.log("Hello command line!")

terminal

node app.js

//result
Hello command line!

터미널에서 node app.js를 입력하면 pp.js가 노드에서 동작합니다(노드는 JS런타임이기 때문이다). 그런데 만약 다음과 같이 커맨드를 입력한다면 어떻게 될까요?

terminal

node app.js add --title="First Task"

app.js 뒤에 add --title="First Task" 이 붙었습니다. 그래도 위 코드는 똑같이 동작합니다. 그러면 add와 --title은 무엇일까요? 위 커맨드라인에서 add는 인자, --title은 옵션입니다.

이에 대해 잠깐 설명하자면 이는 함수와 비슷하다고 볼 수 있습니다. 함수는 인자를 받아 그에 따른 결과를 출력합니다. 즉 node app.js add --title="First Task"이라 했을 때 app.js에 add 인자로, --title="First Task"을 옵션으로 제공한 것입니다.

app.js가 받은 인자와 옵션을 바탕으로 분기처리하여 조건에 맞는 코드가 동작하도록 하는 것입니다(이해가 안되도 예시를 보다보면 이해가 될 것이다). app.js를 다음과 같이 수정해보겠습니다.

app.js

console.log(process.argv);

terminal

node app.js add --title="First Task"

//result
[
  'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\oneze\\Desktop\\node-course\\notes-app\\app.js',
  'add',
  '--test=First Task'
]

process.argv의 첫번째 요소는 node의 위치, 두번째 요소는 node를 실행시킨 파일의 위치가 저장되어있습니다. 그 이후 요소는 인자와 옵션을 받습니다. 즉 인자와 옵션을 받고 process.argv[2]등을 이용하여 app.js의 내부 동작을 컨트롤 할 수 있습니다. 이제 이것을 가지고 TODO를 만들어보겠습니다.

Yargs

그런데 process.argv는 불편한 점이 있습니다. 배열에서 요소를 꺼내쓰는 것도 불편하고, 옵션을 사용하기 편하게 파싱하지도 못합니다. 그래서 우리는 yargs 패키지를 사용할 것입니다.

npm i yargs

yargs는 command line의 옵션 인수를 파싱하는 package입니다. 즉 커맨드라인 인수를 분석하고 사용자 인터페이스를 생성하여 대화형 커맨드 라인을 구축할 수 있습니다/

그러면 yargs의 사용법을 TODO 커맨드라인을 만들며 보겠습니다.

const yargs = require('yargs');

//할 일을 추가하는 코드
yargs.command({
  //커맨드의 첫번째 인자. 만약 node app.js add를 입력하면 이 코드가 실행된다.
  command: "add",
  //add 커맨드의 설명 node app.js --help를 했을 때 나오는 문구다.
  describe: "Add a task",
  //--으로 시작하는 옵션을 설정하는 객체. title과 body를 설정했고, demandOption을 true로 하여 필수적으로 들어가도록 했다.
  builder: {
    title: {
      describe: "Task title",
      demandOption: true,
      type: "string"
    },
    body: {
      describe: "Task body",
      demandOption: true,
      type: "string"
    },
  },
  //node app.js add --title="제목 값" --body="본문"을 입력했을 때 실행될 함수. addNotes는 나중에 만들 예정이다. argv.title과 body값을 전달하고 있다.
  handler(argv){
    addNotes(argv.title, argv.body)
  }
})

위에 주석으로 설명을 작성하였지만 다시한번 설명하자면 yargs.command를 사용하면 커맨드를 추가 할 수 있습니다. 그 안에 command는 어떤 커맨드를 입력할 경우 작동될 코드인지를 커맨드 이름을 지정해주는 것입니다(그 외 이해가 안되는 것이 있으면 공식문서를 보거나 댓글을 달아주세요)

이제 todos파일을 만들어 그 안에 addNotes함수를 만들어주겠습니다.

todos.js

cosnt fs = require('fs');
const chalk = require('chalk')

//addNote
const addNote = (title, body) => {
  const notes = loadNotes();
  const duplicateNote = notes.find((note) => note.title === title);
  if (!duplicateNote) {
    notes.push({
      title,
      body,
    });
    saveNotes(notes);
    console.log(chalk.green.inverse("New note added!"));
  } else {
    console.log(chalk.red.inverse("Note title taken!"));
  }
};

//addNote에서 호출될 laodNotes. 
const loadNotes = () => {
  try {
    const dataBuffer = fs.readFileSync("todos.json");
    const dataJSON = dataBuffer.toString();
    return JSON.parse(dataJSON);
  } catch (e) {
    return [];
  }
};

//addNote에서 호출될 saveNotes
const saveNotes = (notes) => {
  const dataJSON = JSON.stringify(notes);
  fs.writeFileSync("todos.json", dataJSON);
};

위 코드를 설명하자면 이 프로그램은 json파일에 TODO를 저장할 예정입니다. 그래서 loadNotes함수에서 todos.json을 가져옵니다. 만약 없을 경우 빈 배열을 리턴합니다. 이후 title과 body 값을 받아준후 이를 json으로 만들어 todos.json에 저장해주는 것입니다.

나머지 삭제하기, 리스트 가져오기, 할일 읽기는 비슷한 원리이니 전체 코드를 복붙하겠습니다(혹시 이해안되는 것있으면 댓글주세요).

app.js

const yargs = require("yargs");
const tasks = require("./tasks");

yargs.version("1.1.0");

yargs.command({
  command: "add",
  describe: "Add a new task",
  builder: {
    title: {
      describe: "Task title",
      demandOption: true,
      type: "string",
    },
    body: {
      describe: "Task body",
      demandOption: true,
      type: "string",
    },
  },
  handler(argv) {
    tasks.addTask(argv.title, argv.body);
  },
});

yargs.command({
  command: "remove",
  describe: "Remove a task",
  builder: {
    title: {
      describe: "Please enter a title for the task you want to remove.",
      demandOption: true,
      type: "string",
    },
  },
  handler(argv) {
    tasks.removeTask(argv.title);
  },
});

yargs.command({
  command: "read",
  describe: "Read a task!",
  builder: {
    title: {
      describe: "Title of you'll read",
      demandOption: true,
      type: "string",
    },
  },
  handler(argv) {
    tasks.readTask(argv.title);
  },
});

yargs.command({
  command: "list",
  describe: "List your tasks",
  handler() {
    tasks.listTasks();
  },
});
//인수를 parsing 한다.
yargs.parse();
console.log(process.argv);

todos.js

const fs = require("fs");
const chalk = require("chalk");

const addTask = (title, body) => {
  const tasks = loadTasks();
  const duplicateTask = tasks.find((task) => task.title === title);
  if (!duplicateTask) {
    tasks.push({
      title,
      body,
    });
    saveTasks(tasks);
    console.log(chalk.green.inverse("New task added!"));
  } else {
    console.log(chalk.red.inverse("Task title taken!"));
  }
};

const saveTasks = (tasks) => {
  const dataJSON = JSON.stringify(tasks);
  fs.writeFileSync("tasks.json", dataJSON);
};

const loadTasks = () => {
  try {
    const dataBuffer = fs.readFileSync("tasks.json");
    const dataJSON = dataBuffer.toString();
    return JSON.parse(dataJSON);
  } catch (e) {
    return [];
  }
};

const removeTask = (title) => {
  const tasks = loadTasks();
  const tasksToKeep = tasks.filter((task) => task.title !== title);
  if (tasks.length > tasksToKeep.length) {
    console.log(chalk.green.inverse("Task removed!"));
    saveTasks(tasksToKeep);
  } else {
    console.log(chalk.red.inverse("No task found"));
  }
};

const listTasks = () => {
  const listTasks = loadTasks();
  if (listTasks.length !== 0) {
    console.log(chalk.green.inverse("Your tasks"));
    listTasks.map((task) => console.log("-" + task.title));
  } else {
    console.log(chalk.red.inverse("No task found"));
  }
};

const readTask = (title) => {
  const tasks = loadTasks();
  const task = tasks.find((task) => task.title === title);
  console.log(task);
  if (task) {
    console.log(chalk.green.inverse(task.title));
    console.log(task.body);
  } else {
    console.log(chalk.red.inverse("No task found"));
  }
};

module.exports = { addTask, removeTask, listTasks, readTask };

참고한 것
https://www.npmjs.com/package/yargs
https://www.udemy.com/course/the-complete-nodejs-developer-course-2/

오타있으면 피드백 부탁드립니다:)

profile
블로그 이전 했습니다: https://techpedia.tistory.com

0개의 댓글