[Node.js] Node.js를 활용한 SVG 파일 자동 모듈화 방법
안녕하세요. 오늘은 Node.js를 활용한 SVG 파일 자동 모듈화 방법에 대해서 작성해 보고자 합니다.
Background
회사 내부에는 개발에 사용하는 tokript라는 스크립트가 있습니다. 해당 스크립트는 commit 규칙뿐만 아니라 Api, Image, svg, page 등을 생성해 주는 아주 간편하고 유용한 스크립트입니다.
문제는 React-native에서 svg파일의 아이콘을 사용할 때 선언하는 방식이 tokript의 아이콘을 생성하는 방식과는 달라서 tokript에서 gen:icon을 사용할 수 없었습니다.
수정 요청을 할까하다가 직접 만들고 싶은 호기심에 작성해 보게 되었습니다. 폴더 안의 폴더가 있어 재귀함수를 적용시켜 탐색하였습니다.
1. 원하는 모듈의 형태와 추출할 경로
들어가기 앞서 출력하길 원하는 모듈의 형태와 추출할 경로를 먼저 봅니다. 어떤식으로 출력할지 생각을 해보면 더 좋겠습니다.
- 추출 경로
추출할 경로는 src/assets/icons 안에있는 폴더들의 svg입니다. 하위 폴더들의 svg파일을 읽어야 하기 때문에 재귀적으로 탐색해야 합니다.
- 원하는 모듈의 형태
tokript로 generate를 하면 생성되는 경로와 동일하게 했습니다. 해당 경로에 아래와 같은 형태의 모듈로 출력되게 하면 됩니다.
- svg파일이 있는 폴더에서 svg정보 추출하기
- svg파일 이름 PascalCase로 변환
- 경로를 읽어 from 이후에 적용시키기
// src/generated/icons/icons.ts
export { default as BehaviorOffIcon } from '@assets/icons/bottomTab/behavior-off.svg'
export { default as BehaviorOnIcon } from '@assets/icons/bottomTab/behavior-on.svg'
export { default as EducationOffIcon } from '@assets/icons/bottomTab/education-off.svg'
export { default as EducationOnIcon } from '@assets/icons/bottomTab/education-on.svg'
2. 아이콘을 생성해주는 Script 작성 (genIcon.js)
SVG 파일이 있는 디렉토리에서 SVG 파일을 찾아서 해당 파일 이름에 대한 PascalCase 인터페이스를 생성하고, 각 파일에 대한 export 구문을 생성하는 코드를 작성해 보겠습니다.
필요한 node.js 모듈 가져오기
const fs = require('fs');
const path = require('path');
findSVGFiles() 함수 만들기
findSVGFiles() 함수에서 SVG 파일을 찾아서 해당 파일 이름에 대한 PascalCase 인터페이스를 생성하고, 각 파일에 대한 export 구문을 생성합니다.
1. readdirsync로 경로의 파일 목록 가져오기
최종적으로 반환할 fileList를 빈 배열로 먼저 선언해줍니다. 인자에는 svg파일이 들어있는 경로가 들어가게 됩니다.
const directoryPath = '../assets/icons';
const outputPath = '../generated/icons';
const svgFiles = findSVGFiles(directoryPath);
function findSVGFiles(dirPath) {
let fileList = [];
// dirPath경로의 파일 목록을 가져옵니다.
const files = fs.readdirSync(dirPath);
}
return fileList;
}
fs 모듈에서 제공하는 readdirsync함수를 통해 파일의 이름을 가져옵니다. 선언한 directoryPath의 경로의 하위폴더 이름까지 반환하기 때문에 하위디렉터리 이름이면 재귀적으로 디렉터리 탐색을 수행해야 합니다.
readdirSync
Node.js의 내장 모듈 중 하나로, 동기적으로 파일 시스템 디렉토리의 내용을 읽어와 파일과 하위 디렉터리의 이름을 배열로 반환합니다.
2. 파일 목록에 대하여 반복문 실행
파일 목록에 대한 정보는 startSync 모듈을 사용하면 됩니다. startSync에는 filePath를 제공해야하는데 이를 위해서 파일 경로를 다루는 내장모듈인 path 모듈을 사용하면 됩니다.
pth.join()함수로 path를 제공하면 각 파일의 path가 출력됩니다. 이렇게 출력된 path를 startSync 함수를 사용하여 파일의 정보를 구해줍니다.
function findSVGFiles(dirPath, fileList) {
fileList = fileList || [];
// dirPath경로의 파일 목록을 가져옵니다.
const files = fs.readdirSync(dirPath);
// 파일 목록에 대하여 반복문을 실행
for (let i = 0; i < files.length; i++) {
// 파일의 경로를 구합니다.
const filePath = path.join(dirPath, files[i]);
// 파일의 상태 정보를 구합니다.
const fileStat = fs.statSync(filePath);
// 해당 파일이 디렉토리인 경우, 재귀적으로 디렉토리 탐색을 수행합니다.
if (fileStat.isDirectory()) {
findSVGFiles(filePath, fileList);
}
}
return fileList;
}
fileStat.isDirectory()를 통해 파일이 디렉터리인 경우 재귀적으로 디렉터리 탐색을 수행하게 해 줍니다.
startSync
Node.js의 내장 모듈 중 하나로, 동기적으로 파일이나 디렉터리의 상태 정보를 가져오는 메서드입니다. 파일의 크기, 권한, 생성일자, 수정일자 등의 정보를 포함합니다.
fileStat.isDirectory()
Node.js에서 파일 정보 객체를 나타내는 fs.Stats 객체의 메서드 중 하나입니다. 해당 파일 정보 객체가 디렉토리인지 아닌지를 확인하는 데 사용됩니다. isDirectory() 메소드는 호출된 파일 정보 객체가 디렉토리인 경우 true를, 그렇지 않은 경우 false를 반환합니다.
3. 해당 파일이 SVG 파일인 경우 파일명을 변환해 export 구문 생성하기
const fs = require('fs');
const path = require('path');
const directoryPath = '../assets/icons';
const outputPath = '../generated/icons';
const svgFiles = findSVGFiles(directoryPath);
function findSVGFiles(dirPath, fileList) {
fileList = fileList || [];
const files = fs.readdirSync(dirPath);
for (let i = 0; i < files.length; i++) {
...
// 해당 파일이 SVG 파일인 경우
else if (path.extname(filePath) === '.svg') {
// 파일명에서 .svg를 제외한 부분을 추출합니다.
const baseName = path.basename(filePath, '.svg');
// 상대 경로에서 ../assets/ 부분을 제거합니다.
const iconPath = filePath.replace(`../assets/`, '');
// 파일명을 PascalCase로 변환합니다.
const iconName = convertToPascalCase(baseName + 'Icon');
// 변환한 파일명을 이용하여 export 구문을 생성합니다.
const iconExportPath = `export { default as ${iconName} } from '@assets/${iconPath}'`;
fileList.push(iconExportPath);
}
}
return fileList;
}
path.extname(filePath)
Node.js에서 파일 경로를 다루는 내장 모듈인 path의 메소드 중 하나입니다. 해당 파일 경로에서 확장자를 추출하는 데 사용됩니다.
path.basename(filePath, '. svg')
Node.js에서 파일 경로를 다루는 내장 모듈인 path의 메서드 중 하나입니다. 해당 파일 경로에서 파일 이름을 추출하는 데 사용됩니다.
*converToPascalCase()
string을 pascal case로 변환해 주는 함수입니다.
function convertToPascalCase(str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word) {
return word.toUpperCase();
})
.replace(/(-|_)/g, '')
.replace(/\s+/g, '');
}
3. 생성한 fileList 내보내기
const directoryPath = '../assets/icons'; // VG 파일들이 있는 디렉토리 경로
const outputPath = '../generated/icons'; // 생성된 파일이 저장될 디렉토리 경로
const svgFiles = findSVGFiles(directoryPath); // 변환된 export 구문의 svg file 목록
const svgFiles = [
"export { default as BehaviorOffIcon } from '@assets/icons/bottomTab/behavior-off.svg'",
"export { default as BehaviorOnIcon } from '@assets/icons/bottomTab/behavior-on.svg'",
"export { default as EducationOffIcon } from '@assets/icons/bottomTab/education-off.svg'",
"export { default as EducationOnIcon } from '@assets/icons/bottomTab/education-on.svg'",
"export { default as HomeOffIcon } from '@assets/icons/bottomTab/home-off.svg'",
...
]
아래는 변환한 svgFiles을 지정한 outputPath에 출력하게 되는 코드입니다.
mkdirSync() 함수를 이용해 새로운 지정한 outputPath에 디렉터리를 생성해 줍니다.
// 새로운 디렉토리를 생성합니다.
fs.mkdirSync(outputPath, (err) => {
if (err) throw err;
});
스크립트 실행 시 생성될 디렉터리가 이미 존재한다면 오류가 나므로 이미 존재하면 해당 디렉터리와 하위 디렉터리, 파일을 모두 삭제해 주는 로직을 추가해 줍니다.
// 생성될 디렉토리가 이미 존재한다면, 해당 디렉토리와 하위 디렉토리, 파일을 모두 삭제합니다.
if (fs.existsSync(outputPath)) {
fs.rmSync(outputPath, { recursive: true });
}
fs.mkdirSync(outputPath, (err) => {
if (err) throw err;
});
마지막으로 icons.ts라는 이름으로 파일을 생성하고 지정한 outputPath에 해당 파일을 출력해 줍니다.
// icons.ts 파일을 생성하고, svgFiles의 목록들을 지정한 outputPath에 출력해줍니다.
fs.writeFileSync(outputPath + '/icons.ts', svgFiles.join('\n'), 'utf8');
fs.mkdirSync()
동기적으로 디렉터리(폴더)를 생성하는 데 사용됩니다.
fs.existsSync()
주어진 파일 경로가 존재하는지 확인하는 메서드입니다. 파일 경로가 존재하면 true를, 그렇지 않으면 false를 반환합니다.
fs.rmSync()
주어진 파일 경로에 해당하는 파일이나 디렉터리를 삭제하는 메서드입니다. 이 메서드는 동기식으로 작동하며, 파일 삭제 작업이 완료될 때까지 블로킹됩니다.
fs.writeFileSync()
동기적으로 파일에 데이터를 쓰는 데 사용됩니다.
4. package.json에 script 명령어 추가
작업하면서 svg파일을 추가한 후 터미널에서 gen:icon을 명령하면 지정한 경로에 파일이 올바르게 생성될 것입니다.
{
...
"scripts": {
"gen:icon": "cd src/scripts && node genIcons.js",
},
}
Node.js의 내장 모듈인 fs와 path는 공부 초반 백준으로 자바스크립트 알고리즘을 풀 때 입출력을 설정한 것 이후로는 써본 적이 없었습니다.
직접 작성해 보며 내장모듈의 강력함을 느낌과 동시에 생각보다 파일 및 디렉터리 경로를 다루는 코드를 보다 쉽게 작성할 수 있다는 것을 깨달았습니다.
코드의 구조가 재귀적으로 함수를 호출하 과정에서 어떤 순서로 함수가 호출되고 데이터가 처리되는지 고민하는 데에 시간이 걸렸지만, 함수가 호출되면서 처리되는 데이터를 확인하고 디버깅해 가며 코드의 동작 방식을 이해할 수 있었습니다.
결론적으로 Node.js에서 파일 및 디렉터리 경로를 다루는 방법을 배우고, 문자열 처리에 대한 기본적인 개념을 다시 한번 되새겨볼 수 있어 좋은 시간이었던 것 같습니다.
전체 코드
// src/scripts/genIcons.js
const fs = require('fs');
const path = require('path');
function findSVGFiles(dirPath, fileList) {
fileList = fileList || [];
const files = fs.readdirSync(dirPath);
for (let i = 0; i < files.length; i++) {
const filePath = path.join(dirPath, files[i]);
const fileStat = fs.statSync(filePath);
if (fileStat.isDirectory()) {
findSVGFiles(filePath, fileList);
} else if (path.extname(filePath) === '.svg') {
const baseName = path.basename(filePath, '.svg');
const iconPath = filePath.replace(`../assets/`, '');
const iconName = convertToPascalCase(baseName + 'Icon');
const iconExportPath = `export { default as ${iconName} } from '@assets/${iconPath}'`;
fileList.push(iconExportPath);
}
}
return fileList;
}
const directoryPath = '../assets/icons';
const outputPath = '../generated/icons';
const svgFiles = findSVGFiles(directoryPath);
console.log('fileList', svgFiles);
if (fs.existsSync(outputPath)) {
fs.rmSync(outputPath, { recursive: true });
}
fs.mkdirSync(outputPath, (err) => {
if (err) throw err;
});
fs.writeFileSync(outputPath + '/icons.ts', svgFiles.join('\n'), 'utf8');
function convertToPascalCase(str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word) {
return word.toUpperCase();
})
.replace(/(-|_)/g, '')
.replace(/\s+/g, '');
}