오픈 소스 프로젝트에 처음 기여하는 경험이 생겨 공유하고자 포스팅합니다.
기여한 오픈 소스 프로젝트는 백준 허브라는 크롬 익스텐션 프로젝트이며 백준, 프로그래머스 등의 코딩 테스트 연습 사이트에서 문제를 풀 경우 그 풀이를 연동된 깃허브에 업로드해주는 기능을 제공합니다.
국내 사이트에서 코딩 테스트 풀이를 깃허브에 업로드하시는 분들은 사용하시면 좋은 프로그램이 될 것 같습니다.
깃허브 레포지토리에 접속하시면 Issues 탭에 13개의 이슈가 등록된 것을 확인할 수 있습니다.
이 이슈들 중에서 오류에 대한 정의가 잘 되어 있고
, 내가 해결할 수 있는 문제
를 골라 해결하려고 했습니다.
저는 가장 최근에 오픈된 이슈인 서브태스크가 있는 문제 100점이 아닌 결과가 커밋
이라는 이슈를 선정했습니다.
이제 해결해야 할 이슈도 선택했고 해결해야 할 문제도 파악했으니 프로젝트를 로컬 환경에서 수정할 차례입니다. 다른 사람의 레포지토리를 내 로컬 환경에 가져오기 위해서는 다음과 같은 과정을 거쳐야 합니다.
fork
는 다른 사람의 원격 저장소를 나의 원격 저장소로 가져오는 기능이고, clone
은 원격 저장소에서 로컬 환경으로 프로젝트 디렉토리를 가져오는 기능입니다.
해당 git 명령어에 대해서는 더 자세하게 설명한 글들이 많이 있으니 원하시는 분은 검색해보시면 좋을 것 같습니다.
제가 선택한 이슈는 백준
사이트에서 문제 풀이
후 제출된 답안을 비교
하는 과정에서 문제가 발생했기 때문에 프로젝트 내에서 해당 로직을 처리하는 파일들을 살펴봤습니다. (이 때, IDE의 전역 검색(Find in Files)과 참조 이동(Go to References)를 사용하면 함수가 정의된 부분이나 사용되는 부분들을 손쉽게 찾을 수 있습니다.)
백준 허브 프로젝트의 경우 개발자분들께서 파일 분리, 직관적인 함수명 작성, JSDoc 작성 등 소스 코드에 대해 이해하기 쉽게 작성해주셨기 때문에 제가 원하는 코드에 대해서 빨리 파악할 수 있었습니다.
이렇게 프로젝트에 대해서 알아가다보니 baekjoon/parsing.js
→ scripts/util.js
→ baekjoon/util.js
순으로 파일들을 거쳐 compareSubmission() 함수에 도달해 최종적으로 수정할 부분을 찾았습니다.
compareSubmission() 함수의 경우 백준의 결과 테이블을 파싱해 오브젝트 타입으로 전달받습니다. 이 부분은 parsing.js
파일의 findData() 함수부터 시작해서 로직을 따라가보시면 도달할 수 있으니 시도해보시면 좋을 것 같습니다.
제가 수정할 부분은 이 함수입니다. 보시다시피
- 실행 속도 오름차순
- 사용 메모리 오름차순
- 코드 길이 오름차순
- 제출 순서 내림차순(최근 제출한 답안)
순으로 답안에 우선순위를 부여해 어떤 답안을 깃허브에 업로드할 지 정하는 기능을 맡습니다.
아까의 이슈가 언급한 것과 같이 서브태스크가 있는 문제의 경우 답안의 점수를 고려하는 로직이 없기 때문에 추가해주도록 합니다. 여기서, 서브태스크가 있는 문제와 없는 문제를 구분할 수 있어야 합니다.
제가 정의한 서브태스크가 있는 문제와 없는 문제는 다음과 같습니다.
서브태스크가 없는 문제 : 제출한 답안이 맞을 경우 제출 결과가 '맞았습니다!!'인 문제
서브태스크가 있는 문제 : 제출한 답안이 맞을 경우 제출 결과가 'x점'인 문제(이 때, x는 정수)
이를 통해 제출 결과
중 숫자를 파싱했을 때 빈 문자열이라면 서브태스크가 없는 문제, 숫자가 있다면 서브태스크가 있는 문제라고 두고 코드를 수정했습니다.
// baekjoon/util.js
/**
* 서브태스크가 있는 문제의 경우도 고려해 제출 결과를 비교하는 함수입니다.
* @param {string} a 제출 결과 피연산자 a
* @param {string} b 제출 결과 피연산자 b
* @returns {boolean} 서브 태스크가 없는 경우 true, 서브 태스크가 있는 경우 false를 반환합니다.
*/
function hasNotSubtask(a, b) {
a = parseNumberFromString(a);
b = parseNumberFromString(b);
if (isNaN(a) && isNaN(b)) return true;
return false;
}
/**
* 서브태스크가 있는 문제의 경우 점수가 높은 순서로 정렬되도록 값을 반환합니다.
* @param {string} a 제출 결과 피연산자 a
* @param {string} b 제출 결과 피연산자 b
* @returns {number} a의 점수가 높은 경우 음수, b의 점수가 높은 경우 양수
*/
function compareResult(a, b) {
a = parseNumberFromString(a);
b = parseNumberFromString(b);
if (typeof a === 'number' && typeof b === 'number') return -(a - b);
if (isNaN(b)) return -1;
if (isNaN(a)) return 1;
}
// scripts/util.js
function parseNumberFromString(str) {
const numbers = str.match(/\d+/g);
if (isNotEmpty(numbers) && numbers.length > 0) {
return Number(numbers[0]);
}
return NaN;
}
/**
* 제출 목록 비교함수입니다
* @param {object} a - 제출 요소 피연산자 a
* @param {object} b - 제출 요소 피연산자 b
* @returns {number} - a와 b 아래의 우선순위로 값을 비교하여 정수값을 반환합니다.
* 1. 서브태스크가 있는 문제이고, 점수(result)의 차이가 있을 경우 a > b 이면 음수, a < b 이면 양수를 반환합니다.
* 2. 실행시간(runtime)의 차이가 있을 경우 그 차이 값을 반환합니다.
* 3. 사용메모리(memory)의 차이가 있을 경우 그 차이 값을 반환합니다.
* 4. 코드길이(codeLength)의 차이가 있을 경우 그 차이 값을 반환합니다.
* 5. 위의 요소가 모두 같은 경우 제출한 요소(submissionId)의 그 차이 값의 역을 반환합니다.
* */
function compareSubmission(a, b) {
// prettier-ignore-start
/* eslint-disable */
return hasNotSubtask(a.result, b.result)
? a.runtime === b.runtime
? a.memory === b.memory
? a.codeLength === b.codeLength
? -(a.submissionId - b.submissionId)
: a.codeLength - b.codeLength
: a.memory - b.memory
: a.runtime - b.runtime
: compareResult(a.result, b.result)
;
/* eslint-enable */
// prettier-ignore-end
}
가독성을 위해 분리할 로직을 함수로 정의해두고 설명이 필요한 로직이 들어가는 함수는 주석을 잘 작성해줍니다.
또한, 기존의 코드에 수정이 일어났을 경우 함수에 대한 주석이 여전히 올바른지도 잘 살펴보고 수정해야 합니다.
// baekjoon/parsing.js
/**
* 문제의 상세 정보를 가지고, 문제의 업로드할 디렉토리, 파일명, 커밋 메시지, 문제 설명을 파싱하여 반환합니다.
* @param {Object} data
* @returns {Object} { directory, fileName, message, readme, code }
*/
function makeDetailMessageAndReadme(data) {
const { problemId, submissionId, result, title, level, problem_tags,
problem_description, problem_input, problem_output,
code, language, memory, runtime } = data;
const score = parseNumberFromString(result);
const directory = `백준/${level.replace(/ .*/, '')}/${problemId}. ${convertSingleCharToDoubleChar(title)}`;
const message = `[${level}] Title: ${title}, Time: ${runtime} ms, Memory: ${memory} KB`
+ ((isNaN(score)) ? ' ' : `, Score: ${score} point `) // 서브 태스크가 있는 문제로, 점수가 있는 경우 점수까지 커밋 메시지에 표기
+ `-BaekjoonHub`;
// ... (생략)
}
추가로 커밋 메시지에도 답안의 점수가 표기되도록 수정해줍니다.
이제 PR(Pull Request)를 요청할 준비가 되었습니다. 저는 Closed 상태의 PR들을 참고하며 요청을 작성했습니다.
또, 제가 수정한 부분에 대해서 왜 이런 해결 방법을 택했는지까지 Comment를 적어 간결하게 알리고자 노력했습니다.
제 PR이 병합됨으로써 첫 오픈 소스 기여 경험을 가져갈 수 있었습니다.
타인의 프로젝트에 기여하면서 가장 크게 느낀 것은 코드라는 것이 말이나 행동과는 다른, 의사소통 중에 하나 같다는 것입니다.
이미 작성된 코드들을 살펴보면서 코드 스타일을 맞춰나가고 다른 사람이 내가 작성한 코드를 읽기 쉬울 뿐만 아니라 왜 이렇게 작성했는지 납득할 수 있도록 노력을 많이 했습니다.
잘못된 내용이나 오타 지적 언제나 환영입니다.
오픈소스에 기여하는 것 정말 뿌듯한 경험이네요!! 저도 kyu0님 처럼 멋진 개발자가 되고싶네요 :)