구글 개발자가 인정한 성능 최적화 기여 후기 - AI와 함께한 gemini-cli 오픈소스 여정
2025-09-07 · 24 min
출시 직후의 gemini-cli 코드베이스를 AI와 함께 뜯어 동기 파일 처리를 병렬로 바꿨고, 408ms를 107ms로 줄였습니다. 기여할 지점을 찾는 프롬프트부터 구글 개발자에게 리뷰받으며 PR을 머지하기까지의 전략.
이 글을 읽고 나면
AI와 협업해서 오픈소스 기여하는 방법을 알게 됩니다
성능 최적화 PR이 더 좋은 평가를 받는 이유를 이해합니다
구글같은 대기업 프로젝트에 기여하는 전략을 배웁니다
실제 머지된 PR 사례를 통해 구체적인 노하우를 얻습니다
들어가며
"내 코드를 구글의 프로젝트에 넣을 수 있다니!"
2025년 6월 25일 gemini-cli가 출시되었습니다. CLI 환경의 AI 도구들이 쏟아져 나오는 요즘, 이 프로젝트가 특별했던 이유는:
JavaScript 기반: 익숙한 기술 스택
화제성: 출시 직후부터 관심 집중
작은 코드베이스: 아직 분석 가능한 규모
개선 여지: 급하게 만들어진 티가 여기저기 보임
적극적인 오픈소스: 전세계 개발자들의 참여를 적극 유도
무엇보다 구글러들에게 직접 코드리뷰를 받을 수 있다는 점이 가장 매력적이었습니다.
기여할 것을 찾아보자
설레는 마음으로 코드베이스를 뜯어보기 시작했습니다.
ai와 함께 코드베이스를 분석하고 기여할 점을 부탁 하였습니다.
ex) 사용했던 프롬프트 User
여기는 gemini-cli 프로젝트예요. 함께 개선할 점을 찾아봐요!
AI Assistant
read-many-files.ts에서 파일 처리가 동기적으로 이뤄지고 있어요. 개선 방향은 아래와 같아요:
for...of 순회를 Promise 목록 기반 병렬 처리로 전환
detectFileType 함수를 비동기로 변경
User
개선할 코드 제안해주고 이게 어떤 이점을 가져오는지 설명해줘
AI Assistant
(변경 코드 보여주고 설명 하는 중)
이 내용을 보았을 성능관련 기여를 할 수 있다고 생각을 했고 메인테이너의 입장에서는
새로운 기능을 추가하는 PR은 메인테이너 입장에서 고려할 게 많습니다.
프로젝트의 방향성과 맞는지, 다른 기능과 충돌은 없는지, 앞으로 유지보수 비용은 어떨지 등등... 하지만 성능 개선 PR은 빚을 갚아주는 것과 같습니다.
누구도 마다할 이유가 없죠. 프로젝트를 더 건강하게 만드는, 가장 환영받는 기여 중 하나입니다.
하지만 너무 바꿀 범위가 많다고 생각을 했습니다. 그래서 pr을 2개 쪼개어 날리기로 했습니다.
두 번이나 pr을 날릴수도 있고 계획적으로 나만의 로드맵을 그려갈 수 있다고 생각을 했습니다.
fileUtils.ts -> 개선
read-many-files.ts -> 병렬처리로 전환
기여 시작
1차 기여: fileUtils.ts 비동기 전환
저 파일안에 있는 detectFileType 만 비동기로 만들어도 되었지만
ai는 나에게 좀 더 몇 가지 개선점을 알려주었습니다.
javascript
// AS-ISexportfunctionisBinaryFile(filePath: string): boolean {try{const fd = fs.openSync(filePath,'r');// Read up to 4KB or file size, whichever is smallerconst fileSize = fs.fstatSync(fd).size;if(fileSize ===0){// Empty file is not considered binary for content checking fs.closeSync(fd);returnfalse;}const bufferSize =Math.min(4096, fileSize);const buffer =Buffer.alloc(bufferSize);const bytesRead = fs.readSync(fd, buffer,0, buffer.length,0); fs.closeSync(fd);if(bytesRead ===0)returnfalse;let nonPrintableCount =0;for(let i =0; i < bytesRead; i++){if(buffer[i]===0)returntrue;// Null byte is a strong indicatorif(buffer[i]<9||(buffer[i]>13&& buffer[i]<32)){ nonPrintableCount++;}}// If >30% non-printable characters, consider it binaryreturn nonPrintableCount / bytesRead >0.3;}catch{// If any error occurs (e.g. file not found, permissions),// treat as not binary here; let higher-level functions handle existence/access errors.returnfalse;}}// TO-BEexportasyncfunctionisBinaryFile(filePath: string):Promise<boolean>{letfileHandle: fs.promises.FileHandle|undefined;try{ fileHandle =await fs.promises.open(filePath,'r');// Read up to 4KB or file size, whichever is smallerconst stats =await fileHandle.stat();const fileSize = stats.size;if(fileSize ===0){// Empty file is not considered binary for content checkingreturnfalse;}const bufferSize =Math.min(4096, fileSize);const buffer =Buffer.alloc(bufferSize);const result =await fileHandle.read(buffer,0, buffer.length,0);const bytesRead = result.bytesRead;if(bytesRead ===0)returnfalse;let nonPrintableCount =0;for(let i =0; i < bytesRead; i++){if(buffer[i]===0)returntrue;// Null byte is a strong indicatorif(buffer[i]<9||(buffer[i]>13&& buffer[i]<32)){ nonPrintableCount++;}}// If >30% non-printable characters, consider it binaryreturn nonPrintableCount / bytesRead >0.3;}catch(error){// Log error for debugging while maintaining existing behaviorconsole.warn(`Failed to check if file is binary: ${filePath}`, error instanceofError? error.message:String(error),);// If any error occurs (e.g. file not found, permissions),// treat as not binary here; let higher-level functions handle existence/access errors.returnfalse;}finally{// Safely close the file handle if it was successfully openedif(fileHandle){try{await fileHandle.close();}catch(closeError){// Log close errors for debugging while continuing with cleanupconsole.warn(`Failed to close file handle for: ${filePath}`, closeError instanceofError? closeError.message:String(closeError),);// The important thing is that we attempted to clean up}}}}
fs.promises 를 붙인 것만으로도 아래와 개선 효과를 볼 수 있다는 걸 ai와 함께 정리 했습니다.
✅ 간단함: 코드가 직관적이고 이해하기 쉬움
✅ 동기 처리: 순차적 실행으로 디버깅이 용이
✅ 가벼움: 추가 Promise 오버헤드 없음
AS-IS 단점
❌ 블로킹: 파일 읽기 중 다른 작업 불가
❌ 에러 정보 부족: 디버깅 시 구체적 오류 정보 없음
❌ 리소스 누수 위험: 예외 발생 시 파일 디스크립터 정리 불완전
TO-BE 장점
✅ 논블로킹: 다른 작업과 병렬 처리 가능
✅ 상세한 에러 처리: console.warn으로 디버깅 정보 제공
✅ 안전한 리소스 관리: finally 블록으로 확실한 정리
✅ 확장성: 비동기 생태계와 잘 맞음
TO-BE 단점
❌ 복잡성: 코드가 길어지고 이해하기 어려움
❌ Promise 오버헤드: 작은 성능 비용 발생
❌ 호출부 변경 필요: await 키워드 추가 필요
권장사항
대량 파일 처리나 웹 서버 환경에서는 TO-BE 방식을, 간단한 CLI 도구나 소규모 스크립트에서는 AS-IS 방식이 적합합니다.
fs.promises API 설명
기본 개념
fs.promises 는 Node.js의 파일 시스템 모듈에서 Promise 기반의 비동기 API를 제공하는 네임스페이스입니다.
주요 차이점
fs.openSync() - 동기 방식
javascript
const fd = fs.openSync('/path/file','r');// ✅ 즉시 파일 디스크립터 반환// ❌ 파일이 열릴 때까지 코드 실행 중단 (블로킹)// ❌ 이벤트 루프 차단
fs.promises.open() - Promise 방식
javascript
const fileHandle =await fs.promises.open('/path/file','r');// ✅ Promise 반환으로 논블로킹// ✅ 다른 작업과 병렬 처리 가능// ✅ FileHandle 객체 반환 (더 안전한 API)
fs.promises API의 핵심 특징
1. FileHandle 객체
javascript
// 기존: 단순 숫자 파일 디스크립터const fd = fs.openSync('file.txt','r');// 3 (숫자)// fs.promises: FileHandle 객체const fileHandle =await fs.promises.open('file.txt','r');// { fd: 3, read: Function, write: Function, close: Function, ... }
2. 자동 리소스 관리
javascript
// 위험한 패턴 (동기)const fd = fs.openSync('file.txt','r');// 에러 발생 시 close 안됨const data = fs.readSync(fd, buffer,0, buffer.length,0);fs.closeSync(fd);// 안전한 패턴 (Promise + try/finally)let fileHandle;try{ fileHandle =await fs.promises.open('file.txt','r');const data =await fileHandle.read(buffer,0, buffer.length,0);}finally{await fileHandle?.close();// 항상 정리됨}
# feat: Make file type detection and binary checks asynchronous (#3286)## 🔧 Changes Made- Converted sync file operations to async implementations
- Used `fs.promises` for non-blocking file I/O
- Enhanced resource management with proper cleanup
- Updated test cases for async compatibility
## 💡 Why This Matters
"The original sync file operations were blocking the Node.js event loop,
causing UI freezes and poor performance when processing multiple files."
## 🎯 Next Steps
This lays the foundation for parallel file processing (coming in next PR)
리뷰어 반응:
Gemini Code Assist: "clean and thorough implementation"
NTaylorMullen: "Thanked for the contribution" ✅ 승인
첫 PR이 성공적으로 머지되었을 때의 기쁨은 정말 컸습니다.
이슈 등록부터 시작해 제 코드로 직접 성능 개선에 기여했다는 성취감, 그리고 다음 기여를 위한 발판까지 마련했다는 생각에 뿌듯했습니다.
jacob314: "Praised the performance optimization and test coverage" ✅ 승인
SandyTao520: 머지 완료
이과정에서 약간의 코드리뷰가 있었는대
리뷰어는 ! non-null assertion이 잠재적 버그를 가릴 수 있다고 지적했습니다.
파일 처리 중 실패하는 엣지 케이스에서 에러를 던지는 대신 undefined를 반환하며 조용히 넘어가버릴 수 있기 때문입니다.
그의 지적에 따라, 성공과 실패 케이스를 명확히 구분하는 Result 타입을 도입하여 코드의 안정성을 한층 높일 수 있었습니다.
작은 기호 하나에도 깊은 뜻이 있다는 것을 배운 순간이었습니다.
그래서 이렇게 타입을 만들어 성공과 실패 케이스를 나누어서 처리를 하였습니다.
typescript
/**
* Result type for file processing operations
*/typeFileProcessingResult=|{ success:true; filePath:string; relativePathForDisplay:string; fileReadResult:NonNullable<Awaited<ReturnType<typeof processSingleFileContent>>>; reason?:undefined;}|{ success:false; filePath:string; relativePathForDisplay:string; fileReadResult?:undefined; reason:string;};
그리고 성능이 중요하다고 생각 하여 병렬 처리에대한 속도 처리 테스트를 만들어 넣었습니다.
typescript
it('should process files in parallel for performance',async()=>{// Mock detectFileType to add artificial delay to simulate I/Oconst detectFileTypeSpy = vi.spyOn(awaitimport('../utils/fileUtils.js'),'detectFileType',);// Create filesconst fileCount =4;const files =createMultipleFiles(fileCount,'Batch test');// Mock with 100ms delay per file to simulate I/O operations detectFileTypeSpy.mockImplementation(async(_filePath:string)=>{awaitnewPromise(resolve =>setTimeout(resolve,100));return'text';});const startTime =Date.now();const params ={ paths: files };const result =await tool.execute(params,newAbortController().signal);const endTime =Date.now();const processingTime = endTime - startTime;console.log(`Processing time: ${processingTime}ms for ${fileCount} files`);// Verify parallel processing performance improvement// Parallel processing should complete in ~100ms (single file time)// Sequential would take ~400ms (4 files × 100ms each)expect(processingTime).toBeLessThan(200);// Should PASS with parallel implementation// Verify all files were processedconst content = result.llmContentasstring[];expect(content).toHaveLength(fileCount);// Cleanup mock detectFileTypeSpy.mockRestore();});
성능 테스트가 게임 체인저였다!
구글러들이 특히 좋아한 부분은 구체적인 성능 측정 테스트였습니다.
typescript
// 실제 성능 개선을 증명하는 테스트it('should process files in parallel for performance',async()=>{// 4개 파일 처리 시간 측정const startTime =Date.now();const result =await tool.execute(params, signal);const endTime =Date.now();const processingTime = endTime - startTime;// 🎯 병렬 처리 효과 검증: 400ms → 200ms 이하expect(processingTime).toBeLessThan(200);// ✅ PASS!});
jacob314의 극찬:"Praised the performance optimization and test coverage"