지난 달 Tidy First?를 읽고 코드 레벨에서의 코드 정리법에 대해 간단하게 작성해 보았습니다. 몇 가지 유용한 코드 정리 방법을 익혔지만, 실제로 코드 정리가 어려운 이유는 단순히 방법을 모르기 때문이 아니라, 지속적으로 새로운 코드가 코드베이스에 추가되고 기존 코드에 덧붙여지는 과정에서 어떻게 코드 정리를 계획하고 관리해야 하는지에 대한 어려움 때문이었습니다.
실제로 개발을 할 때는 새로운 기능 추가와 버그 수정이 끊임없이 이루어지며, 그 과정에서 코드베이스는 복잡해지고 유지보수가 어려워집니다. 이러한 상황에서 효율적으로 코드 정리를 수행하기 위해서는 체계적인 계획과 관리가 필수적입니다.
이번 글에서는 'Tidy First?'의 두 번째 파트인 '관리'를 읽고, 코드 정리 과정을 어떻게 계획하고 관리할 수 있는지에 대해 제 의견을 더해 정리해 보았습니다. 또한 이를 실천하기 위한 몇 가지 액션 아이템도 함께 소개하려 합니다.
코드 정리를 처음 배우는 개발자들은 종종 코드 정리와 기능 추가를 동시에 진행하다가 예상치 못한 어려움에 부딪히곤 합니다. 이러한 상황에서 코드 정리를 어떻게 계획하고 실행해야 하는지, 구체적인 예시를 통해 단계별로 알아보겠습니다.
약 300줄에 달하는 코드가 있다고 가정해 봅시다. 이 코드는 openapi-generator-cli
를 사용하여 OpenAPI 명세를 기반으로 TypeScript 클라이언트를 생성하는 스크립트이며, 여러 기능이 하나의 파일에 복잡하게 얽혀 있습니다. 여기에 msw-auto-mock
을 추가하여 API 모킹 기능을 구현하려고 합니다.
// OpenAPI 명세를 기반으로 TypeScript 클라이언트를 생성하는 스크립트
const { exec } = require('child_process');
const fs = require('fs');
function main() {
let specUrl = 'https://api.example.com/openapi.yaml';
let outputDir = './generated-client';
// 폴더를 만드는 로직
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
// OpenAPI 명세를 가져오는 로직
exec(`curl -o openapi.yaml ${specUrl}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error fetching OpenAPI spec: ${error}`);
return;
}
// 클라이언트 코드 생성하는 로직
exec(
`openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ${outputDir}`,
(error, stdout, stderr) => {
if (error) {
console.error(`Error generating client code: ${error}`);
return;
}
// 파일 입력 처리를 하는 로직
fs.copyFileSync('./custom-config.ts', `${outputDir}/config.ts`);
// 더는 필요 없는 파일을 삭제하는 로직
fs.unlinkSync('openapi.yaml');
// 기타 방어 처리들이 혼합되어 있음
console.log('TypeScript client generated successfully.');
}
);
});
// 에러 처리가 부족하고 코드가 중첩되어 있음
}
main();
specUrl
, outputDir
등).msw-auto-mock
기능 추가).이러한 상태에서 msw-auto-mock
기능을 추가하려고 하니 코드가 더욱 복잡해지기 때문에 기능 추가와 코드 정리를 동시에 진행하고 싶은 욕심에 사로잡히게 됩니다.
// OpenAPI 명세를 기반으로 TypeScript 클라이언트를 생성하고 MSW 모킹 핸들러를 생성하는 스크립트
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const SPEC_URL = 'https://api.example.com/openapi.yaml';
const OUTPUT_DIR = './generated-client';
async function main() {
try {
createOutputDirectory(OUTPUT_DIR);
await fetchOpenAPISpec(SPEC_URL);
await generateClientCode('openapi.yaml', OUTPUT_DIR);
await generateMswMocks('openapi.yaml', './msw-mocks'); // 새로운 기능 추가
copyCustomConfig('./custom-config.ts', `${OUTPUT_DIR}/config.ts`);
cleanUpFiles(['openapi.yaml']);
console.log('TypeScript client and MSW mocks generated successfully.');
} catch (error) {
console.error('Error:', error);
}
}
function createOutputDirectory(dir) {
// ...
}
function fetchOpenAPISpec(url) {
// ...
}
function generateClientCode(specFile, outputDir) {
// ...
}
function copyCustomConfig(src, dest) {
// ...
}
function cleanUpFiles(files) {
// ...
}
function generateMswMocks(specFile, outputDir) {
// ...
}
main();
겉보기에는 코드가 멀쩡해 보이지만, 코드 변경 내역(Diff)을 살펴보면 리뷰어가 변경 사항의 맥락을 파악하기 어렵다는 문제가 있습니다.
결과적으로 코드가 개선되었지만, 리뷰어들은 변경 사항의 의도를 파악하기 어려워집니다.
아래의 코드 변경 내역(Diff)을 보면, 코드의 변경 범위가 광범위하고, 여러 종류의 변경 사항이 한꺼번에 이루어져 리뷰어가 어떤 부분이 코드 정리이고, 어떤 부분이 기능 추가인지 구분하기 어렵습니다.
이로 인해 리뷰어들은 이 PR을 빠르게 검토하지 못하고, 코드 병합이 지연될 수 있습니다. 더 꼼꼼한 리뷰어들은 PR을 다시 올리라고 요청하거나(Re-request), 심지어는 PR을 닫아버리는 경우도 발생할 수 있습니다.
그렇다면 이러한 PR을 어떻게 다루는 것이 좋을까요? 효율적인 코드 정리와 기능 추가를 위해서는 어떤 접근 방법이 필요할까요?
효율적인 코드 정리와 기능 추가를 위해서는 변경 사항을 명확히 구분하고, 올바른 순서로 진행하며, 작은 단위로 관리하는 것이 중요합니다. 이를 통해 코드베이스의 품질을 높이고, 팀의 생산성을 향상시킬 수 있습니다.
먼저 코드 정리를 선행하여 코드의 구조를 개선한 후, 새로운 기능을 추가합니다. 이렇게 하면 리뷰어들이 각 변경 사항의 의도와 목적을 명확히 이해할 수 있으며, 검토 과정도 훨씬 수월해집니다.
변경 사항을 작은 단위로 나누어 PR을 분리하면, 리뷰어들이 변경의 의도를 명확히 이해할 수 있습니다. 이는 코드 리뷰의 효율성을 높이고, 피드백 반영도 용이하게 합니다.
먼저 코드 정리를 선행하여 코드의 구조를 개선한 코드입니다. 이렇게 하면 기능 추가 전 리뷰어들이 변경 사항의 의도와 목적을 명확히 이해할 수 있으며, 검토 과정도 훨씬 수월해집니다.
// 코드 정리만 수행한 개선된 코드 예시
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const SPEC_URL = 'https://api.example.com/openapi.yaml';
const OUTPUT_DIR = './generated-client';
async function main() {
try {
createOutputDirectory(OUTPUT_DIR);
await fetchOpenAPISpec(SPEC_URL);
await generateClientCode('openapi.yaml', OUTPUT_DIR);
copyCustomConfig('./custom-config.ts', `${OUTPUT_DIR}/config.ts`);
cleanUpFiles(['openapi.yaml']);
console.log('TypeScript client generated successfully.');
} catch (error) {
console.error('Error:', error);
}
}
function createOutputDirectory(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
}
function fetchOpenAPISpec(url) {
return new Promise((resolve, reject) => {
exec(`curl -o openapi.yaml ${url}`, (error) => {
if (error) {
reject(`Error fetching OpenAPI spec: ${error.message}`);
} else {
resolve();
}
});
});
}
function generateClientCode(specFile, outputDir) {
return new Promise((resolve, reject) => {
exec(
`openapi-generator-cli generate -i ${specFile} -g typescript-axios -o ${outputDir}`,
(error) => {
if (error) {
reject(`Error generating client code: ${error.message}`);
} else {
resolve();
}
}
);
});
}
function copyCustomConfig(src, dest) {
fs.copyFileSync(src, dest);
}
function cleanUpFiles(files) {
files.forEach((file) => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
});
}
main();
미리 정리해 둔 코드베이스에 함수 하나와 main
함수에 한 줄만 추가하면 됩니다.
async function main() {
try {
createOutputDirectory(OUTPUT_DIR);
await fetchOpenAPISpec(SPEC_URL);
await generateClientCode('openapi.yaml', OUTPUT_DIR);
await generateMswMocks('openapi.yaml', './msw-mocks'); // 새로운 기능 추가
copyCustomConfig('./custom-config.ts', `${OUTPUT_DIR}/config.ts`);
cleanUpFiles(['openapi.yaml']);
console.log('TypeScript client and MSW mocks generated successfully.');
} catch (error) {
console.error('Error:', error);
}
}
// ...
function generateMswMocks(specFile, outputDir) {
return new Promise((resolve, reject) => {
exec(
`msw-auto-mock -i ${specFile} -o ${outputDir}`,
(error) => {
if (error) {
reject(`Error generating MSW mocks: ${error.message}`);
} else {
resolve();
}
}
);
});
}
이렇게 코드 정리와 기능 추가를 분리하여 진행함으로써, 리뷰어들은 변경 사항의 의도를 명확히 파악할 수 있고, 검토 과정도 훨씬 효율적으로 만들 수 있습니다.
코드 정리와 기능 추가를 동시에 진행하면 일시적으로 개발 속도가 빨라지는 것처럼 보일 수 있지만, 장기적으로는 유지보수성과 코드 품질에 악영향을 미칩니다. 특히, 팀원들과의 협업 환경에서는 이러한 방식이 커뮤니케이션 비용 증가와 프로젝트 지연으로 이어질 수 있습니다.
따라서 위와 같은 예시를 통해 변경 사항을 명확히 구분하고, 올바른 순서로 진행하며, 작은 단위로 관리하는 것의 중요성을 체감할 수 있습니다. 이를 통해 코드베이스의 품질을 높이고, 팀의 생산성을 향상시킬 수 있습니다.
이러한 원칙을 잘 지키기 위해 도움이 되는 다음과 같은 액션 아이템을 제안합니다.
tidy
라벨을 추가하여 리팩터링보다 작은 규모의 코드 정리임을 명시합니다. 이를 통해 리뷰어들이 PR의 목적을 빠르게 이해할 수 있고, 코드 정리에 대한 의사소통 비용을 줄일 수 있습니다.tidy
라벨을 붙여 관리하고 있으며, 이를 통해 리뷰어들이 변경 사항의 의도를 빠르게 파악하고 있습니다.변경 사항의 종류를 명확히 구분하기
작은 단위로 PR을 만들기
팀 내에서 합의된 코드 정리 규칙을 수립하기
코드 정리와 기능 추가의 순서를 전략적으로 결정하기
코드 정리 목록을 관리하기
'Tidy First?'의 두 번째 파트인 '관리'를 읽고, 코드 정리를 어떻게 계획하고 관리할 수 있는지에 대해 제 의견과 경험을 더해 정리해 보았습니다. 책에서 제시한 원칙들을 실제 개발 현장에서 적용해 보니, 코드 정리와 기능 추가를 명확히 구분하고 작은 단위로 관리하는 것이 얼마나 중요한지 다시 한 번 깨닫게 되었습니다.
코드 정리는 단순히 코드를 깔끔하게 만드는 것을 넘어, 팀의 생산성을 높이고 소프트웨어의 품질을 향상시키는 핵심적인 과정입니다. 그러나 코드 정리와 기능 추가를 동시에 진행하면 혼란을 야기하고 협업에 어려움을 초래할 수 있습니다.
이번 글에서는 코드 정리와 기능 추가를 명확히 구분하고, 올바른 순서로 진행하며, 작은 단위로 관리하는 것의 중요성을 살펴보았습니다. 구체적인 예시를 통해 코드 정리를 선행하고 기능 추가를 별도의 PR로 분리함으로써 리뷰어들이 변경 사항의 의도를 명확히 파악하고, 검토 과정을 원활하게 진행할 수 있음을 보여드렸습니다.
또한, 팀 내에서 코드 정리에 대한 공통된 규칙과 절차를 수립하고, 코드 정리의 시점을 전략적으로 결정하는 것이 중요하다는 것을 강조했습니다. 상황에 따라 코드 정리를 선행할지, 동작 변경 후에 진행할지 판단하여 효율적인 개발 프로세스를 구축할 수 있습니다.
앞으로 코드베이스를 관리할 때, 이 글에서 소개한 원칙과 방법들을 적용해 보시길 권장합니다. 코드 정리를 체계적으로 계획하고 관리함으로써, 더 효율적이고 건강한 개발 문화를 만들어갈 수 있을 것입니다.
코드에 새로운 기능을 추가하거나 버그를 수정할 때, 이번에 소개한 방법으로 코드 정리를 관리해보면 어떨까요?
긴 글 읽어주셔서 감사합니다.
정리를 너무 잘해주셔서 이 책은 저도 사서 한번 읽어보고 싶어지네요!
잘 읽었습니다!