이제 모든 데이터를 다 마련했고 시각화 수단도 마련했다.
시각화를 위한 데이터를 만들기만 한다면 시각화를 할 수 있을 것으로 생각한다.
이 시리즈의 글은 개발 과정을 작성한 것으로 내용 중 코드 또는 판단이 이상한 부분이 있다면 높은 확률로 뒷부분에서 수정될 것입니다.
재귀함수
트리의 순회, 생성에 있어서 가장 먼저 떠오른 방법이 재귀함수였다.
재귀적 특성을 보이며 자식을 따라 leaf까지 재귀적으로 작동하기 때문이다.
간단하게 재귀함수를 만들어보면 아래와 같다.
import { appendExtensions } from './appendExtensions.js';
import { getAllFiles } from './getAllFiles.js';
import { getImportPaths } from './getImportPaths.js';
import { resolveImportPaths } from './resolveImportPaths.js';
export class FileTree {
constructor(root, targetDir, baseUrl, paths) {
this.root = root;
this.baseUrl = baseUrl;
this.paths = paths;
this.allFiles = getAllFiles(targetDir);
this.tree = {};
this.init();
}
init() {
this.tree = this.createNode(this.root);
this.generateTree(this.tree);
}
createNode(filePath) {
return {
name: filePath,
attributes: '',
children: [],
};
}
generateTree(node) {
const imports = getImportPaths(node.name);
const resolvedPath = resolveImportPaths(imports, this.baseUrl, this.paths);
const resolvedPathWithExtensions = appendExtensions(resolvedPath, this.allFiles);
resolvedPathWithExtensions.forEach((path) => {
if (path === undefined) return;
const newNode = this.createNode(path);
node.children.push(newNode);
this.generateTree(newNode);
});
}
}
(재귀함수와 관련된 스택 오버플로우 문제는 추후 해결될 예정입니다)
위와 같이 함수를 구성하고 generateTree를 재귀적으로 호출하는 형태가 된다.
위의 클래스를 인스턴스로 만들게 되면 generateTree가 실행되고 tree에 접근하면 실제 트리 데이터를 가져올 수 있다.
import fs from 'node:fs';
import { resolve } from 'node:path';
const template = (data) =>
`<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Import-Visualizer</title>
</head>
<body>
<div id="root"></div>
</body>
<script>const data = ${data}</script>
<script src="./dist/lib/tree.js"></script>
</html>
`;
export function generateTemplate(data) {
fs.writeFileSync(resolve(process.argv[1], '../../index.html'), template(JSON.stringify(data)));
}
이제 받아온 데이터를 예전에 만들었던 템플릿에 삽입하면 데이터를 보여줄 수 있을 것이다.
#!/usr/bin/env node
import open from 'open';
import { resolve } from 'node:path';
import { cliConfig } from '../src/cliConfig.js';
import { getConfigFile } from '../src/getConfigFile.js';
import { FileTree } from '../src/FileTree.js';
import { generateTemplate } from '../src/generateTemplate.js';
const { root, targetDir } = cliConfig(process.argv.slice(2));
const { compilerOptions } = getConfigFile();
const { baseUrl, paths } = compilerOptions;
const fileTree = new FileTree(root, targetDir, baseUrl, paths);
generateTemplate(fileTree.tree);
await open(resolve(process.argv[1], '../../index.html'));
최종적인 코드의 모습은 위와 같으며 실행한 결과는 아래와 같다.
문제: 레이아웃
파일의 이름이 너무 길기 때문에 레이아웃이 깨지는 문제가 발생했다.
이를 해결하기 위해 node 인터페이스에서 사용하지 않았던 attributes 속성을 활용하기로 했다.
generateTree(node) {
const filePath = `${node.attributes.dir}/${node.name}`;
const imports = getImportPaths(filePath);
const resolvedPath = resolveImportPaths(imports, this.baseUrl, this.paths);
const resolvedPathWithExtensions = appendExtensions(resolvedPath, this.allFiles);
resolvedPathWithExtensions.forEach((path) => {
if (path === undefined) return;
const [newFilePath, newFileName] = this.splitFilePath(path);
const newNode = this.createNode(newFileName, newFilePath);
node.children.push(newNode);
this.generateTree(newNode);
});
}
splitFilePath(filePath) {
const splitPath = filePath.split('/');
const fileName = splitPath.pop();
const path = splitPath.join('/');
return [path, fileName];
}
generateTree를 수정하여 name에는 파일의 이름과 확장자만 들어가고 나머지 경로는 attributes의 dir 속성으로 설정하도록 구성했다.
조금 나아진 모습을 보이지만 아직도 여전히 겹치는 것을 확인할 수 있다.
이를 해결하기 위해서는 react-d3-tree에서 제공하는 옵션을 설정하면 된다.
import React from 'react';
import ReactDOM from 'react-dom';
import Tree from 'react-d3-tree';
function App() {
return (
<div style={{ width: '98vw', height: '98vh' }}>
<Tree data={data} pathFunc='step' orientation='vertical' translate={{ x: 500, y: 250 }} separation={{ nonSiblings: 5, siblings: 5 }} />
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
seperator 옵션을 조정하면서 적정 값을 찾은 결과 3정도가 괜찮기에 3으로 설정하게 되면 아래와 같은 결과를 얻을 수 있다.
이제 기능은 어느정도 완성이 된 것 같으니 예외 처리를 추가하고 라이브러리로 배포하면 될 것 같다.
여담: 파일 삭제
index.html이 프로젝트 내에 남는 것을 원치 않았기 때문에 브라우저에서 렌더링 된 이후 fs 모듈의 unlink를 이용해서 파일을 지울 수 있도록 구성했다.
이미 파일이 열린 뒤에 지우기 때문에 트리를 보는데는 문제가 없었다.
#!/usr/bin/env node
import open from 'open';
import { resolve } from 'node:path';
import fs from 'node:fs/promises';
import { cliConfig } from '../src/cliConfig.js';
import { getConfigFile } from '../src/getConfigFile.js';
import { FileTree } from '../src/FileTree.js';
import { generateTemplate } from '../src/generateTemplate.js';
const { root, targetDir } = cliConfig(process.argv.slice(2));
const { compilerOptions } = getConfigFile();
const { baseUrl, paths } = compilerOptions;
const fileTree = new FileTree(root, targetDir, baseUrl, paths);
generateTemplate(fileTree.tree);
const resultFilePath = resolve(process.argv[1], '../../index.html');
await open(resultFilePath, { wait: true });
console.log('Tree has been created. Check your browser.');
await fs.unlink(resultFilePath);
'개발 > 개발과정' 카테고리의 다른 글
[import-visualizer] 8. tsc의 한계 및 배포 (0) | 2024.06.14 |
---|---|
[import-visualizer] 7. 번들링(빌드) (0) | 2024.06.14 |
[import-visualizer] 5. CDN의 한계 (0) | 2024.06.14 |
[import-visualizer] 4. 시각화 (0) | 2024.06.14 |
[import-visualizer] 3. 확장자 (0) | 2024.06.14 |