CLI(Command Line Interface) ๊ธฐ๋ฐ ๋ ธ๋ ํ๋ก๊ทธ๋จ์ ์ ์ํด๋ณด๊ธฐ
๋ ธ๋ ํ์ผ์ ์คํํ ๋ node [ํ์ผ๋ช ] ๋ช ๋ น์ด๋ฅผ ์ฝ์์ ์ ๋ ฅํจ
node-cli ํด๋ ์์ package.json๊ณผ index.js ์์ฑ
๐ปpackage.json
{
"name": "node-cli",
"version": "0.0.1",
"description": "nodejs cli program",
"main": "index.js",
"author": "ZeroCho",
"license": "ISC",
"bin": {
"cli": "./template.js"
}
}
๐ปindex.js
#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
const answerCallback = (answer) => {
if (answer === 'y') {
console.log('๊ฐ์ฌํฉ๋๋ค!');
rl.close();
} else if (answer === 'n') {
console.log('์ฃ์กํฉ๋๋ค!');
rl.close();
} else {
console.clear();
console.log('y ๋๋ n๋ง ์
๋ ฅํ์ธ์.');
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
}
};
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
package.json์ ๋ค์ ์ค์ ์ถ๊ฐ
๐ปpackage.json
{
"name": "node-cli",
"version": "0.0.1",
"description": "nodejs cli program",
"main": "index.js",
"author": "ZeroCho",
"license": "ISC",
"bin": {
"cli": "./template.js"
}
}
npm i -g๋ก ์ค์น ํ cli๋ก ์คํ
process.argv๋ก ๋ช ๋ น์ด์ ์ด๋ค ์ต์ ์ด ์ฃผ์ด์ก๋์ง ํ์ธ ๊ฐ๋ฅ(๋ฐฐ์ด๋ก ํ์)
๐ปindex.js
#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
const answerCallback = (answer) => {
if (answer === 'y') {
console.log('๊ฐ์ฌํฉ๋๋ค!');
rl.close();
} else if (answer === 'n') {
console.log('์ฃ์กํฉ๋๋ค!');
rl.close();
} else {
console.clear();
console.log('y ๋๋ n๋ง ์
๋ ฅํ์ธ์.');
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
}
};
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
๋ ธ๋ ๋ด์ฅ ๋ชจ๋ readline ์ฌ์ฉ
๐ปindex.js
#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
const answerCallback = (answer) => {
if (answer === 'y') {
console.log('๊ฐ์ฌํฉ๋๋ค!');
rl.close();
} else if (answer === 'n') {
console.log('์ฃ์กํฉ๋๋ค!');
rl.close();
} else {
console.clear();
console.log('y ๋๋ n๋ง ์
๋ ฅํ์ธ์.');
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
}
};
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
console.clear๋ก ์ฝ์ ๋ด์ฉ ์ง์ฐ๊ธฐ
ํ๋ก๊ทธ๋จ ์์ ์์, ์๋ชป๋ ๋ต๋ณ ํ์ ์ฝ์ ์ง์
๐ปindex.js
#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
const answerCallback = (answer) => {
if (answer === 'y') {
console.log('๊ฐ์ฌํฉ๋๋ค!');
rl.close();
} else if (answer === 'n') {
console.log('์ฃ์กํฉ๋๋ค!');
rl.close();
} else {
console.clear();
console.log('y ๋๋ n๋ง ์
๋ ฅํ์ธ์.');
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
}
};
rl.question('์์ ๊ฐ ์ฌ๋ฏธ์์ต๋๊น? (y/n) ', answerCallback);
๐ปtemplate.js
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
let rl;
let type = process.argv[2];
let name = process.argv[3];
let directory = process.argv[4] || '.';
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Template</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => { // ํด๋ ์กด์ ํ์ธ ํจ์
try {
fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => { // ๊ฒฝ๋ก ์์ฑ ํจ์
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplate = () => { // ํ
ํ๋ฆฟ ์์ฑ ํจ์
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if (exist(pathToFile)) {
console.error('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค');
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(pathToFile, '์์ฑ ์๋ฃ');
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if (exist(pathToFile)) {
console.error('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค');
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(pathToFile, '์์ฑ ์๋ฃ');
}
} else {
console.error('html ๋๋ express-router ๋ ์ค ํ๋๋ฅผ ์
๋ ฅํ์ธ์.');
}
};
const dirAnswer = (answer) => { // ๊ฒฝ๋ก ์ค์
directory = (answer && answer.trim()) || '.';
rl.close();
makeTemplate();
};
const nameAnswer = (answer) => { // ํ์ผ๋ช
์ค์
if (!answer || !answer.trim()) {
console.clear();
console.log('name์ ๋ฐ๋์ ์
๋ ฅํ์
์ผ ํฉ๋๋ค.');
return rl.question('ํ์ผ๋ช
์ ์ค์ ํ์ธ์. ', nameAnswer);
}
name = answer;
return rl.question('์ ์ฅํ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ์ธ์.(์ค์ ํ์ง ์์ผ๋ฉด ํ์ฌ๊ฒฝ๋ก) ', dirAnswer);
};
const typeAnswer = (answer) => { // ํ
ํ๋ฆฟ ์ข
๋ฅ ์ค์
if (answer !== 'html' && answer !== 'express-router') {
console.clear();
console.log('html ๋๋ express-router๋ง ์ง์ํฉ๋๋ค.');
return rl.question('์ด๋ค ํ
ํ๋ฆฟ์ด ํ์ํ์ญ๋๊น? ', typeAnswer);
}
type = answer;
return rl.question('ํ์ผ๋ช
์ ์ค์ ํ์ธ์. ', nameAnswer);
};
const program = () => {
if (!type || !name) {
rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
rl.question('์ด๋ค ํ
ํ๋ฆฟ์ด ํ์ํ์ญ๋๊น? ', typeAnswer);
} else {
makeTemplate();
}
};
program(); // ํ๋ก๊ทธ๋จ ์คํ๋ถ
๋๋ ํ ๋ฆฌ๊ฐ ์กด์ฌํ๋์ง ํ์ธํ๋ exist ํจ์์ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํ๋ mkdirp ํจ์๋ฅผ ๋ง๋ฆ
program์ด๋ผ๋ ํจ์๋ template.js์ ์คํ๋ถ, makeTemplate์ ์ต์
์ ์ฝ์ด์ ์๋ง์ ํ
ํ๋ฆฟ์ ์์ฑํด์ฃผ๋ ํจ์
์ต์
์ ๋ฐ๋ผ ๋ค๋ฅธ ๋์์ ํ๋๋ก ๋ถ๊ธฐ ์ฒ๋ฆฌ
package.json์ ๋ช
๋ น์ด๋ฅผ ๋ฐ๊ฟ์ฃผ๊ณ ์ ์ญ ์ฌ์ค์น
template.js๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์
๐๋งํฌ์ฐธ๊ณ
npm์๋ CLI ํ๋ก๊ทธ๋จ์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ง์ด ์ค๋น๋์ด ์์
npm i commander inquirer chalk
command.js ํ์ผ ์์ฑ
- version: ํ๋ก๊ทธ๋จ์ ๋ฒ์ ์ค์ (--version ๋๋ -v๋ก ํ์ธ)
- usage: ํ๋ก๊ทธ๋จ ์ฌ์ฉ ๋ฐฉ๋ฒ ๊ธฐ์ (--help๋ก ๋๋ โh๋ก ํ์ธ)
- command: ๋ช ๋ น์ด ๋ฑ๋ก(template ๊ณผ * ๋ฑ๋กํจ)
- <>๋ ํ์ ์ต์ ์ ์๋ฏธ
- []๋ ์ ํ ์ต์ ์ ์๋ฏธ
- description: ๋ช ๋ น์ด์ ๋ํ ์ค๋ช ์ ์ค์ ํ๋ ๋ฉ์๋
- alias: ๋ช ๋ น์ด์ ๋ํ ๋ณ์นญ
- option: ๋ช ๋ น์ด์ ๋ํ ์ต์ ์ ๋ฑ๋ก
- --์ต์ [๊ฐ] ๋๋ โ์ต์ <๊ฐ> ํ์
- ๋ ๋ฒ์งธ ์ธ์๋ ์ค๋ช , ์ธ ๋ฒ์งธ ์ธ์๋ ๊ธฐ๋ณธ๊ฐ
- Action: ๋ช ๋ น์ด๊ฐ ์คํ๋ ๋ ์ํํ ๋์ ๋ฑ๋ก
- parse: process.argv๋ฅผ ํ์ฑํ์ฌ ์ต์ ๋ฑ๋ก
๐ปcommand.js
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta chart="utf-8" />
<title>Template</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => {
try {
fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => {
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplate = (type, name, directory) => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else {
console.error(chalk.bold.red('html ๋๋ express-router ๋ ์ค ํ๋๋ฅผ ์
๋ ฅํ์ธ์.'));
}
};
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('ํ
ํ๋ฆฟ์ ์์ฑํฉ๋๋ค.')
.alias('tmpl')
.option('-f, --filename [filename]', 'ํ์ผ๋ช
์ ์
๋ ฅํ์ธ์.', 'index')
.option('-d, --directory [path]', '์์ฑ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์', '.')
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((cmd, args) => {
if (args) {
console.log(chalk.bold.red('ํด๋น ๋ช
๋ น์ด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.'));
program.help();
} else {
inquirer.prompt([{
type: 'list',
name: 'type',
message: 'ํ
ํ๋ฆฟ ์ข
๋ฅ๋ฅผ ์ ํํ์ธ์.',
choices: ['html', 'express-router'],
}, {
type: 'input',
name: 'name',
message: 'ํ์ผ์ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.',
default: 'index',
}, {
type: 'input',
name: 'directory',
message: 'ํ์ผ์ด ์์นํ ํด๋์ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์.',
default: '.',
}, {
type: 'confirm',
name: 'confirm',
message: '์์ฑํ์๊ฒ ์ต๋๊น?',
}])
.then((answers) => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('ํฐ๋ฏธ๋์ ์ข
๋ฃํฉ๋๋ค.'));
}
});
}
})
.parse(process.argv);
ํ๋ก๊ทธ๋จ ์ ์ญ ์ฌ์ค์น ํ ์คํํด๋ณด๊ธฐ
command.js ์์
๐ปcommand.js
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta chart="utf-8" />
<title>Template</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => {
try {
fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => {
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplate = (type, name, directory) => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else {
console.error(chalk.bold.red('html ๋๋ express-router ๋ ์ค ํ๋๋ฅผ ์
๋ ฅํ์ธ์.'));
}
};
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('ํ
ํ๋ฆฟ์ ์์ฑํฉ๋๋ค.')
.alias('tmpl')
.option('-f, --filename [filename]', 'ํ์ผ๋ช
์ ์
๋ ฅํ์ธ์.', 'index')
.option('-d, --directory [path]', '์์ฑ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์', '.')
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((cmd, args) => {
if (args) {
console.log(chalk.bold.red('ํด๋น ๋ช
๋ น์ด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.'));
program.help();
} else {
inquirer.prompt([{
type: 'list',
name: 'type',
message: 'ํ
ํ๋ฆฟ ์ข
๋ฅ๋ฅผ ์ ํํ์ธ์.',
choices: ['html', 'express-router'],
}, {
type: 'input',
name: 'name',
message: 'ํ์ผ์ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.',
default: 'index',
}, {
type: 'input',
name: 'directory',
message: 'ํ์ผ์ด ์์นํ ํด๋์ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์.',
default: '.',
}, {
type: 'confirm',
name: 'confirm',
message: '์์ฑํ์๊ฒ ์ต๋๊น?',
}])
.then((answers) => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('ํฐ๋ฏธ๋์ ์ข
๋ฃํฉ๋๋ค.'));
}
});
}
})
.parse(process.argv);
๋ช ๋ น์ด๋ค์ ์ปค๋งจ๋ ์์ผ๋ก ์ ํํจ
์ฌ์ ํ ์ต์ ๋ค์ ์ธ์์ผ ํ๋ ๋ถํธํจ
๐ปcommand.js
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta chart="utf-8" />
<title>Template</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => {
try {
fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => {
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplate = (type, name, directory) => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if (exist(pathToFile)) {
console.error(chalk.bold.red('์ด๋ฏธ ํด๋น ํ์ผ์ด ์กด์ฌํฉ๋๋ค'));
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(chalk.green(pathToFile, '์์ฑ ์๋ฃ'));
}
} else {
console.error(chalk.bold.red('html ๋๋ express-router ๋ ์ค ํ๋๋ฅผ ์
๋ ฅํ์ธ์.'));
}
};
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('ํ
ํ๋ฆฟ์ ์์ฑํฉ๋๋ค.')
.alias('tmpl')
.option('-f, --filename [filename]', 'ํ์ผ๋ช
์ ์
๋ ฅํ์ธ์.', 'index')
.option('-d, --directory [path]', '์์ฑ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์', '.')
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((cmd, args) => {
if (args) {
console.log(chalk.bold.red('ํด๋น ๋ช
๋ น์ด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.'));
program.help();
} else {
inquirer.prompt([{
type: 'list',
name: 'type',
message: 'ํ
ํ๋ฆฟ ์ข
๋ฅ๋ฅผ ์ ํํ์ธ์.',
choices: ['html', 'express-router'],
}, {
type: 'input',
name: 'name',
message: 'ํ์ผ์ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.',
default: 'index',
}, {
type: 'input',
name: 'directory',
message: 'ํ์ผ์ด ์์นํ ํด๋์ ๊ฒฝ๋ก๋ฅผ ์
๋ ฅํ์ธ์.',
default: '.',
}, {
type: 'confirm',
name: 'confirm',
message: '์์ฑํ์๊ฒ ์ต๋๊น?',
}])
.then((answers) => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('ํฐ๋ฏธ๋์ ์ข
๋ฃํฉ๋๋ค.'));
}
});
}
})
.parse(process.argv);
readline๋ณด๋ค ๊ฐ๊ฒฐํด์ง
์ฝ์์ ์์ ์ถ๊ฐํจ
๋ง๋ CLI ํ๋ก๊ทธ๋จ์ ๊ณต์ ํ๊ณ ์ถ๋ค๋ฉด 5์ฅ์ ๊ณผ์ ๋๋ก npm์ ๋ฐฐํฌํ๋ฉด ๋จ
๐์ถ์ฒ๐
Node.js ๊ต๊ณผ์ - ๊ธฐ๋ณธ๋ถํฐ ํ๋ก์ ํธ ์ค์ต๊น์ง
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard