회사에서 피그마 변환 프로젝트를 수행하며
피그마 변환 -> HTML 파일로 저장 하는 과정에서 fs 모듈을 많이 사용했다.
그 중 가장 기초가 되고 많이 호출한 메서드를 정리해보려 한다.
(거의 비동기로 작성했다.)
0. 경로 생성
fs 모듈을 사용하기 위해서는 경로값이 필요하다.
노드 서버를 실행하는 컴퓨터마다 서버 파일의 위치가 달라질 수 있으므로,
1) fileURLToPath(import.meta.url) -- 실행 컴퓨터의 현재 파일의 절대경로를 먼저 추출하고
2) path.dirname(절대경로) -- 추출한 경로에서 현재 위치의 파일만 추출한 뒤
3) path.join(현재위치, '목표파일상대경로') -- 현재 위치를 기준으로 상대 경로를 찾는다.
import fs from "fs/promises";
import path from "path";
import axios from "axios";
import * as cheerio from 'cheerio';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); // 현재 파일의 절대 경로 (사용자 별 상이함)
const __dirname = path.dirname(__filename); // 디렉토리 이름만 추출
const directoryPath = path.join(__dirname, '../f_screen'); // 디렉토리 + 상대경로
__filename == /Users/my-name/projects/my-app/index.js
__dirname == index.js
directoryPath == /Users/my-name/projects/my-app/f_screen
1. fs.promises.readdir -- 디렉토리 파일 유무 체크 & 반환
앞단에 파일 리스트를 전달해야 할 때
디렉토리에 파일이 있는지 체크하고, 있으면 파일 리스트를 전달한다.
나같은 경우는 체크할 디렉토리가 하나밖에 없어서 바로 집어넣었지만,
경로가 여러개일 경우 파라미터로 받아서 넣어주거나 지역변수를 여러개 선언해놓으면 된다.
/**
* 파일 유무 체크
* @returns {Promise<string[]>}
*/
export async function checkFile() {
try {
let files = await fs.readdir(directoryPath);
return files;
} catch (err) {
console.error('Error --fs.promises.readdir :', err);
return [];
}
}
2. fs.promises.stat -- 파일 정보 리턴
파일 이름에 디렉토리 경로를 붙여 파일 정보값을 추출해 반환한다.
추가로 같이 전달할 정보가 있을 경우 이 메서드를 수정하면 간편하다.
/**
* 디렉토리에서 파일 정보를 리턴
* @param {*} files
* @returns
*/
export async function returnFileList(files) {
// Promise.all --> 비동기적으로 모든 파일의 정보를 가져옴
let fileDetails = await Promise.all(
files
.filter(file => file.endsWith(".html")) // .html 파일만 필터링
.map(async (file) => {
const filePath = path.join(directoryPath, file);
try {
const stats = await fs.stat(filePath);
return {
name: file, // 파일 이름
path: filePath, // 저장 경로
time: stats.mtime, // 저장 시간
};
} catch(statErr) {
console.error(`Error -- fs.stat ${file}:`, statErr);
return null;
}
})
);
// null이 아닌 결과만 필터링하여 반환
return fileDetails.filter((details) => details !== null);
}
두 기능을 조합하면
클라이언트에게 파일 정보가 담긴 리스트를 전달할 수 있다.
/**
* f_screen 디렉토리 파일 유무 체크 & 있으면 리턴
*/
app.get('/checkFile', async (req, res) => {
console.log("Client is requesting checking the HTML files");
try {
let files = await checkFile();
if (files && files.length > 0) {
files = await returnFileList(files);
console.log("checkFile success");
res.send({ files: files });
} else {
console.log("checkFile failed");
res.send({ message: "Nothing in directory"});
}
} catch (err) {
console.log(err);
res.status(500).send(err.message);
}
});
3. fs.promises.writeFile -- 파일 저장
파일이 없으면 만들고, 있으면 내용을 업데이트 한다.
가장 많이 쓰인 기능.
/**
* 파일 저장
* @param {String} path
* @param {String} data
*/
export async function writeFile(path, data) {
try {
await fs.writeFile(path, data, 'utf8');
} catch (err) {
console.error(err);
}
}
4. fs.promises.readFile -- 파일 내용 읽기
'utf-8' 로 지정해주지 않으면 이상한 숫자들의 나열로 파일 내용을 읽어올거다.,,, 꼭 지정해주기.
/**
* 파일 내용 읽기
* @param {String} path
* @returns
*/
export async function readFile(path) {
try {
const data = await fs.readFile(path, 'utf-8');
return data;
} catch (error) {
if (error.code === 'ENOENT') {
return [];
} else {
console.error('Error reading File:', error);
throw error;
}
}
}
위의 기능을 활용하면
디렉토리에서 파일 내용을 읽어와서 수정하는 기능을 구현할 수 있다.
참고로, 내가 불러올 파일은 HTML 로 되어있기 때문에,
내용을 수정하려면 Dom으로 변환할 필요가 있어서 cheerio 를 이용했다.
(+) cheerio -- html 텍스트를 객체화하는 모듈
이 메서드는 화면 이름(screenName) 과 { 객체 id : 속성값 } 으로 되어있는 controlTypes 를 인자로 받아서
화면 이름으로 경로를 만들고 파일 내용을 읽어와
객체화 시킨 뒤
id 로 수정이 필요한 객체를 찾아서 속성을 업데이트한다.
export async function editControlType(screenName, controlTypes) {
try {
const screenDataPath = path.join(directoryPath, `${screenName}.html`);
const data = await readFile(screenDataPath);
if (data.length > 0) {
// HTML DOM parsing
const $ = cheerio.load(data);
// controltype 업데이트
const updatePromises = Object.entries(controlTypes).map(async ([elementId, newControlType]) => {
const $element = $(`#${elementId}`);
if ($element.length > 0) {
$element.attr('controltype', newControlType);
console.log(`Updated ID ${elementId} to ${newControlType}`);
} else {
console.log(`ID ${elementId} not found.`);
}
});
await Promise.all(updatePromises);
await writeFile(screenDataPath, $.html());
console.log(`Updated HTML file saved: ${screenDataPath}`);
} else {
console.log(`File ${screenName}.html is empty or not found.`);
}
} catch (error) {
console.error(`Error reading or modifying file ${screenName}.html:`, error);
}
}
5. fs.rename -- 파일 이름 변경
지금은 프로젝트 요구사항에서 빠지게 되어 사용하지 않는 메서드가 되었지만,
구현해둔 코드가 있어서 기록으로 남긴다.
/**
* 파일 name & id 변경
* @param {String} oldName
* @param {String} newName
*/
export async function editFileName(oldName, newName) {
try {
const oldFilePath = path.join(directoryPath, `${oldName}.html`);
const newFilePath = path.join(directoryPath, `${newName}.html`);
await fs.rename(oldFilePath, newFilePath);
console.log(`File renamed from ${oldName} to ${newName}`);
} catch (err) {
console.error('Error renaming file:', err);
throw err;
}
}
노드 공식 문서를 보면 최신 문법으로 작성된 코드를 볼 수 있다.
최신문법으로도 리팩토링해보고 싶다.
https://nodejs.org/docs/latest-v20.x/api/fs.html#fspromisesreadfilepath-options