2019년 새해가 밝았습니다. 새해 기념으로 다른 분들처럼 회고 포스팅을 해볼까 생각도 했었지만, 다른 개발자들분께서 써놓으신 회고를 살펴보다가 ‘나는 한 해 동안 무엇을 했나’라는 자괴감(…)이 들어 마음을 접었습니다. 사실상 ‘회고’라 하면, 지난 시간동안 무엇을 했는지를 먼저 되돌아봐야 하기 때문이겠지요. 마땅히 한 게 있어야 회고를 하지…
개발자로써 주변에 다른 사람들을 살펴보면 정말로 개발 자체를 즐기시는 분들이 많습니다. 포트폴리오를 쌓기 위해 일을 하는 것이 아닌, 일을 즐기다보니 포트폴리오가 뒤따라 오는 듯한 느낌이 듭니다. 그런 분들을 지켜보자면, 경외심과 존경심이 들지만 저 스스로에게 열등감을 느끼게 되는 건 어쩔 수 없더라구요.
그래도 지난 해에 했던 활동 중에 좀 뜻깊었던 게 뭐가 있을까 생각해보다가 떠오른 게 바로 처음으로 오픈 소스에 풀 리퀘스트를 보내 기여해 본 적이 있다는 것입니다. 간단히 요약하자면 어쩌다보니 회사에서 쓰고 있는 오픈 소스에 있는 버그를 발견하게 되었는데, 이를 수정하는 풀 리퀘스트를 보내 기여한 경험입니다. 처음 해보는 거라 어떻게 하는 건지 헷갈렸는데, 회사에 있는 다른 개발자분들께서 도움을 주신 덕분에 한 번에 풀 리퀘스트 요청을 승인받을 수 있었습니다.
그래서 저 뿐만 아니라 다른 주니어 개발자분들도 참고하시라고, 제 경험을 상세히 공유하고자 합니다. 그렇게 어렵지 않아요.
오픈 소스에서 버그 발견
모든 것은 사소한 버그의 발견으로부터 시작됩니다. 저는 회사에서 iView라는 Vue.js기반 오픈 소스 UI 프레임워크를 사용하고 있습니다. 마치 부트스트랩처럼 다양한 UI 컴포넌트를 쉽게 활용할 수 있게 만들어주는 편리한 도구입니다. iView는 깃허브에서 약 20,000개 정도의 스타를 받은 꽤 큰 프레임워크인데, 중국 쪽에서 많이 활용하나 봅니다.
그러던 어느 날, 버그 리포트가 하나 들어와 살펴보고 있었습니다. 특정 컴포넌트에서 뭔가 이상한 점이 있다는 내용이었습니다.
특정 범위(max
와 min
) 내에서 하나의 값을 사용자로부터 입력받는 용도의 이 컴포넌트를 슬라이더 라고 부릅니다. 아무튼 지금 위의 사진을 보시면, 슬라이더를 드래그할 때는 정상적으로 작동하나 오른쪽에서 값을 나타내어주는 Input에서 값을 바꾸면 이상한 점이 생깁니다.
자세히 살펴보자면 Slider
컴포넌트 오른쪽에 있는 Input 값을 0으로 조정하면, 그 값이 min
으로 변해버린다 입니다.
처음에는 제가 컴포넌트의 props(Vue에서 컴포넌트의 특정 값을 사전에 설정하는 것으로 이해하시면 됩니다) 값을 잘못 주고 있었나 싶어 저희 쪽 코드를 계속 찾아보았지만, 아무리 찾아도 저희 쪽 코드에는 문제가 없었습니다.
버그의 원인을 (개발자라면) 스스로 찾아보자
Do it by myself!
이상하다 싶어, 깃허브에 들어가 이 컴포넌트의 소스를 한 줄씩 읽어보고 있었습니다. 그러다가 눈에 띄는 소스 코드 한 줄을 발견했는데…
handleInputChange (val) {
this.currentValue = [val || this.min, this.currentValue[1]];
this.emitChange();
}
바로 이 함수의 첫 번째 줄이 문제였습니다. 특히 val || this.min
이 부분이 그랬습니다.
이 버그가 일어난 이유는 max
는 양수, min
는 음수였기 때문에 발생한 거였습니다. 제가 변수 val
을 숫자 0으로 만드는 순간, 조건문 val || this.min
에서 val
이 숫자 0을 false
값으로 인식하면서 OR 조건문 뒤의 값 때문에 val
가 항상 min
이 되는 것이었습니다!
간단하지만 황당한 버그였습니다. 이 버그가 아직까지 발견되지 않았던 이유는 아마 대부분의 사람들이 일반적으로 슬라이더 컴포넌트를 min: 0, max: 100
으로 사용해서 아마 눈에 띄지 않았나 봅니다. 저는 각도(Angle) 표시를 위해 슬라이더 Props를 min: -180, max: 180
으로 쓰다보니 버그를 찾게 되었네요.
원인을 알았다면 스스로 버그를 고쳐보자
그럼 이 버그는 어떻게 고칠까요? 다른 좋은 방법이 있는지는 잘 모르겠지만 제가 생각났던 건 그냥 단순히 숫자 0에 대한 예외 처리를 해 주는 것이었습니다. 그래서 코드를 이렇게 고쳐보았습니다.
handleInputChange (val) {
this.currentValue = [val === 0 ? 0 : val || this.min, this.currentValue[1]];
this.emitChange();
}
좋은 코드인지는 잘 모르겠지만 val === 0
일 경우에 한해서만 문제가 발생했고, 워낙 간단한 버그였기 때문에 그냥 삼항연산자를 이용해서 강제로 0을 때려박았습니다. 이렇게 해보니 잘 작동하더군요.
풀 리퀘스트를 보내기 전에, 우선 해당 리포지토리에 이러한 버그가 있다고 제보를 해야겠지요?
리포지토리에 이슈(Issue) 생성하기
우선 원본 깃허브 리포지토리의 이슈 탭에 들어가 새 이슈를 만듭니다. 아마 규모가 있는 프로젝트라면, 이슈를 등록하는 규격이 있을 것입니다. 거기에 맞춰서 등록해줍니다.
풀 리퀘스트가 끝난 이후에 포스트를 작성하는 중이라 상태가 Closed가 되었습니다. 방금 만든 상태라면 Open이 맞습니다.
짧은 영어 실력이지만 이슈를 등록해줍니다. 웬만하면 JSFiddle, CodePen을 이용해 예시 링크를 걸어주는 것이 권장됩니다. 이슈 넘버는 #5002
인데 나중에 쓸 데가 있으니 기억해둡시다.
Fork, Checkout, Commit and Push
우선 iView 리포지토리는 제 것이 아니기 때문에, 당연하게도 제가 직접 코드를 수정할 수 없습니다.
따라서 먼저 수정하고 싶은 리포지토리를 포크(Fork)합니다. iView 리포지토리를 완전히 복사해, 내 리포지토리로 만드는 것이죠.
잠깐만 기다린다면…
리포지토리 제목 밑에 forked from이라는 단어와 함께 포크(Fork)가 된 상태입니다. 이제 이 리포지토리를 로컬 환경에서 수정하기 위해, 로컬 컴퓨터로 리포지토리를 클론(Clone)해봅시다.
우선 이 리포지토리를 Clone하기 위해 URL을 아래 위치에서 복사해줍니다. 그 다음, 터미널을 열어 복사하고 싶은 디렉토리에 이동한 후, git clone
명령어 이후에 해당 URL을 입력해줍니다.
C:\Users\wormw\Documents\GitHub> git clone https://github.com/wormwlrm/iview.git
그리고 입력하면…
C:\Users\wormw\Documents\GitHub> git clone https://github.com/wormwlrm/iview.git
Cloning into 'iview'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 20560 (delta 1), reused 0 (delta 0), pack-reused 20555
Receiving objects: 100% (20560/20560), 22.90 MiB | 3.89 MiB/s, done.
Resolving deltas: 100% (14878/14878), done.
Checking out files: 100% (650/650), done.
C:\Users\wormw\Documents\GitHub>
리포지토리가 로컬 컴퓨터에 복사되었습니다. 디렉토리에 직접 가서 확인해보죠.
Fork 이후에는 해당 디렉토리로 들어가 개발환경 세팅을 위해 NPM 패키지를 설치해주어야 합니다.
C:\Users\wormw\Documents\GitHub> cd .\iview\
C:\Users\wormw\Documents\GitHub\iview> npm install
여기서 설치해야 할 NPM 패키지가 많다면 조금 시간이 걸릴 수 있습니다. 영겁의 시간을 거쳐 NPM 패키지 설치가 완료되었다면 원본 브랜치 말고, 이슈를 수정할 목적의 새로운 브랜치를 만들어야 합니다. 브랜치 이름을 issue-5002
으로 한다면, 나중에 이 커밋을 확인할 사람이 이 브랜치의 목적이 이슈 해결이라는 것을 쉽게 확인할 수 있겠지요? 브랜치를 아래와 같은 명령어로 바꾼 후, 확인할 수 있습니다.
C:\Users\wormw\Documents\GitHub\iview> git checkout -b issue-5002
Switched to a new branch 'issue-5002'
C:\Users\wormw\Documents\GitHub\iview> git branch
2.0
* issue-5002
이제 이슈를 수정할 전용 브랜치도 만들었으니, 로컬 컴퓨터에서 테스트 하기 위한 개발용(dev) 서버를 켜보도록 합시다.
C:\Users\wormw\Documents\GitHub\iview> npm run dev
> iview@3.2.1 dev C:\Users\wormw\Documents\GitHub\iview
> webpack-dev-server --content-base test/ --open --inline --hot --compress --history-api-fallback --port 8081 --config build/webpack.dev.config.js
Project is running at http://localhost:8081/
webpack output is served from /
Content not from webpack is served from C:\Users\wormw\Documents\GitHub\iview\test
404s will fallback to /index.html
webpack: wait until bundle finished: /
일반적으로 dev 서버를 실행시키는 방법은 해당 리포지토리의 README.md
파일을 읽어보시거나, 또는 package.json
안에 있는 scripts
객체를 찾아보시면 쉽게 발견할 수 있습니다. 서버는 위에 나와있는대로 localhost:8081
에서 확인할 수 있습니다.
로컬 호스트에서 접속 가능해졌습니다. 이제 src/
디렉토리에 들어가, 아까 위에서 말한 내용대로 수정한 후 잘 동작하는 지 확인합니다. 당연히 잘 동작합니다.
이제 해당 내용을 커밋합니다. 근데 아마 로컬 컴퓨터에서 개발 서버를 구축하면, dist/
라는 디렉토리와 package-lock.json
파일처럼 서버 실행을 위한 파일과 디렉토리들이 존재하게 됩니다. 결과적으로 직접 수정된 파일만 커밋해야 하므로, 이 결과물들은 커밋하시면 안됩니다. git add
에서 수정한 파일만 스테이징 상태로 올려주세요.
C:\Users\wormw\Documents\GitHub\iview> git add src\components\slider\slider.vue
C:\Users\wormw\Documents\GitHub\iview> git commit -m "Issue 5002"
[issue-5002 d3aa3c5a] Issue 5002
1 file changed, 1 insertion(+), 1 deletion(-)
C:\Users\wormw\Documents\GitHub\iview> git push origin issue-5002
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 509 bytes | 127.00 KiB/s, done.
Total 6 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
remote:
remote: Create a pull request for 'issue-5002' on GitHub by visiting:
remote: https://github.com/wormwlrm/iview/pull/new/issue-5002
remote:
To https://github.com/wormwlrm/iview.git
* [new branch] issue-5002 -> issue-5002
C:\Users\wormw\Documents\GitHub\iview>
푸시(Push)가 끝났으면 포크(Fork)한 내 리포지토리에 방금 만든 브랜치가 있는지 다시 확인합니다.
아주 좋습니다. 이제 풀 리퀘스트를 보낼 준비가 완료되었습니다.
풀 리퀘스트 보내기
푸시 리퀘스트는 원본 리포지토리에 들어가서 할 수 있습니다.
깃허브가 친절하게도 방금 전 저의 커밋을 보고 풀 리퀘스트를 보낼 것이냐 물어보네요. 클릭합니다.
오른쪽(저의 커밋)의 수정본을 왼쪽(원본 리포지토리)로 머지(Merge)할 것이라고 물어봅니다. 맞는 설정이니 건드리지 않습니다.
마지막으로 아래의 제목과 내용을 작성합니다. #5002
번 이슈를 인용하여, 최대한 자세하게 내용을 작성해줍니다. 마크다운 문법을 지원하니, 이를 사용하면 좀 더 깔끔한 리퀘스트를 만들 수 있습니다.
풀 리퀘스트가 끝난 이후에 포스트를 작성하는 중이라 상태가 Merged가 되었습니다. 방금 만든 상태라면 Open이 맞습니다.
와, 드디어 풀 리퀘스트를 만들었습니다. 여기까지 따라오셨다면 훌륭합니다. 이제 남은 일은 원본 리포지토리 관리자(저 같은 경우에는 iView)가 저의 풀 리퀘스트를 보고 승인해주거나, 거절하거나 둘 중 하나입니다. 행운을 빌어봅니다.
오잉…? 풀 리퀘스트의 상태가?
축하합니다! 풀 리퀘스트는 머지(Merged)로 진화했습니다!
풀 리퀘스트 요청 글을 작성한 지 일주일 정도가 지나 답변이 달렸습니다. 결과는 승인(Merged)! 게다가 무려 리포지토리 최고책임자 분께서 ‘Nice Work’에 덧붙여 원따봉을 박아주셨습니다. 뿌듯했습니다. 좀 과장해서, 세계를 아우르는 따뜻한 인류애까지 느껴졌습니다.
그리고 저 풀 리퀘스트는 나중에 공식 릴리즈노트에 기록되게 됩니다.
느낀 점
나의 작은 도움이 거대한 프로젝트에 기여된다는 것이 되게 뿌듯합니다. 한 번 시도해 보니까, 다른 오픈 소스 프로젝트에서도 뭔가 문제를 발견하면 제가 고쳐주고 싶다는 마음이 듭니다. 풀 리퀘스트가 머지(Merge)되면, 본인의 깃허브에서도 이를 확인할 수 있어서 자랑하기에도 좋습니다.
“나 이 프로젝트 기여자야~!”
거창하지 않은 시작도 좋다고 생각합니다. 저도 단 1줄의 코드만 수정한 것만으로 기여자가 되었는걸요. 버그를 고쳐도 좋고, 사소한 오탈자를 수정하는 것도 좋습니다. README.md
파일을 한국어로 번역하는 것도 좋아요. 지금 당장 시작하지 않아도 좋습니다. 관심있는 리포지토리에 스타(Star) 표시 해 놓은 후, 나중에 뭔가를 해도 좋아요.
확실한 것은, 풀 리퀘스트를 보내는 행동 자체만으로도 오픈 소스 생태계를 바라보는 시야가 정말 넓어질 수 있다는 것입니다.