이전 단계에서 프로젝트의 설정이 끝났다.
이제는 요구사항에 맞게 만들면 된다.
이 시리즈의 글은 개발 과정을 작성한 것으로 내용 중 코드 또는 판단이 이상한 부분이 있다면 높은 확률로 뒷부분에서 수정될 것입니다.
Cli 기반의 프로그램
cli 기반의 프로그램을 실행할 때 아래와 같이 입력하는 경우가 많다.
<프로그램> --option1 option1 --option2 option2
Node.js에서는 위에서 option1과 같은 프로그램 실행 인자를 process를 이용해 가져올 수 있다.
#!/usr/bin/env node
console.log(process.argv);
위와 같이 process.argv의 내용을 살펴보면 아래와 같은 결과를 확인할 수 있다.
첫 번째 요소는 해당 파일을 실행하는 nodejs의 경로, 두 번째 요소는 프로그램이 실행된 파일의 위치에 관한 정보다.
세 번째 인자부터 사용자가 프로그램을 실행할 때 전달하는 인자가 담기게 된다.
문제1: 경로 별칭
가장 먼저 마주친 문제는 경로 별칭이다.
tsconfig.json 또는 jsconfig.json에는 경로 별칭을 설정할 수 있다.
baseUrl을 이용하여 프로젝트 내에서 경로의 기준을 정할 수 있고, paths를 이용하면 경로에 별칭을 설정할 수 있다.
"paths": {
"@/*": ["*"],
"@/components": ["components/*"],
"@/constants": ["constants/*"],
"@/hooks": ["hooks/*"],
"@/pages": ["pages/*"],
"@/stores": ["stores/*"],
"@/styles": ["styles/*"],
"@/types": ["types/*"],
"@/utils": ["utils/*"],
"@/assets": ["assets/*"]
}
위와 같은 경로 별칭들이 설정되어 있는 경우 import문을 그대로 가져와서 읽는다면 해당 위치의 파일이 없다고 판단할 가능성이 있다.
해결
이를 해결하기 위한 방법으로 tsconfig.json 또는 jsconfig.json이 프로젝트에 포함되어 있는 경우는 해당 파일의 내용을 읽을 수 있도록 했다.
해당 파일 자체를 읽어 json 데이터로 만들고 이를 활용하면 경로 별칭을 해결할 수 있을 것으로 생각했다.
하지만 또 다른 문제는 tsconfig.json과 jsconfig.json에는 주석이 들어갈 수 있다는 점이다.
fs로 읽어온 파일의 내용(string)에 JSON.parse를 써서 각 파일을 json으로 변환하고자 했지만 주석이 있는 경우 제대로 작동하지 않는 문제가 있었다.
이를 해결하기 위해 json 내용에 주석이 있어도 parse할 수 있는 json5 라이브러리를 활용하게 되었다.
import fs from 'node:fs';
import JSON5 from 'json5';
export function getConfigFile() {
try {
const tsconfig = fs.readFileSync('tsconfig.json', 'utf-8');
return JSON5.parse(tsconfig);
} catch {
try {
const jsconfig = fs.readFileSync('jsconfig.json', 'utf-8');
return JSON5.parse(jsconfig);
} catch {
return undefined;
}
}
}
해당 라이브러리를 활용한다면 주석이 포함된 json형식 문자열도 json 객체로 변환할 수 있다.
root 파일 분석
이제 경로 별칭에 관한 정보를 얻었으니 위의 데이터를 활용한다면 경로 별칭이 적용되어 있더라도 일반 경로로 변환할 수 있다.
import fs from 'node:fs';
import parser from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
export function getImportPaths(filePath) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const babelOptions = {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
};
const ast = parser.parse(fileContent, babelOptions);
const importPaths = [];
traverse(ast, {
ImportDeclaration({ node }) {
importPaths.push(node.source.value);
},
});
return importPaths;
}
이제 실제 파일의 AST를 만들고 이를 분석하여 import에 해당하는 내용들을 배열로 만들어야 한다.
AST를 생성하기 위해 babel을 활용하였으며 React와 TypeScript를 지원하기 위해 plugin에 설정해준다.
AST의 순회의 경우 babel의 traverse 라이브러리를 활용하기로 했다.
여기서 라이브러리를 도입하고자 하는 것은 이 프로젝트는 import 계층을 시각화 하는 것이 목표기 때문이다.
AST는 목표를 달성하고자 하는 수단이며 AST 생성에 관한 내용과 AST 순회에 관한 내용을 모두 만들기에는 프로젝트의 주제에서 벗어난다고 판단했다.
(AST를 만드는 ast-generator와 같은 프로젝트라면 직접 구현하는 것이 더 도움이 되었을 것이다)
traverse를 이용해 AST를 순회하면서 타입이 ImportDeclaration인 토큰들의 value을 가져온 뒤 배열로 만들어 내보내는 역할을 수행한다.
특이하게도 babel에서 traverse를 import 하여 사용하는 과정에서 문제가 발생한다.
babel팀에서 esm방식은 위와 같이 사용할 것을 제안하고 있었다.
import-visualizer ./src/App.tsx
위와 같은 명령어를 통해 실행 결과를 확인해보면 사진과 같은 배열을 얻을 수 있다.
import path from 'node:path';
function pathMapping(aliasPath, baseUrl, paths) {
for (const [alias, targets] of Object.entries(paths)) {
const aliasPrefix = alias.replace('*', '');
if (aliasPath.startsWith(aliasPrefix)) {
const targetPrefix = targets[0].replace('*', '');
const resolvedPath = aliasPath.replace(aliasPrefix, targetPrefix);
return path.join(baseUrl, resolvedPath);
}
}
return aliasPath;
}
export function resolveImportPaths(imports, baseUrl, paths) {
return imports.map((importPath) => pathMapping(importPath, baseUrl, paths));
}
경로 별칭의 해제를 위해 위와 같은 함수를 만들게 되었다.
간단하게 경로 별칭을 구성하는 baseUrl에 대해 *을 빈 문자열로 바꾸고 paths의 key에 포함된 *도 마찬가지로 빈 문자열로 바꾼다.
이후 key에 해당하는 내용으로 시작된 경로가 있다면 paths배열의 첫 번째 요소로 바꾸는 것이다.
위 함수까지 적용하고 난 뒤의 모습은 아래와 같다.
(비교를 위해 전과 후를 함께 표시)
이제 경로 별칭이 지정된 파일의 실제 경로를 가져올 수 있게 되었다.
[참고자료]
'개발 > 개발과정' 카테고리의 다른 글
[import-visualizer] 4. 시각화 (0) | 2024.06.14 |
---|---|
[import-visualizer] 3. 확장자 (0) | 2024.06.14 |
[import-visualizer] 1. 프로젝트 시작 (요구사항 분석) (0) | 2024.06.13 |
React createElement로 리팩토링 하기 (권장 X) (0) | 2024.05.04 |
[AlgoITNi] 홈 화면 성능 개선하기: 7. 최종 결과 (0) | 2024.04.02 |