최근 혁신적인 DX 개선 사례들을 유튜브나 컨퍼런스에서 자주 접하게 됩니다.
이 글는 그에 비하면 너무 귀여운 수준이지만 Node.js를 활용해 개발 과정에서 마주치는 작은 불편함들을 개선한 사례를 공유하고자 합니다.
작은 불편함을 해소하는 것부터 시작해도 팀의 생산성과 만족도를 높일 수 있다고 생각합니다!!!
feature/특정 feature명
├── components/ # UI 컴포넌트
│ # 해당 기능에 특화된 재사용 가능한 컴포넌트들을 포함
│
├── constants/ # 상수 정의
│ # 기능별 분리된 상수들을 체계적으로 관리
│
├── models/ # 타입 정의
│ # 인터페이스, API 응답/요청 타입 등 타입 시스템 관리
│
├── pages/ # 페이지 컴포넌트
│ # 라우팅 대상이 되는 페이지 컴포넌트들을 포함
│
├── services/ # 비즈니스 로직
│ # API 통신 로직 및 데이터 처리(query/mutation)를 담당
│
├── hooks/ # 커스텀 훅
│ # 재사용 가능한 로직을 훅으로 모듈화
│
├── store/ # 상태 관리
│ # 기능별 격리된 상태 관리 로직을 포함
│
└── utils/ # 유틸리티
# feature 내 헬퍼 함수들을 모듈화
저희 팀은 feature 기반의 폴더 구조를 사용하고 있습니다. 프로젝트를 초기 단계부터 구축하고 있었기에 신규 feature를 자주 만들어야 했는데요. 매번 이 폴더 구조를 만드는 일이 단순하면서도 반복적이었습니다.
이런 불편함을 느끼던 중 문득 Nest.js의 CLI 도구가 떠올랐습니다. Nest.js를 사용해보신 분들이라면 nest g resource 명령어로 필요한 폴더와 파일을 한 번에 생성했던 경험이 있으실 텐데요.
이전에 이 기능을 사용하면서 느꼈던 편리함이 떠올라, Node.js의 파일시스템을 활용해 비슷한 도구를 만들어보기로 했습니다.
목표는 간단했습니다. 터미널에서 feature 이름만 입력하면 필요한 모든 폴더 구조가 자동으로 생성되도록 만드는 것이었죠.
const fs = require("fs");
const path = require("path");
const createFeatureFolders = (featureName) => {
try {
const featuresDir = path.join(process.cwd(), "src", "features");
const featureDir = path.join(featuresDir, featureName);
const subFolders = ["components", "constants", "models", "pages", "layouts", "services", "hooks", "store", "utils"];
// features 폴더 생성
if (!fs.existsSync(featuresDir)) {
fs.mkdirSync(featuresDir);
}
// 하위 폴더 생성
subFolders.forEach((folder) => {
const subFolderPath = path.join(featureDir, folder);
if (!fs.existsSync(subFolderPath)) {
fs.mkdirSync(subFolderPath);
console.log(`하위 폴더 생성 완료: ${subFolderPath}`);
} else {
console.log(`하위 폴더가 이미 존재합니다: ${subFolderPath}`);
}
});
} catch (e) {
console.error(e.message);
}
};
// 커맨드 라인 인자로 feature 이름 받기
const featureName = process.argv[2];
if (!featureName) {
console.error("신규 기능 이름을 넣어주세요!");
process.exit(1);
}
createFeatureFolders(featureName);
신규 feature를 개발할 때마다 단순 반복 작업에서 벗어나 실제 기능 구현에만 집중할 수 있게 되었습니다.
(mkdir로 하면되지 라고 하신다면 슬퍼요ㅠㅠ)
feature 기반의 폴더 구조를 사용하고 계시다면, 한 번쯤 시도해보시면 좋을 것 같네요!
UI 개발에서 아이콘은 빠질 수 없는 요소입니다. 저희 팀은 SVGR을 활용해 SVG 아이콘들을 React 컴포넌트로 변환하여 사용하고 있는데요. 이 과정에서 두 가지 불편함이 있었습니다.
첫째는 SVG 파일을 직접 import할 때 IDE의 자동 완성 기능을 사용할 수 없다는 점이었고, 둘째는 다크모드 대응을 위해 CSS 변수를 주입해야 하는 번거로움이었습니다.
이 작업을 Node.js를 통해 자동화했습니다. kebab-case로 작성된 SVG 아이콘 파일을 PascalCase 네이밍을 가진 컴포넌트로 변환하고, CSS 변수를 자동으로 주입한 뒤, 배럴 파일까지 자동으로 생성하도록 구현했습니다.
(실제로는 아이콘 별 별도 컴포넌트를 생성하고 추가 작업들이 있지만 코드 소개가 주된 내용이 아니기에 배럴 파일화 하는 로직만 작성했습니다.)
const fs = require("fs");
const path = require("path");
const SVG_DIR = path.join(process.cwd(), "public", "assets", "icons");
const OUTPUT_FILE = path.join(process.cwd(), "src", "shared", "assets", "icons", "index.ts");
// kebab-case를 PascalCase로 변환하는 함수
const toPascalCase = (str) =>
str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
// SVG 파일들을 스캔하고 배럴 파일 생성
const generateBarrelFile = () => {
const svgFiles = fs.readdirSync(SVG_DIR).filter((file) => file.endsWith(".svg"));
const exports = svgFiles.map((file) => {
const componentName = toPascalCase(file.replace(".svg", ""));
return `export { default as ${componentName} } from './${file}';`;
});
fs.writeFileSync(OUTPUT_FILE, exports.join("\n"));
console.log("아이콘 컴포넌트화 완료!");
};
generateBarrelFile();
이렇게 생성된 배럴 파일 덕분에 아이콘을 사용할 때 IDE의 자동 완성 기능을 활용할 수 있게 되었고, 코드의 일관성도 높아졌습니다.
새로운 아이콘이 추가될 때마다 스크립트만 실행하면 되니, 이전보다 훨씬 효율적으로 아이콘을 관리할 수 있게 되었답니다!
최근 외주 프로젝트에 참여하면서 i18n 관리 프로세스의 문제점을 경험했는데요. 상황이 꽤나 복잡했습니다.
디자이너분이 피그마에서 번역기로 임시 번역한 문구를 사용하고 있었고, 이후 엑셀을 통해 문구가 지속적으로 수정되고 있었습니다. 여기에 문구 자체가 변경되어 모든 언어의 번역을 수정해야 하는 경우도 빈번했죠. 게다가 실제로는 문구가 변경됐지만 수정이 필요하다는 표시를 안해주시는 휴먼 에러가 발생하면서 다른 개발 작업의 흐름이 자주 끊기곤 했습니다.
근본적으로는 공유 문서 템플릿 자체의 개선이 필요해보였지만, 당장의 고통을 벗어나기 위해 간단한 자동화를 해보기로 했습니다. XLSX 라이브러리를 활용해 Excel파일을 JSON 파일로 변환하는 자동화하는 스크립트를 작성했습니다.
(실제 문서를 공개할 수 없기에 매우 간략한 버전으로 코드를 작성해보겠습니다!)
const XLSX = require("xlsx");
const fs = require("fs");
const path = require("path");
const EXCEL_FILE = path.join(process.cwd(), "translations.xlsx");
const LOCALES_DIR = path.join(process.cwd(), "public", "locales");
const convertExcelToJson = () => {
try {
// Excel 파일 읽기
const workbook = XLSX.readFile(EXCEL_FILE);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
// 언어별로 데이터 분류
const translations = data.reduce((acc, row) => {
const key = row.Key; // Excel의 Key 열
// 각 언어별로 데이터 정리
Object.entries(row).forEach(([lang, value]) => {
if (lang === "Key") return; // Key일 경우 건너뛰기
if (!acc[lang]) acc[lang] = {};
acc[lang][key] = value;
});
return acc;
}, {});
// 각 언어별 폴더 생성 및 JSON 파일 저장
Object.entries(translations).forEach(([lang, trans]) => {
const langDir = path.join(LOCALES_DIR, lang);
// 언어별 폴더가 없으면 생성
if (!fs.existsSync(langDir)) {
fs.mkdirSync(langDir, { recursive: true });
}
// JSON 파일 생성
const jsonContent = JSON.stringify(trans, null, 2);
fs.writeFileSync(path.join(langDir, "index.json"), jsonContent);
});
console.log("✨ 번역 파일이 성공적으로 생성되었습니다!");
} catch (error) {
console.error("❌ 번역 파일 생성 중 오류가 발생했습니다:", error);
}
};
convertExcelToJson();
번역 파일이 수정될 때마다 스크립트를 실행하기만 하면 되니, 수작업으로 인한 실수를 줄이고 작업 시간도 단축할 수 있게 되었습니다.
물론 임시방편일 수 있지만, 때로는 이런 작은 자동화만으로도 업무 효율을 크게 높일 수 있다는 걸 새삼 깨달았네요!
너무나도 소소한 개선이지만, 이런 작은 변화들이 생각보다 핵심 작업에 더 집중할 수 있게 할 수 있는 경험들이였습니다. 특히 Node.js의 file system 모듈과 같은 기본적인 도구만으로도 충분히 의미 있는 DX 개선을 이룰 수 있다는 점이 인상적이었습니다.
처음에는 단순히 '귀찮은 일을 줄여보자'는 생각으로 시작했지만, 반복적인 작업에서 벗어나 본연의 문제 해결에 집중할 수 있게 되었고, 휴먼 에러도 줄어들었습니다.
이런 작은 자동화의 경험들이 모여 다른 불편한 지점들도 개선할 수 있다는 생각이 자연스럽게 들게 되었고, 팀원들과 함께 더 나은 개발 환경을 만들어가는 여정이 즐거워졌습니다.
혹시 이 글을 읽으시는 분들은 어떤 방식으로 개발 과정의 불편함을 해소하고 계신가요?
여러분들의 개선 경험이 궁금합니다!!!🙂