util.inspect가 음수 소수점을 잘못 포매팅하는 버그를 AI와 페어 프로그래밍으로 파고들어 4시간 만에 PR 승인까지 받았습니다. 이슈를 고르는 기준부터 메인테이너와 주고받은 과정까지, Node.js 코어 기여의 전 과정.
AI 페어 프로그래밍으로 오픈소스 진입 장벽 낮추기
“무조건 Node.js에 내 코드를 넣고 말겠다!”
오픈소스 기여가 점점 재미있어지고, 자신감도 차오르던 시기였다.
하지만 매일같이 이슈를 살펴봐도 여전히 모르는 부분이 많았다. C++ 코드가 보이기도 했고, fs 같은 코어 모듈이 대부분이었기 때문이다. (게다가 이슈를 완벽히 이해한 것도 아니었다.)
그러던 중 AI를 활용한 오픈소스 기여 모임을 알게 되었고, 거기서 본 한 링크가 나에게 혁신을 가져다주었다.
1. 이슈 찾기: AI로 시작하는 빠른 탐색
이슈 분석에 어려움을 느끼던 내게 구세주가 나타났다.
바로 이슈 수집기였다.
곧장 nodejs/node 저장소와 내 토큰을 입력하고 모든 이슈를 긁어왔다. (지금도 매일 한다.)
내용:numericSeparator: true 옵션 사용 시 음수 소수점 숫자 포맷팅 버그
원인:formatNumber 함수에서 -0 처리 시 부호 손실
해결 방향: 없음
적합도: 상 (재현 코드와 명확한 문제 설명)
기술 난이도: 중
C++도 없고, 복잡한 코어 모듈도 아닌, 숫자 유틸 함수 하나를 수정하는 일이었다.
게다가 이슈가 올라온 지 5시간밖에 되지 않아 바로 댓글을 달고 선점했다.
곧 메인테이너가 해결 방향과 최적화 아이디어를 제시해주었고, 나는 지체 없이 코드를 수정하러 들어갔다.
AI 활용 팁: 이슈 선택 시 체크리스트
난이도: 너무 어려운 영역(C++, 네이티브 모듈 등)은 피하고, 변경 범위가 명확한 이슈를 선택합니다.
재현 가능성: 이슈 설명에 재현 코드나 명확한 조건이 포함되어 있는지 확인합니다.
라벨 여부: 'good first issue', 'help wanted' 라벨은 초심자에게 좋은 출발점입니다.
2. 문제 분석: 버그 원인 파악하기
문제는 간단했다. util.inspect에서 numericSeparator 옵션을 켜면, 0과 1 사이의 음수 값에서 부호가 사라지는 현상이 있었다.
typescript
import{ inspect }from'util';const values =[0.1234,-0.12,-0.123,-0.1234,-1.234];const text =inspect(values,{ numericSeparator:true});console.log(text);// 기대 출력[0.123_4,-0.12,-0.123,-0.123_4,-1.234]// 실제 출력[0.123_4,0..12,0..12_3,0..12_34,-1.234]
문제의 원인은 lib/internal/util/inspect.js에서 숫자를 문자열로 변환할 때, -0이 "0"으로 변환되어 부호가 사라지는 것이었다.
3. 개발 환경 구축: 로컬에서 Node.js 빌드하기
Node.js 저장소를 포크 후 클론 받아 AI CLI 환경에서 코드베이스를 학습시켰다.
처음에는 C++이 없어서 빌드 에러가 발생했다. AI와 함께 원인을 찾아보니 Command Line Tools 설치가 필요했다. 설치 후 다시 빌드하니 약 30분 만에 성공.
처음 직접 빌드한 Node.js를 실행했을 때, 테스트 코드가 정상 동작하는 것을 보고 꽤 뿌듯했다.
AI 활용 팁: 빌드 에러 대처
환경 의존성: C++ 컴파일러, Python 버전 등 필수 의존성을 먼저 확인합니다.
빌드 로그 분석: 에러 메시지를 그대로 AI에게 전달하면 빠르게 해결책을 제시해줍니다.
부분 빌드: 전체 빌드가 오래 걸린다면, 변경된 부분만 빌드하는 방법을 찾아보세요.
4. 테스트 작성: 버그 재현부터 시작
다음 단계는 재현 테스트 작성이었다.
이슈(#59376) 내용을 기반으로 아직 통과하지 못하는 테스트를 만들고, 단일 파일 테스트를 실행했다.
// 수정 전 (버그가 있던 코드)const integer =MathTrunc(number);const string =String(integer);// 소수점 처리returnfn(`${addNumericSeparator(string)// <- 여기서 음수 부호 손실}.${addNumericSeparatorEnd(StringPrototypeSlice(String(number), string.length+1),)}`,'number',);
수정 후 formatNumber
javascript
functionformatNumber(fn, number, numericSeparator){if(!numericSeparator){if(ObjectIs(number,-0))returnfn('-0','number');returnfn(`${number}`,'number');}const numberString =String(number);const integer =MathTrunc(number);if(integer === number){if(!NumberIsFinite(number)||StringPrototypeIncludes(numberString,'e')){returnfn(numberString,'number');}returnfn(addNumericSeparator(numberString),'number');}if(NumberIsNaN(number)){returnfn(numberString,'number');}// 원본 문자열에서 직접 소수점 위치 찾기const decimalIndex =StringPrototypeIndexOf(numberString,'.');// 방어적 주석: IEEE-754에서 이 분기까지 오는 경우는 거의 없음if(decimalIndex ===-1)returnfn(numberString,'number');// 원본 문자열에서 정수/소수 부분 분리const integerPart =StringPrototypeSlice(numberString,0, decimalIndex);const fractionalPart =StringPrototypeSlice(numberString, decimalIndex +1);returnfn(returnfn(`${addNumericSeparator(integerPart)// <- 음수 부호 보존}.${addNumericSeparatorEnd(fractionalPart)}`,'number');}