Outsider's Dev Story: Programming 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T10:18:37+09:00Textcube 1.10.7 : Tempo primoGit의 새로운 기본 Merge 전략 ortOutsiderhttps://blog.outsider.ne.kr/17072024-02-11T21:24:45+09:002024-02-11T21:24:45+09:00<p>현재 Git이 Merge 할 때 사용하는 전략이 <code>ort</code>로 바뀌었다. Git에 <code>ort</code> 머지 전략이 들어온 것은 <a href="https://github.blog/2021-08-16-highlights-from-git-2-33/#merge-ort-a-new-merge-strategy">Git 2.33</a>부터였고 <a href="https://github.blog/2021-11-15-highlights-from-git-2-34/#a-new-default-merge-strategy">Git 2.34</a>에서 별도의 설정 없이도 Merge할 때 사용되는 기본 전략으로 바뀌었다.</p>
<p>이 Merge 전략은 <code>git merge</code>와 <code>git pull</code>을 할 때 주로 사용되지만 <code>rebase</code>, <code>cherry-pick</code>, <code>revert</code>, <code>stash</code>, <code>checkout</code>에서도 사용된다. 2.34가 2021년 11월에 나왔으니 바뀐 지는 꽤 되었지만 뭐가 바뀌는지는 정확히 모르고 있어서 정리를 해봤다. 이 글 쓰는 시점의 Git 최신 버전은 <a href="https://raw.githubusercontent.com/git/git/master/Documentation/RelNotes/2.43.0.txt">2.43.0</a><br />
<br></p>
<h2><code>resolve</code> 전략</h2>
<p>Git이 2개의 브랜치를 머지할 때 변경 사항을 합치기 위해 여러 가지 전략 중 하나를 선택한다. 원래의 전략은 <a href="https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge">3-way merge</a> 알고리즘을 사용하는 <code>resolve</code> 전략을 사용했다.</p>
<p>3-way merge는 A 파일과 B 파일의 차이점을 분석한 후 두 파일의 공통 조상인 C 까지 고려해서 합칠 변경 사항을 구성하고 A, B, C 셋 다 다른 경우는 충돌(Conflict)로 표시해서 사용자가 해결하도록 한다.<br />
<br></p>
<h2><code>recursive</code> 전략</h2>
<p>Git이 만들어진 초기인 2005년에 <code>resolve</code> 전략은 <code>recursive</code>라는 전략으로 <a href="https://github.com/git/git/commit/fbf8ac212caa74fc506434da83f8e9630b09ed12">교체</a>된다. 이렇게 <code>recursive</code>로 바꾼 주요 이유는 머지할 두 브랜치가 공통된 조상이 없는 경우에도 각 브랜치에서 그 이름대로 재귀적으로 머지할 수 있어서 <code>resolve</code> 전략에서 충돌하는 경우에도 머지할 수 있고 한 브랜치에서는 파일이 수정되고 다른 브랜치에서는 파일명이 변경된 경우에도 이를 감지할 수 있기 때문이다.</p>
<p>이렇게 적용된 <code>recursive</code>는 오랫동안 Git의 기존 전략으로 사용되었다. 2005년에 적용되고 2021년에 나온 Git 2.33에서 바뀌었으니 16년 동안 사용된 셈이다.<br />
<br></p>
<h2><code>ort</code> 전략</h2>
<p><code>ort</code>는 재귀(recursion)와 파일이름 변경 탐지를 하는 <code>recursive</code>와 같은 컨셉을 가지고 처음부터 새로 작성된 전략이다. 그래서 ort라는 이름도 Ostensibly Recursive’s Twin의 약자로 표면적으로는 Recursive의 쌍둥이라는 의미이고 이전에 비해 <a href="https://lore.kernel.org/git/4a0f088f3669a95c7f75e885d06c0a3bdaf31f42.1628055482.git.gitgitgadget@gmail.com/">훨씬 빨라졌다</a>.</p>
<p><a href="https://git-scm.com/docs/merge-strategies#Documentation/merge-strategies.txt-ort">Git 공식 문서</a>를 보면 <code>ort</code> 전략을 다음과 같이 설명하고 있다.</p>
<blockquote>
<p>하나의 브랜치를 가져오거나(pull) 머지할 때 사용하는 기본 머지 전략이다. 이 전략은 3-way 머지 알고리즘을 사용해서 두 개의 헤드(head)만 처리할 수 있다. 3-way 머지에 사용할 수 있는 공통 조상이 2개 이상 있다면 공통 조상의 머지된 트리를 만들어서 이를 3-way 머지의 참조 트리로 사용한다. Linux 2.6 커널 개발 기록에서 가져온 실제 머지 커밋을 대상으로 테스트한 결과 ort가 잘못된 머지도 없고 머지 충돌이 더 적게 발생했다. 또한, 이 전략은 이름 변경과 관련된 머지를 탐지하고 처리할 수 있으며 감지된 사본(detected copies)을 사용하지 않는다.</p>
</blockquote>
<p>그러면 오랫동안 사용하던 <code>recursive</code> 전략을 왜 바꿀 필요가 있는지를 알아야 하는데 새로운 전략이긴 하지만 방법 자체를 바꾸었다기보다는 많은 최적화 작업을 합쳐서 <code>ort</code>라는 새로운 전략이 탄생했다고 볼 수 있고 그렇기에 이름도 <code>recursive</code>의 쌍둥이라는 이름을 가지게 된 것이다.</p>
<p>Git에서 <code>recursive</code> 머지의 코드 베이스는 문제를 해결하기 위해 조금만 고치다 보니 결국 고칠 수 없는 상황까지 왔습니다. 그래서 버그도 많이 발생하고 해결하기 어려운 엣지 케이스도 발생하고 있었고 개발자들도 이 부분의 코드 수정은 피하고 있었다. Git의 핵심 개발자 중 한 명인 <a href="https://github.com/newren">Elijah Newren</a>가 큰 변경을 하려고 하자 초기부터 Git을 리드하고 있는 <a href="https://github.com/gitster">Junio Hamano</a> 그냥 다시 작성하자는 제안하고 이에 따라 아주 대규모의 최적화 작업이 시작된다.</p>
<p>이 최적화 과정을 이해하려면 Git의 3-way 머지를 좀 이해해야 한다.</p>
<pre class="line-numbers"><code class="language-bash"> A---B---C topic
/
D---E---F---G main
</code></pre>
<p>즉, 위처럼 2개의 브랜치를 머지한다고 했을 때 각 브랜치의 최신 커밋인 <code>C</code>와 <code>G</code>, 두 브랜치의 공통 조상이 되는 커밋인 <code>E</code>까지 고려해서 머지하는 것이다. 특정 라인이 C 커밋에는 없고 G 커밋에는 있다고 했을 때 이 둘만 가지고는 이 라인이 추가된것인지 제거된 것인지 알 수 없다. 공통 조상이 E 커밋을 봤을 때 해당 라인이 있다면 C 커밋에서 제거한 것이고 E 커밋에 해당 라인이 없다면 G 커밋에서 추가된 것이다.(참고로 git에서 <a href="https://blog.outsider.ne.kr/805">diff3 설정</a>을 하면 충돌이 발생했을 때 공통 조상의 내용도 같이 보여서 수정할 때 편하다.)</p>
<p>여기서 파일 이름 변경까지 되면 훨씬 복잡해진다. 각 브랜치에서 파일 내용을 수정할 수 있지만 파일명을 바꾸거나 파일명은 그대로이지만 디렉터리 위치가 바뀔 수 있다. Git은 파일명을 따로 추적하고 있지 않기 때문에 머지할 때 이 이름 변경을 감지해서 이름이 변경된 파일을 찾아내야 하는 것이다. 위에서 말한 대로 머지를 할 때 3개의 커밋이 필요한데 각 커밋에서 고유의 파일명 목록을 만들고 서로 일일이 비교하면서 내용의 유사성을 비교해서 파일 변경인지를 표시하게 되므로 상당히 느린 부분이다.</p>
<p>코딩에서 추상화는 중요한 개념이지만 때로는 경계를 만들어서 경계에 걸쳐서 어떤 작업을 하려고 할 때 어렵게 만들기도 한다. Git도 이러한 추상화로 인해서 최적화가 어려웠던 경우인데 Git에도 파일 이름 변경을 탐지하는 컴포넌트가 분리되어 있었다. 이 컴포넌트는 3개의 커밋에 대한 정보가 아니라 2개 커밋에 대한 정보만 받고 있었는데 파일 이름 변경을 추적하려면 3개 커밋의 정보가 모두 필요했기 때문에 이 추상화 경계를 넘어서 추가 정보를 제공해야 했다.</p>
<p>또한, rebase나 cherry-pick을 할 때도 각 커밋 단계마다 이름 변경 탐지를 하게 되는데 이게 반복적으로 진행되므로 인메모리에 캐싱해서 개선하게 된다. 당연히 왜 이전에는 안 했는지 궁금할 수 있지만 캐싱해도 동작의 차이가 없는지를 확인하는 것이 아주 복잡했기 때문에 못 했던 것인데 이번에는 몇 가지 제약사항을 가진 채 이 캐싱 최적화를 추가했다.</p>
<p>Git에서 Index라는 것은 보통 우리가 Stage 영역이라고 부르는 것으로 다음 커밋에 포함될 파일에 대한 정보를 가지고 있는 데이터 구조인데 <code>recursive</code> 전략에서는 이 Index 데이터 구조가 핵심이었다(working tree 포함). <code>ort</code> 전략에서는 이 Index를 사용하지 않고 <code>recursive</code>에서 성능에 영향을 많이 주던 <code>unpack_trees()</code>라는 저수준 함수의 사용을 안 하도록 바뀌었다. 그래서 이 두 가지에 의존함으로써 생긴 제약을 대부분 해결할 수 있게 되었다.</p>
<p>다시 정리하면 <code>ort</code>는 index와 working tree를 건드리지 않고 머지 결과를 트리로 만들어서 이 머지 결과가 나왔을 때만 <code>ort</code>가 체크아웃 로직을 이용해서 머지 결과로 이동하게 된다. index에 항목을 추가 제거하는 동작과 머지하면서 수행되는 값비싼 트리 탐색을 피할 수 있게 되어 속도가 훨씬 빨라지게 된다.</p>
<p>최대한 간단히 정리했지만, 이 내용은 이 수많은 최적화 작업을 주도한 <a href="https://github.com/newren">Elijah Newren</a>이 작성한 6편의 글 Optimizing Git’s Merge Machinery, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-1-127ceb0ef2a1">#1</a>, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-2-d81391b97878">#2</a>, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-3-2dc7c7436978">#3</a>, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-part-iv-5bbc4703d050">#4</a>, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-part-v-46ff3710633e">#5</a>, <a href="https://blog.palantir.com/optimizing-gits-merge-machinery-6-7bf887a131d8">#6</a>에 잘 나와 있다. 아주 긴 글이지만 한번 읽어볼 만한 좋은 글이다.</p>
<p><a href="https://github.blog/2021-08-16-highlights-from-git-2-33/#merge-ort-a-new-merge-strategy">Git 2.33 공지</a>에 따르면 ort가 이전보다 훨씬 빨라져서 파일명 변경이 많고 복잡한 머지의 경우 500배가 빨라졌고 rebase 과정에서 비슷한 머지를 반복해서 하게 되면 <code>ort</code>가 일부 계산을 캐싱하기 때문에 9,000배 이상 빨라진다고 한다. 이건 특수한 경우고 일반적으로도 <code>ort</code>가 <code>recursive</code>보다 약간 빠른 것으로 나타났지만 <code>recursive</code>는 상황에 따라 속도 편차가 크지만 <code>ort</code>는 일관된 속도를 보여주었다.</p>
<p>일반적인 상황에서 보통 머지 속도가 문제 되진 않지만 아마 머지와 리베이스를 가장 많이 실행하는 <a href="https://github.blog/2023-07-27-scaling-merge-ort-across-github/">GitHub이 <code>ort</code>를 적용</a>한 결과를 보면 머지 속도가 p50에서는 10배 p99에서는 5배 빨라졌고 리베이스에서도 이전에 512시간 걸리던 리베이스가 <code>ort</code>에서는 33시간으로 줄어들었다는 것을 보면 속도가 얼마나 개선되었는지 알 수 있다.</p>
<p>추가로 Git으로 머지할 때 다음과 같이 어떤 전략이 사용되었는지 나온다.</p>
<pre class="line-numbers"><code class="language-bash">Auto-merging a.txt
Merge made by the 'ort' strategy.
</code></pre>
<p><code>git merge --strategy recursive BRANCH-NAME</code>처럼 머지할 때 <code>-s</code>나 <code>--strategy</code> 옵션으로 머지 전략을 지정하면 다른 머지 전략을 사용할 수 있다. 여기서처럼 <code>ort</code> 대신 <code>recursive</code>를 지정하면 메시지에서도 <code>Merge made by the 'recursive' strategy.</code>라고 나와서 머지 전략이 바뀌었음을 확인할 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1707?commentInput=true#entry1707WriteComment">댓글 쓰기</a></strong></p>Merge vs. Rebase vs. SquashOutsiderhttps://blog.outsider.ne.kr/17042024-01-16T03:19:09+09:002024-01-13T18:27:31+09:00<p><a href="https://www.hashicorp.com/">HashiCorp</a>의 공동창업자인 <a href="https://gist.github.com/mitchellh">Mitchell Hashimoto</a>가 얼마 전 <a href="https://gist.github.com/mitchellh/319019b1b8aac9110fcfb1862e0c97fb">Merge vs. Rebase vs. Squash</a>라는 글을 작성했다.</p>
<p>Git에서 Merge, Rebase, Squash에 관한 질문을 자주 받아서 정리했다고 하는데 요약하면 다음의 내용이다.</p>
<ul>
<li>셋 중 특정 전략이 100% 정답이라고 말하는 사람은 틀렸고 각 전략은 상황에 따라 다르다.</li>
<li>Merge 커밋을 만드는 Merge가 히스토리를 가장 잘 표현한다고 생각해서 Merge를 선호한다. </li>
<li>Merge 커밋이 있으면 쉽게 되돌릴 수 있다.</li>
<li>모든 커밋이 빌드된다면 커밋이 많을수록 git bisect가 좋아진다. </li>
<li>이상적으로 커밋은 +50/-50 정도로 유지하는 것이 좋다.</li>
<li>PR에 많은 WIP 커밋이 있지만 하나의 목표라면 Squash를 한다.</li>
<li>Squash할 때 Git과 GitHub이 제공하는 기본 메시지는 좋지 않아서 Squash를 할 때 커밋 메시지를 새로 작성한다.</li>
<li>PR에 WIP가 많은데 각 커밋의 차이가 크다면 인터랙티브 Rebase로 커밋을 합치고 순서를 변경한다. </li>
<li>개발자들이 커밋 관리에 신경 쓰기를 기대하지만, 많은 개발자가 Git에 익숙하지 않다. </li>
<li>대규모로 인터랙티브 Rebase를 할 때는 Git GUI를 사용한다. macOS에서는 Tower를 쓰고 있다.</li>
</ul>
<p>대부분의 내용을 공감해서 공유하면서 나는 어떻게 생각하고 있지를 고민하다 보니 그동안 개발을 하면서 내가 선호하는 방법도 다양하게 바뀌었다는 것을 깨달았다. 개발하면서 Git에 어울리게 쓰고 싶어서 한 것들도 있고 협업하다 보니내 취향과는 다르게 절충하게 된 것들도 있어서 트위터에서 글을 쓰다가 블로그에 정리하게 되었다.<br />
<br></p>
<h2>Merge</h2>
<p>Git을 배우면 처음에 Merge에 관해 배우기 마련이다. Git에서는 브랜치를 만들어서 작업하는 게 일반적이기 때문에 작업 후에 이 브랜치를 기본 브랜치에 합치는 Merge는 아주 중요하다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5118444125.jpg" width="390" height="160" alt="사용자 삽입 이미지" title="" /></p>
<p>Merge하면 그래프가 위처럼 만들어지고 <code>Merge branch '브랜치 이른'</code>의 메시지를 가진 Merge 커밋이란 게 만들어진다. 이 그래프처럼 Merge 커밋이 있으면 작업할 때 브랜치가 분리되었다가 합쳐졌다는 게 시각적으로나 구조적으로 구분되고 히스토리에도 남게 된다.</p>
<p>물론 더 정확히 얘기하면 머지할 때 fast-forward 머지가 아닐 때만 이 머지 커밋이 생긴다. fast-forward 머지가 가능할 때는 머지커밋 없이 바로 생기고 fast-forward와 상관없이 머지 커밋을 만들려면 <code>git merge --no-ff</code> 옵션을 사용해야 한다.</p>
<p>초기에 Git을 배우고 익숙해지면서 주변 사람들도 Merge 커밋 선호자와 비선호자로 나뉘게 되었는데 나는 Merge 커밋이 좋은가? 안 좋은가를 많이 고민했다. 그래도 시간이 지나면서 <strong>머지 커밋을 안 남기는 것을 더 좋아하게 되었다.</strong></p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8020900616.jpg" width="750" height="321" alt="사용자 삽입 이미지" title="" /></p>
<p>당시 내가 가장 좋아하던 Node.js 프로젝트는 Git 히스토리를 일직선으로만 만들었다. 당시 GitHub의 Pull Request는 무조건 <code>--no-ff</code>로 머지했기 때문에 항상 머지 커밋이 남게 된다. 그래서 머지커밋을 안 남기려면 Pull Request 브랜치를 로컬에 가져와서 fast-forward로 머지한 뒤에 이를 푸시하는 방식으로 머지해야만 가능했기에 메인테이너들이 아주 바빠지게 되지만 Node.js는 항상 이렇게 했다. 그래서인지 일직선으로 유지되는 히스토리가 더 멋지다고 생각했고 머지 커밋은 나한테는 불필요한 정보가 남는 걸로 보여서 장황하게 느껴졌다.</p>
<p>물론 브랜치를 나누어서 작업한다는 것이 보통 하나의 작업 단위가 되기 때문에 머지 커밋을 남기면 이 작업 단위에 커밋이 여러 개 있다고 하더라도 이를 분리해서 볼 수 있고 분리되어 있으므로 필요하다면 한꺼번에 revert 할 수도 있다. 머지 커밋이 없는데 revert 한다면 커밋을 일일이 찾아야 한다. 하지만 현실에서는 머지를 통째로 revert 하는 일이 많지 않기도 하고 머지를 통째로 revert 하기보다는 수정 커밋이 새로 올라가는 게 더 자주 있는 일이라고 생각했다.</p>
<p>혼자 할 때는 얼마든지 내가 원하는 대로 히스토리를 관리할 수 있지만 협업하면 얘기가 달라진다. 얘기했듯이 GitHub의 Pull Request를 사용하면 항상 머지 커밋을 남길 수밖에 없었다. <a href="https://github.blog/2016-04-01-squash-your-commits/">Squash로 머지할 수 있는 기능은 2016년에 와서야 추가</a>되었다.</p>
<p>그렇다 보니 머지 커밋을 싫어하는 내 취향과 관계없이 GitHub에서 협업할 때는 머지 커밋을 남기게 되었다. GitHub 사이트에서 머지 버튼 누르는 게 훨씬 편하고 모두가 Git을 잘 다루는 것도 아니니까 "머지는 버튼으로 안 하고 로컬에 받아서 fast-forward로 머지합니다"라고 할 수도 없어서 어쩔 수 없이 그냥 머지 커밋을 쓰게 되었다.<br />
<br></p>
<h2>Rebase</h2>
<p>Rebase는 Git의 아주 훌륭한 기능이지만 Git을 배울 때 가장 어려워하는 기능이기도 하다. 예전에 Git 강의를 할 때도 Rebase를 알려주는 게 나을지 고민을 정말 많이 하곤 했다. 처음부터 Rebase 익히지 않는 게 나을 수도 있지만 Git을 제대로 쓰려면 Rebase를 필수라고 생각하고 있긴 하다.</p>
<p>Git을 쓰면서 히스토리 관리를 중요하게 생각하는 편이라 코드 리뷰할 때 커밋 메시지도 리뷰하고는 했다. 영어를 잘하는 건 아니니까 커밋 메시지의 문법을 보는 건 아니지만 WIP 라던가 아무 의미 없는 커밋 메시지를 가진 커밋이 쌓이는 건 싫어했다.</p>
<p>내가 커밋할 때도 Pull Request를 올리면서 모든 커밋은 인터랙티브 Rebase로(<code>git rebase -i</code>)로 히스토리를 다시 정리했다. 작업을 하면서 <code>A</code>-<code>B</code>-<code>C</code> 순으로 작업을 하는데 놓친 부분이나 수정할 게 생기면 <code>A</code>-<code>B</code>-<code>C</code>-<code>A'</code>-<code>B'</code>-<code>A''</code>-<code>C'</code> 형태로 커밋 히스토리가 복잡해 지는 게 인터랙티브 Rebase를 써서 순서를 바꾸고 커밋을 합쳐주면 다시 A-B-C 처럼 만들 수 있다.(<code>--fixup</code> 와 <code>--autosquash</code>를 쓰면 편하다.) 그래서 결과적으로 보면 처음부터 모든 경우를 고려해서 순서대로 작업한 것 같은 히스토리가 만들어지게 되는데 커밋 메시지는 모든 작업 과정을 다 담는 것이 아니라 나중에 이해할 수 있게 정리된 히스토리여야 한다고 생각하기 때문이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/4462388846.jpg" width="450" height="293" alt="사용자 삽입 이미지" title="" /></p>
<p>머지에서 피곤하게 느낀 건 위와 같은 상황이었다. Pull Request를 올리려고 작업을 하다 보면 작업 시간이 몇 시간에서 며칠이 걸리기도 하니까 다른 사람들의 작업도 기본 브랜치에 머지되면서 브랜치의 각 커밋이 흩어지게 된다. Git 도구를 쓰면 못할건 아니지만 머지 커밋은 상단에 바로 보이지만 그 브랜치에서 진행된 커밋을 보려고 하면 저 아래에 있어서 보기 쉽지 않은 게 싫었다. 당연히 협업자가 많으면 머지 커밋은 더 많이 생기고 히스토리는 복잡해진다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8210372704.jpg" width="450" height="291" alt="사용자 삽입 이미지" title="" /></p>
<p>이 문제를 해결하려면 Rebase를 하면 된다. 머지 하기 전에 Rebase를 하면 기본 브랜치를 대상으로 커밋을 다시 하게 되므로 자연히 커밋이 모이게 되고 이후에 머지를 하면 머지 커밋과 해당 브랜치의 커밋이 한 곳에 모이게 된다.</p>
<p>하지만 이것도 위에 Rebase로 커밋 히스토리를 정리하자는 것과 같은 상황이기 때문에 나 혼자서는 지킬 수 있는 규칙이지만 협업할 때 모두가 이렇게 하라고 하는 건 어려운 문제가 된다. 그래서 처음에는 Rebase 요청이나 커밋 히스토리에 대한 정리에 대한 요청도 많이 하곤 했지만, 시간이 지나면서 점점 커밋 히스토리를 너무 신경 쓰지 않으려고 노력하게 되었다.</p>
<p>Pull Request에서 Rebase를 적극적으로 사용하지 않게 된 이유가 또 하나 있는데 코드 리뷰 때문이었다. 예를 들어 변수명에 오타가 있다가 있다고 코드 리뷰를 받았을 때 이를 수정해서 올리면 새로 올라온 커밋만 보고 리뷰대로 오타가 수정되었구나!' 할 수 있다. 하지만 내가 이걸 수정한 다음 인터랙티브 Rebase로 커밋을 원래 커밋에 다시 합친 뒤에 force push를 하면 리뷰한 사람 입장에서는 전체 diff를 다시 보면서 제대로 수정되었는지 확인하기가 쉽지 않아진다. <strong>내가 가진 Rebase 습관이 Git 관점에서는 맞는다고 생각하지만, 협업 관점에서는 오히려 문제가 되기 때문에 Rebase를 너무 적극적으로 안 하기 시작했다.</strong><br />
<br></p>
<h2>Squash</h2>
<p>GitHub Pull Request가 Merge 커밋을 남기게만 동작하다가 <a href="https://github.blog/2016-04-01-squash-your-commits/">2016년 4월에 Squash and merge</a> 기능이 나오고 <a href="https://github.blog/2016-09-26-rebase-and-merge-pull-requests/">2016년 9월에는 Rebase and merge</a>가 나왔다.</p>
<p>Squash는 Pull Request의 모든 커밋을 합쳐서 하나의 커밋으로 만드는 기능이고 Rebase는 타겟 브랜치를 기준으로 모든 커밋을 다시 커밋(rebase)해서 fast-forward 머지가 가능한 상태로 만든 뒤에 머지하는 기능이다. Rebase and merge는 앞에서 말한 대로 내가 기다리던 기능이었기 때문에 나는 이쪽을 더 선호했고 그 덕에 커밋 히스토리를 한 줄로 만들 수 있게 되었다. 하지만 여전히 Pull Request에서 코드 리뷰와 관련해서 불필요한 커밋을 남길 수밖에 없는 문제는 해결되지 않았다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5173110327.jpg" width="500" height="308" alt="사용자 삽입 이미지" title="" /></p>
<p>그런 고민을 하던 중 지인이 자기 팀에서는 Squash를 기본으로 사용한다는 것을 알고 Squash에 관심을 가지게 되었다. Squash를 사용해 보면서 팀 단위로 협업할 때는 Squash를 선호하게 되었다.(여전히 개인 프로젝트에서는 Rebase and merge를 선호한다.) 아까 말한 대로 <strong>머지 커밋을 사용하지 않을 수 있으면서 코드 리뷰를 위해 Pull Request 브랜치에서는 히스토리 조정을 하지 않고 머지할 때는 하나의 커밋으로 합쳐서 최종 히스토리는 깔끔하게 유지할 수 있다. 협업 때 규칙을 합의 보고 지키기 위해서는 규칙이 너무 복잡하지 않아야 하는데 그런 부분에서도 Squash는 장점이 많다.</strong> 가끔 까먹을 때도 있지만 Squash로 합칠 때 커밋 메시지는 다시 조정하는 편이다. 안 그러면 커밋 메시지에 Pull Request에 포함된 모든 커밋 메시지가 다 포함된다.</p>
<p>더군다나 Pull Request의 변경 사항을 너무 크게 만들지 않게 하는 데도 유용하다. 기본적으로 코드 리뷰를 원활하게 하기 위해서는 Pull Request의 변경 사항은 너무 크지 않아야 한다. 그동안 Rebase로 히스토리 조정을 많이 해오면서 두 가지 목적의 작업은 거의 한 커밋에 안 섞는 편이다. 코드 수정을 하다가 리팩토링 할게 보인다거나 컨벤션 일부를 수정해야 해서 스타일 설정 파일을 수정해야 한다고 했을 때 이 부분만 따로 작업해서 Pull Request를 올리고 기능 추가나 버그 수정은 해당 변경 사항만 담기도록 노력하고 있다.</p>
<p>그렇기에 <strong>Squash를 기본 규칙으로 가게 하면서 하나의 커밋(Pull Request 내에서는 여러 커밋이 있지만 결국은 하나로 합쳐지므로)에는 너무 많은 변경 사항이 담기지 않도록 서로 노력하는 게 더 쉬웠고 문제가 생겼을 때 Revert 할 단위도 커밋 단위로 만들 수 있게 되었다.</strong> 결국 Pull Request 하나가 하나의 원자적 단위가 되는데 Mitchell Hashimoto 말대로 모든 커밋이 빌드할 수 있어야 하는 건 중요하고 그래야 어느 커밋으로도 되돌아갈 수 있는데 Pull Request에서 모든 커밋에서 CI를 다 돌리진 않지만, Squash로 한다면 모든 커밋에서 CI가 통과했다는 보장을 할 수 있게 된다.</p>
<p>Pull Request를 잘게 쪼개는 건 아주 중요하고 이건 연습이 좀 필요하다고 생각한다. 코드 리뷰를 한다는 것은 언제 머지될지 모른다는 얘기가 되므로 Pull Request를 쪼개기 시작하면 Pull Request를 올려두고 이어진 작업을 하기 위해 이전 Pull Request가 필요하게 된다. 이를 <a href="https://www.youtube.com/watch?v=XRZPkYnWa48">Stacked Changes</a> 혹은 Stacked Pull Request라고 부른다. 나는 손이 느려서 그런지 예전부터 쪼개는 것에 익숙해져서인지 Stacked Pull Request는 잘 사용하지 않지만, Stacked Pull Request가 필요하다면 <a href="https://graphite.dev/">Graphite</a>같은 서비스도 도움이 된다. 관련한 도구에서는 제일 잘 만들지 않았나 싶다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1704?commentInput=true#entry1704WriteComment">댓글 쓰기</a></strong></p>공개SW 페스티벌 2023에서 발표한 "오픈소스에 기여할 때 알면 좋을 개발 프로세스"Outsiderhttps://blog.outsider.ne.kr/16972024-01-01T18:24:58+09:002023-12-11T23:26:46+09:00<p>지난 12월 1일 <a href="https://ossfestival.kr/">공개SW 페스티벌 2023</a>에서 "오픈소스에 기여할 때 알면 좋을 개발 프로세스"라는 제목으로 발표했다. 공개SW 페스티벌은 3년전인 2020년에도 <a href="https://blog.outsider.ne.kr/1517">"오픈소스 뒤에 메인테이너 있어요"</a>라는 제목으로 발표했었다.</p>
<p>이번에 발표 요청을 받고 고민을 많이 했다. 올해는 회사 일도 바빴고(핑계지만...) 집에 와서는 좀 쉬기 바빴던 한해라서 오픈소스 활동도, 사이드 프로젝트도 거의 못 했기 때문에 발표할 주제가 마땅치 않았고 이전에 발표한 주제를 또 하고 싶지는 않았다. 이런저런 고민을 하면서 김태곤 님한테도 연락받고 오랜만에 보는 분들도 꽤 오시는 것 같아서 사람들도 볼 겸 발표를 먼저 수락했다.(이후 이날 팀 회의가 있는 날일걸 깨달았지만 어쩔 수 없었다)</p>
<p>발표 수락을 하고 난 바로 <a href="https://blog.outsider.ne.kr/1695">미국으로 날아갔다</a>. 시차 적응도 하고 이것저것 하느라고 정신없이 보내다가 발표 제목을 내야 하는 날이 임박해서야 고민을 시작했다.</p>
<p>정확히는 몰랐지만, 참석자는 오픈소스를 잘 모르는 초심자들이 많을 것 같았기에 쉬운 주제로 해야 할 것 같았고 뭘 하면 좋을까 고민하다가 프로세스 생각이 났다. CI나 CLA처럼 사소하고 별거 아닌 내용이긴 하지만 또 전혀 모르면 처음에는 이해하기 어려운 프로세스를 정리해서 한번 설명하면 가볍게 들으면서 한번 듣고 나면 좀 도움이 되지 않을까 싶어서 말할 내용을 몇 가지 정리해 보니 발표할 수 있을 것 같아서 주제를 요약해서 보냈다.</p>
<script defer class="speakerdeck-embed" data-id="9aebedf4676e472b9693680b74d603d1" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>발표 자료를 만들면서는 역시 어려웠다. 항상 그렇듯이 발표 스토리가 꽤 잡혀있지 않으면 만들면서 스토리 라인 잡느라고 고생하는 편인데 이번에도 역시 그랬다. 그리고 사실 내용이 너무 쉬운 내용이라 이걸 발표하는 게 맞나 하는 생각을 장표 한 장 만들 때마다 생각했다. 그래도 정리하다 보니 발표 분량은 나왔고 현장에서 발표했는데 이번엔 연습을 많이는 못 해서 말을 좀 절었던 것 같다.</p>
<p>전체 사람들의 느낌은 모르지만, 발표 끝나고 발표 잘 들었다고 질문하시는 분들이 있어서 그래도 몇몇 분에게는 도움이 되었구나 하고 안심했다.</p>
<p>이번에 특히 흥미로웠던 것은 마지막 세션이라 다른 발표자분들처럼 끝나고 질문 시간이 따로 없어서 질문이 없을 거로 생각했는데 다 끝나고 가려고 하는데 두 분이 와서 질문을 했다. 처음에는 회사에 대한 간단한 질문부터 시작해서 나도 편하게 얘기하고 있었는데 이번에 정부에서 한 행사에서 수상한(공개 SW 페스티벌은 한 해 동안 정부에서 시행한 오픈소스 사업의 시상식도 포함하고 있는데 어디서 수상했는지는 정확히 찾기가 어려웠다..) <a href="https://haetae.dev/">Haetae - Your Smart Incremental Tasks</a>라는 프로젝트였는데 의존성을 추적해서 영향받는 파일의 테스트만 돌리는 등의 작업을 해주는 증분 태스크 러너였다.</p>
<p>코엑스에 서서 1시간 정도 얘기를 나누었는데 설명을 듣다 보니 결국 노트북도 꺼내서 설명을 듣게 되었고 가볍게 만들기 시작한 게 아니라 <a href="https://bazel.build/?hl=ko">Bazel</a>부터 빌드도구나 태스크 도구에 대해서 오랫동안 고민하고 어떻게 만들어야 하는지 긴 준비 끝에 만들었음을 느낄 수 있었고 써보진 않았지만, 퀄리티도 꽤 좋아 보여서 정식 릴리스가 기대되는 프로젝트였다.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/zOKx-Kj8mww?si=OV7hddhY6lpoX9xM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
<p><br></p>
<p><strong><a href="https://blog.outsider.ne.kr/1697?commentInput=true#entry1697WriteComment">댓글 쓰기</a></strong></p>Atom 개발자가 만든 텍스트 에디터 ZedOutsiderhttps://blog.outsider.ne.kr/16652023-04-13T02:32:58+09:002023-04-13T02:32:58+09:00<p><a href="https://zed.dev/">Zed</a>라는 텍스트 에디터가 지난달에 퍼블릭 베타로 공개되었다. VS Code가 거의 대세가 된 가운데 또 새로운 에디터에 아주 큰 관심은 없었지만(난 아직 VS Code로 넘어가지도 못했다.) GitHub에서 Atom을 만들던 사람들이 만든 에디터라는 것을 알고 관심이 갔다.<br />
<br></p>
<h1>Atom</h1>
<p>Atom은 GitHub이 2014년에 공개한 텍스트 에디터이다. Atom은 웹 기술을 기반으로 만들어졌는데 Atom을 처음 보았을 때 <a href="https://blog.outsider.ne.kr/1035">살펴본 느낌</a>도 기록으로 남겨두었는데 GitHub이 왜 텍스트 에디터를 만들는지 관해 궁금했던 기억이 난다. GitHub과 텍스트 에디터가 잘 어울리지만, 당시 Sublime Text도 있고 딱히 텍스트 에디터가 여기서 무엇이 달라질지 잘 상상을 못했기에 GitHub의 의도를 알 수 없었다. 그래도 당시에 GitHub이 만드는 Atom 생태계에 관심이 생겨서 <a href="https://blog.outsider.ne.kr/1039">패키지도 만들어서 공개</a>했다.</p>
<p>작년 말에 <a href="https://github.blog/2022-06-08-sunsetting-atom/">Atom 프로젝트가 공식적으로 종료</a>되었지만 첫 릴리스 후 9년이 지난 지금 Atom이 텍스트 에디터 생태계와 데스크탑 앱에 얼마나 많은 영향을 끼쳤는지 알고 있다. Atom에서 웹 기술로 데스크탑 기반의 앱을 만드는 기반만 따로 추출해서 Atom Shell이 나왔고 <a href="https://www.electronjs.org/blog/electron">이 Atom Shell은 2015년 Electron으로 바뀌었다</a>. 그리고 호불호가 있지만 수년간 꽤 많은 데스크탑 앱이 Electron으로 만들어졌고 지금 텍스트 에디터의 대세가 된 VS Code도 Electron 기반으로 만들어졌다.</p>
<p>지난 2월 Atom 프로젝트가 종료되는 걸을 추모(?)하며 GitHub의 창업자 중 한 명인 <a href="https://github.com/defunkt">Chris Wanstrath</a>가 <a href="https://twitter.com/defunkt/status/1622027796301385729">Atom의 과거 얘기</a>를 했고 다음은 해당 트윗 스레드를 번역한 내용이다.(DeepL의 도움을 받았다.)</p>
<blockquote>
<p>2008년 <a href="https://atomicity.s3.amazonaws.com/atomicity1.mp4">Atom의 아주 초기 프로토타입</a>이며 원래는 Atomicity라고 불렀습니다.<br />
2008년 Atomicity 작업을 시작했지만, GitHub에 집중하기 위해 중단했습니다. 또한, <frame>에서 Cocoa와 JS를 연결하려다가 이상한 버그도 만났습니다.<br />
저는 2011년 이 일을 다시 시작했는데 그사이에 웹, JS, JavaScript 코어가 많이 발전했습니다. <a href="https://github.com/atom/atom/commit/3a09528a62f29e86bc15140a13d1bdbd9322e0e9">atom/atom에 올린 첫 커밋</a>에서도 에디터를 Atomicity라고 불렀습니다.<br />
이 기간에 Corey와 Atomicity를 해킹하기 시작했습니다. Corey는 GitHub에서 일했지만, 저희는 부업으로 에디터를 작업하고 있었습니다. GitHub에서 텍스트 에디터를 출시하는 것이 맞는지 확신은 없었는데 그냥 에디터를 원했습니다.<br />
어느 날 TextMate의 "mate"나 Sublime의 "subl"처럼 터미널에서 파일을 열 방법을 만들고 싶어서 "atom"이라는 스크립트를 만들었고 "이보다 더 좋은 편집기 이름은 없겠다"고 생각했습니다.<br />
그러던 중 넷플릭스가 PlayStation, TV 등의 앱을 웹 브라우저로 포장하여 출시했다는 기사를 읽었습니다. 덕분에 핵심 앱을 웹 기술로 작성하고 여러 플랫폼에 배포할 수 있었습니다.<br />
UI에 새로운 flexbox HTML 기능을 사용하고 싶었지만, 모든 브라우저가 이를 지원하는 것은 아니었습니다. 그래서 넷플릭스를 생각하면서 "젠장! 그냥 해버려!"라고 생각하고 Atom에 자체 버전의 Chrome을 탑재하기로 했습니다.<br />
저희는 이 기술이 텍스트 편집기뿐만 아니라 모든 종류의 앱을 작성하는 데 사용될 수 있다는 것을 금방 깨달았습니다. 첫 번째 버전의 Electron은 "RadFish"라고 불렀고 Corey가 프로젝트에 부여하는 이름입니다. 나중에 AtomShell로 그다음에는 Electron으로 바뀌었습니다.<br />
몇 년 전 Nathan Sobo를 만났고 그가 매우 똑똑하다는 것과 Ruby로 에디터를 만들고 싶어 한다는 것을 알고 있었습니다. 그래서 한번은 Corey와 Atom에 대한 진정한 비전이 생겼을 때 저는 Nathan Sobo에게 술을 마시면서 비전을 현실로 만들기 위해 함께 일하자고 제안했습니다.<br />
그는 거절했습니다.<br />
하지만 계속 이야기를 나누었고 결국 관심을 보였습니다. 그때쯤 실제 GitHub 프로젝트가 되길 바랬기에 크롬 개발자 도구로 Atom을 해킹하고 CSS로 UI를 변경하는 데모 영상을 만들었습니다. 2011년이었으므로 배경 음악은 dubstep이었습니다. 당연합니다.<br />
저는 데모 영상을 회사내에 공유했고 Nathan을 고용했습니다. Nathan과 Corey는 풀타임으로 Atom을 시작했습니다. 어느 순간 Kevin Sawicki와 Jerry Chen이 팀에 합류했습니다.<br />
Atom은 2014년 5월에 공식 출시되어 8년 후인 2022년 12월에 서비스를 종료했습니다.<br />
Atom은 사라졌지만 그 영향력은 계속 이어집니다. Atom이 없었다면 Electron도 없고 VS Code도 없었을 겁니다. 한정된 리소스로 이룬 성과가 정말 자랑스럽습니다.<br />
아, 그리고 Nathan Sobo는 여전히 그 일을 하고 있습니다. 그는 Atom을 만들면서 배운 교훈을 활용하여 더 빠르고 협업적인 편집기를 만들고 있습니다. Zed를 확인해 보세요.</p>
</blockquote>
<p>Atom의 역사도 흥미롭지만 마지막에 언급한 Zed가 글 앞에서 언급한 그 Zed다.<br />
<br></p>
<h1>Zed Industries</h1>
<p><a href="https://zed.dev/">Zed Industries</a>는 앞에서 언급된 Nathan Sobo가 만든 회사다. <a href="https://zed.dev/team">Zed의 팀 소개</a>와 <a href="https://www.linkedin.com/in/nathan-sobo-92b46720/">LinkedIn</a>을 보면 Nathan은 2011년부터 Atom 텍스트 에디터를 만들고 2018년까지 이끌었다. 그 이후 21세기 터미널이라는 <a href="https://www.warp.dev/">Warp</a>에 잠시 있었지만 직접 회사를 시작하고 싶어서 2011년 3월 Zed Industries를 창업했다.</p>
<p>이 회사에서 Zed 텍스트 에디터를 만들고 있는데 <a href="https://zed.dev/about">회사 소개</a>에 나온 비전을 보면 목표로 하는 것을 이해할 수 있다.</p>
<ul>
<li>미션 크리티컬한 도구는 반응성이 뛰어나야 한다. 거의 느끼지 못할 정도의 멈춤도 하루 동안 쌓이면 불필요한 스트레스를 유발한다.</li>
<li>실시간 협업이 더 나은 소프트웨어를 만든다. Pull Request도 나름의 역할이 있지만 함께 코딩하고 싶을 때가 있고 협업이 쉬워야 한다. 코드에 관한 다른 사람의 관점이 듣고 싶다면 메시지 보내듯이 쉽게 코딩할 수 있어야 한다.</li>
<li>대화는 코드와 가까운 곳에서 이뤄져야 한다. 코드에 관해 얘기하려고 GitHub에 커밋/푸시하거나 채팅 앱에 코드를 붙여 넣는 불편함 대신 에디터에 채팅을 통합하면 코드에 관해 쉽게 대화할 수 있다.</li>
<li>에디터는 사라져야 한다. 우리의 목표는 각 코딩 작업을 가장 효율적으로 수행할 수 있는 방법을 찾아 접근성을 높이는 것이다. 즐거운 코딩을 방해하지 않는 에디터에서 시작된다.<br />
<br></li>
</ul>
<h1>Zed 살펴보기</h1>
<p>이제 퍼블릭 베타이기 때문에 <a href="https://zed.dev/download">다운로드 페이지</a>에서 바로 다운로드 받을 수 있다. Rust로 만들어진 텍스트 에디터인데 현재는 macOS만 지원한다. 내가 테스트한 현재 버전은 0.80.6이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5199927816.jpg" width="750" height="763" alt="Zed 화면" title="" /></p>
<p>에디터의 모습은 보통 모는 익숙한 모습이다.(에디터가 크게 다를 것은 없으므로...) 아직 확장이 없기 때문인지 기본에서도 다양한 테마를 선택할 수 있고 키 매핑도 VS Code, Atom, JetBrains, Sublime Text, Textmate 중에서 선택할 수 있다. <a href="https://zed.dev/features#vim">Vim 모드</a>는 아직 작업 중이라고 하는데 설정에 <code>"vim_mode": true</code>를 추가하면 지금도 사용할 수 있는데 neovim과 비교하며 계속 개발 중이라고 한다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6768334860.jpg" width="750" height="626" alt="Zed" title="" /></p>
<p>프로젝트 코드를 열면 위와 같은 화면이 된다. Atom의 경험이 있기 때문인지 첫 퍼블릭 베타임에도 품질은 꽤 괜찮아 보인다. 물론 제대로 된 평가하려면 일정 기간동안 메인 에디터로 개발을 해봐야 알 수 있을 것 같다. 위 Kubernetes 프로젝트를 열면서 처음은 Go 열었는데 자동으로 Go 랭귀지 서버를 다운로드 받아서 사용 경험을 꽤 자연스럽게 유지하는 걸로 느껴진다.</p>
<p>Zed의 기능은 <a href="https://zed.dev/features">홈페이지</a>에서 확인할 수 있는데 아직 주로 Zed로 개발하지 않아서 그런지 명확히 눈에 띄는 기능은 많지 않다. 에디터가 갑자기 혁신적이기도 쉽지 않고... 상세 기능은 시간을 두고 더 살펴봐야 알 수 있을 것 같은데 기본적인 기능은 다 갖추고 있는 것으로 보이고 계속 얘기하는 대로 속도도 괜찮게 느껴진다.(Zed는 속도를 제일 중요하게 생가하고 있는 걸로 보인다.)<br />
<br></p>
<h2>협업 기능</h2>
<p>가장 독특한 기능은 협업 기능이 내장되어 있다는 부분이다. 다른 에디터나 IDE는 보통 플러그인으로 이를 제공하는데 Zed에서는 기본으로 내장되어 있다. 아직 익스텐션도 없고 다른 기능은 크게 특별하지 않아서 협업 기능만 살펴보자.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/9532171500.jpg" width="340" height="174" alt="사용자 추가" title="" /></p>
<p>Zed를 로그인은 GitHub 로그인과 연동되는데 로그인하고 나면 위처럼 사람 아이콘이 나타난다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6224038083.jpg" width="335" height="146.5" alt="사용자 검색" title="" /></p>
<p>여기에 GitHub 유저명을 입력하면 사용자를 찾을 수 있다. 물론 Zed에 회원가입을 한 사용자만 나타나는데 Zed가 계속 인기 있으면 가능한 방식인지는 잘 모르겠다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/7470321808.jpg" width="335" height="167" alt="사용자 등록 요청" title="" /></p>
<p>사용자를 추가하면 요청 상태로 나오게 되고 상대 쪽에서 수락을 하면 아래처럼 Online 상태를 확인할 수 있게 된다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/1356219862.jpg" width="335" height="162.5" alt="친구로 등록된 사용자" title="" /></p>
<p>Zed의 비전에 나왔듯이 협업을 꽤 중요하게 생각하는 걸 알 수 있는데 Contact에서 원하는 사용자를 클릭하면 바로 현재 프로젝트를 공유할 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/9264225697.jpg" width="430" height="100.5" alt="프로젝트 공유 요청 알림" title="" /></p>
<p>당연히 상대 쪽에서 승인을 해야하고 승인하면 요청한 쪽에서도 아래와 같이 연결 상태가 표시된다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/1692943456.jpg" width="370" height="197" alt="연결된 협업 세션" title="" /></p>
<p>두 사용자가 연결되면 아래처럼 우측 상단에 각 사용자의 아이콘이 표시되고 각 사용자의 커서도 표시가 된다. 혼자서 테스트해 본거지만 각 사용자의 동시 수정에도 꽤 잘 대응한다. 앞으로는 지원하는지 모르겠지만 보이스 챗이나 채팅은 아직 따로 없다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6163749362.jpg" width="750" height="404" alt="Zed의 동시 편집" title="" /></p>
<p>이 정도는 지금도 각 에디터에서도 협업 기능을 추가하고 있어서 가능한 부분인데 내장되어 있다는 점이 특정인 거 같다. 혼자 테스트할 때는 나쁘지 않을 데 실제로 협업해봐야 알 것 같은데 코드를 같이 볼때 얼마나 유용할지는 업무에서 써봐야 더 느낌이 올 것 같고 아직은 그렇게 강력하게 느껴지진 않는다. 우측 상단에 사용자 프로필을 누르면 해당 사용자의 스크롤이나 이동도 따라가면서 같이 협업할 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/2795250861.jpg" width="750" height="467" alt="Zed의 화면 공유" title="" /></p>
<p>협업할 때는 코드 에디터만 연동이 되기 때문에 사용자 아이콘 옆에 스크린 아이콘을 누르면 화면을 공유하는 것도 가능하다. 협업하다가 필요시 화면 공유를 바로 할 수 있는 건 괜찮아 보이는데 약간 테스트해봤을 때는 화면 공유의 속도와 품질이 좋지 않아서 실제로 유용하진 않을 것 같았다. 선명하지 않게 흐린 상태가 꽤 많이 유지 되어서 화면 공유는 했지만, 글자는 알아보기 어려웠다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8699823433.jpg" width="300" height="109" alt="Zed의 코드 액션" title="" /></p>
<p>추가로 코드 블록을 선택하면 좌측에 번개 표시가 나오는데 Zed에서는 이를 Code Actions라고 부른다. 해당 코드에 대해서 할 수 있는 동작이 있으면 이게 표시가 되고 <code>cmd</code> + <code>.</code>로도 똑같이 할 수 있다.<br />
<br><br></p>
<p>Zed의 첫 인상은 나쁘지 않았고 VS Code의 아성을 무너뜨릴 수 있을지는 모르겠지만 Atom을 봤을때도 비슷한 생각을 했기 때문에 Zed도 관심있게 보려고 하고 있다. 일단 빠른 속도만 계속 유지해줘도 좋을 것 같다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1665?commentInput=true#entry1665WriteComment">댓글 쓰기</a></strong></p>GitHub Copilot for CLI 소개Outsiderhttps://blog.outsider.ne.kr/16632023-03-31T20:51:21+09:002023-03-31T20:51:21+09:00<p>이제 GitHub Copilot은 쓰진 않더라도 대부분 존재는 알고 있을 정도로 보급이 된 편인데 <a href="https://githubnext.com/projects/copilot-cli/">GitHub Copilot for CLI</a>는 CLI에서 AI에 돕는 프로젝트로 간단히 말하면 자연어를 입력하면 CLI 명령어를 만들어 준다.</p>
<p><a href="https://githubnext.com/">GitHub Next</a>에서는 다양한 실험 프로젝트를 진행하고 있는데 얼마 전에 공개된 <a href="https://github.com/features/preview/copilot-x">GitHub Copilot X</a>는 AI로 소프트웨어 개발을 돕는 여러 Copilot 제품군의 집합이라고 생각할 수 있고 <a href="https://githubnext.com/projects/copilot-cli/">GitHub Copilot for CLI</a>도 GitHub Copilot X에 포함되어 있다.<br />
<br></p>
<h1>GitHub Copilot for CLI 설치</h1>
<p><a href="https://githubnext.com/projects/copilot-cli/">GitHub Copilot for CLI</a>는 현재 waitlist에 등록하고 승인받아야 사용할 수 있고 얼마전에 승인을 받아서 사용해 볼 수 있게 되었다.</p>
<p>GitHub Copilot for CLI는 소스 코드가 공개되어 있지는 않은데 <a href="https://www.npmjs.com/package/@githubnext/github-copilot-cli">npm에 배포되어 있으므로</a> npm으로 설치해서 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ npm install -g @githubnext/github-copilot-cli
npm WARN deprecated @opentelemetry/api-metrics@0.33.0: Please use @opentelemetry/api >= 1.3.0
added 233 packages, and audited 234 packages in 12s
47 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
</code></pre>
<p>나는 Node.js 개발을 하기 때문에 쉽게 설치했지만 Node.js 환경이 필요하기 때문에 접근성이 좋다고 할 수는 없다. 아직 실험단계로 npm을 유지하고 있는지 이후로도 npm 배포를 유지할 예정인지는 잘 모르겠다.</p>
<p>어쨌든 설치했으니 명령어를 <code>github-copilot-cli</code> 명령어를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ github-copilot-cli
Usage: Copilot CLI [options] [command]
A CLI experience for letting GitHub Copilot help you on the command line.
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
auth Retrieve and store a GitHub Access Token.
alias <shell path> Add convenient GitHub Copilot CLI aliases to your shell.
what-the-shell [options] <query...> Use GitHub Copilot to get shell commands from natural language
descriptions.
git-assist [options] <query...> Translate a natural language description of a git command to an actual git
command.
gh-assist [options] <query...> Convert plain english to GitHub CLI commands.
help [command] display help for command
$ github-copilot-cli --version
0.1.30
</code></pre>
<p>npm에 배포되어 있으므로 설치는 누구나 할 수 있지만 실제 사용은 GitHub 계정을 인증해야 한다. <code>github-copilot-cli auth</code> 명령어로 인증을 하면 GitHub 페이지에서 인증해서 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ github-copilot-cli auth
Copy this code: CDE3-724F
Then go to https://github.com/login/device, paste the code in and approve the access.
</code></pre>
<p><br></p>
<h1>GitHub Copilot for CLI 설정</h1>
<p>앞의 명령어 목록을 보면 3개의 명령어가 있음을 알 수 있다.</p>
<ul>
<li><code>github-copilot-cli what-the-shell</code>: 자연어를 쉘 명령어로 변환한다.</li>
<li><code>github-copilot-cli git-assist</code>: 자연어를 git 명령어로 변환한다.</li>
<li><code>github-copilot-cli gh-assist</code>: 자연어를 <a href="https://cli.github.com/">GitHub CLI</a> 명령어로 변환한다.(<a href="https://blog.outsider.ne.kr/1557">이전 글</a> 참고)</li>
</ul>
<p>GitHub Copilot for CLI에서 제공하는 기능도 이 3가지인데 쉘, Git, gh 3가지를 다르게 명령어를 사용해야 한다. 하지만 이 긴 명령어를 쓴다면 실제로 사용할 때 전혀 편하지 않을 것이다. 그래서 <code>github-copilot-cli</code>에서 별칭 기능을 제공한다.</p>
<p>아래 명령어를 쉘의 프로파일에 추가하면 되는데 나는 ZSH을 사용하고 있으므로 <code>$HOME/.zshrc</code> 파일에 아래 명령어를 추가했다.</p>
<pre class="line-numbers"><code class="language-bash">eval "$(github-copilot-cli alias -- "$0")"
</code></pre>
<p>이제 새로운 쉘을 열면 위 3가지 명령어에 대한 별칭을 사용할 수 있게 된다. <code>??</code> 명령어는 <a href="https://github.com/microsoft/Codex-CLI">Codex CLI</a>의 영향을 받았다고 한다.</p>
<ul>
<li><code>??</code> -> <code>github-copilot-cli what-the-shell</code></li>
<li><code>git?</code> -> <code>github-copilot-cli git-assist</code></li>
<li><code>gh?</code> -> <code>github-copilot-cli gh-assist</code><br />
<br></li>
</ul>
<h1>GitHub Copilot for CLI의 사용</h1>
<h2>쉘 명령어(<code>??</code>)</h2>
<p>쉘 명령어는 안 쓰던 건 까먹어서 구글링해서 쓰곤 하는데 이런 부분을 상당히 해소해 준다.</p>
<p>예를 들어 OS 버전을 확인해 본다고 해보자.(안타깝게도 한국어는 알아듣지 못한다.)</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
──────────────────── Command ────────────────────
cat /etc/os-release
────────────────── Explanation ──────────────────
○ cat is used to print the contents of a file to the terminal.
◆ /etc/os-release is the path to the file containing the OS release information.
❯ Run this command
Revise query
Cancel
</code></pre>
<p><code>?? show OS version</code>이라고 입력하자 <code>cat /etc/os-release</code>라는 명령어를 만들어 주었고 아래는 해당 명령어에 대한 추가 설명이 나온다. <code>Run this command</code>를 선택하고 엔터를 누르면 아래처럼 정말 실행할 것인지를 묻는다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
──────────────────── Command ────────────────────
cat /etc/os-release
────────────────── Explanation ──────────────────
○ cat is used to print the contents of a file to the terminal.
◆ /etc/os-release is the path to the file containing the OS release information.
This will execute the suggested command in your shell.
Are you sure? (y/N)
</code></pre>
<p><code>y</code>를 누르면 실행되는데 약간 귀찮기는 하다. 아마도 파일 삭제 같은 위험한 명령어를 실행할 수도 있으므로 한번 확인 절차를 넣는 게 아닐까 싶은데 앞에서 명령어 실행을 선택하고 왔으니 바로 실행해도 되지 않을까 싶다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
──────────────────── Command ────────────────────
cat /etc/os-release
────────────────── Explanation ──────────────────
○ cat is used to print the contents of a file to the terminal.
◆ /etc/os-release is the path to the file containing the OS release information.
Hold on, executing commmand...
[bat error]: '/etc/os-release': No such file or directory (os error 2)
</code></pre>
<p>하지만 <code>cat /etc/os-release</code>는 Linux에서 사용하는 방식이므로 macOS에는 이런 파일이 없어서 명령어를 실행해도 오류가 난다. ChatGPT에 다들 익숙해지고 있다시피 구체적인 요구사항을 주는 게 좋고 나온 명령어가 맞는지 아닌지 검증도 필요하다. 원하는 명령어가 아닐 경우에 추가 정도를 더 제공할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
──────────────────── Command ────────────────────
cat /etc/os-release
────────────────── Explanation ──────────────────
○ cat is used to print the contents of a file to the terminal.
◆ /etc/os-release is the path to the file containing the OS release information.
Run this command
❯ Revise query
Cancel
</code></pre>
<p>아까 이 단계에서 <code>Run this command</code> 대신 <code>Revise query</code>를 선택하면 쿼리를 수정할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
──────────────────── Command ────────────────────
cat /etc/os-release
────────────────── Explanation ──────────────────
○ cat is used to print the contents of a file to the terminal.
◆ /etc/os-release is the path to the file containing the OS release information.
──────────────────── Revision ────────────────────
Enter your revision: for macos
</code></pre>
<p><code>Enter your revision</code>라는 말이 좀 헷갈리는데 전체 문장을 다 입력할 필요는 없고 추가 정보만 입력하면 된다. 위에서는 macOS에서 OS 버전을 보고 싶었으므로 <code>for macos</code>라고 입력했다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version
───────────────────── Query ─────────────────────
1) show OS version 2) for macos
──────────────────── Command ────────────────────
sw_vers
────────────────── Explanation ──────────────────
○ sw_vers is used to print information about the current macOS version.
❯ Run this command
Revise query
Cancel
</code></pre>
<p>이제 제대로 된 명령어가 나왔다. <code>Run this command</code>를 선택하면 명령어가 실행되고 OS 버전이 나온다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? show OS version for macOS
──────────────────── Command ────────────────────
sw_vers
────────────────── Explanation ──────────────────
○ sw_vers is used to print information about the current macOS version.
Hold on, executing commmand...
ProductName: macOS
ProductVersion: 13.2.1
BuildVersion: 22D68
</code></pre>
<p>글이라서 좀 장황했지만, GitHub Copilot for CLI의 인터페이스가 어떻게 동작하는지 보기 위해서 단계별로 찬찬히 봤고 모든 명령어는 다 이와 똑같이 동작한다.</p>
<p>좀 더 복잡한 요구사항을 전달해 보자. 현재 폴더에서 "terraform"이라는 문자열이 들어있는 모든 JS 파일을 찾도록 했다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? list only js files contain "terraform" in contents
──────────────────── Command ────────────────────
find . -name "*.js" | xargs grep -l "terraform"
────────────────── Explanation ──────────────────
○ find is used to list files.
◆ -name "*.js" stipulates that we search for files ending in .js.
○ | xargs passes the list of files to the next command.
◆ grep is used to search for a pattern in a file.
◇ -l means we only want the file names, not the lines that contain the pattern.
◇ "terraform" is the pattern we search for.
❯ Run this command
Revise query
Cancel
</code></pre>
<p>특정 포트를 사용하는 프로세스를 찾아보았다.</p>
<pre class="line-numbers"><code class="language-bash">$ ?? who use 3000 port
──────────────────── Command ────────────────────
sudo lsof -i :3000
────────────────── Explanation ──────────────────
○ sudo is used to run a command with elevated rights, to e.g. change system files.
◆ lsof is used to list open files.
◇ -i :3000 specifies that we want to list all files that are listening on port 3000.
❯ Run this command
Revise query
Cancel
</code></pre>
<p><br></p>
<h1>Git(<code>git?</code>)</h1>
<p>git 명령어는 <code>git?</code>를 사용한다. 사용 방법은 똑같으니 몇 가지 예시만 살펴보자. 다음은 커밋 히스토리에서 author 별로 커밋 카운트를 세어봤다.</p>
<pre class="line-numbers"><code class="language-bash">git? commit count by author
──────────────────── Command ────────────────────
git shortlog -s -n
────────────────── Explanation ──────────────────
○ git shortlog is used to list the commits in the current branch.
◆ -s specifies that we want to list the number of commits per author.
◆ -n specifies that we want to sort the authors by the number of commits
</code></pre>
<p><code>main</code> 브랜치만의 커밋 히스토리를 출력해 봤다.</p>
<pre class="line-numbers"><code class="language-bash">$ git? show history main branch only
──────────────────── Command ────────────────────
git log --oneline main
────────────────── Explanation ──────────────────
○ git log is used to list commits.
◆ --oneline specifies that we want a short listing.
◆ main specifies that we want to list the commits on the main branch
</code></pre>
<p>다음은 좀 잘못 나온 예시이다. 난 상대적 시간 대신 절대 시간으로 출력하길 원했지만 <a href="https://git-scm.com/docs/git-log#Documentation/git-log.txt---dateltformatgt"><code>--date</code></a> 옵션에 <code>relative</code>는 있지만 <code>absolute</code>는 없다. <code>iso</code>나 <code>local</code>같은 옵션을 주었어야 했지만 그렇게까진 해주지 않았다.</p>
<pre class="line-numbers"><code class="language-bash">$ git? show history with commit sha 3 length, author, absolute date and email
──────────────────── Command ────────────────────
git log --pretty=format:"%h %an %ad %ae" --abbrev-commit --date=absolute
────────────────── Explanation ──────────────────
○ git log is used to list commits.
◆ --pretty=format:"%h %an %ad %ae" specifies the format of the output.
◇ %h is the abbreviated commit hash.
◇ %an is the author name.
◇ %ad is the date of the commit.
◇ %ae is the author email.
◆ --abbrev-commit specifies that we want to abbreviate the commit hash.
◆ --date=absolute specifies that we want to use absolute dates.
</code></pre>
<p><br></p>
<h1>GitHub CLI(<code>gh?</code>)</h1>
<p><a href="https://cli.github.com/">GitHub CLI</a>는 아주 유용하기 때문에 애용해서 쓰는 편이지만 모든 명령어를 다 아는 것도 아니고 <code>gh</code>로 GitHub API도 사용할 수 있기 때문에 API를 사용할 때는 GitHub 문서를 봐야 했었는데 이럴 때도 유용할 것 같다.</p>
<p>다음은 <a href="https://github.com/github/docs">github/docs</a> 저장소의 워크플로우 목록을 조회하도록 했는데 API 조회후 <a href="https://stedolan.github.io/jq/">jq</a>로 데이터 정리까지 잘해주었다.</p>
<pre class="line-numbers"><code class="language-bash">$ gh? show workflow id and name from workflow list of github/docs
──────────────────── Command ────────────────────
gh api repos/github/docs/actions/workflows | jq '.workflows | map({id: .id, name: .name}) | .[]'
────────────────── Explanation ──────────────────
○ gh api is used to issue GitHub API requests.
◆ repos/github/docs/actions/workflows specifies that we want to list the workflows of the github/docs
repository.
◆ | jq uses the jq tool to process the response using a series of filters.
◇ .workflows selects the list of workflows in the response.
◇ | map(...) builds a new object for each workflow in the list with the following elements.
▪ id build from the existing id field.
▪ name is taken from the existing name field.
◇ | .[] selects the first element of the list.
</code></pre>
<p>현재 브랜치를 기준으로 Pull Request를 생성하는 명령어를 작성했다.</p>
<pre class="line-numbers"><code class="language-bash">$ gh? open pr from current branch
──────────────────── Command ────────────────────
CUR_BRANCH=$(git branch --show-current) gh pr create --fill --base=main --head=$CUR_BRANCH
────────────────── Explanation ──────────────────
○ CUR_BRANCH=$(...) is used to assign the output of the command inside $(...) to the variable CUR_BRANCH.
◆ git branch --show-current prints the name of the current branch.
○ gh pr create is used to create a new PR.
◆ --fill means that we want to fill in the PR title and description from the commit message.
◆ --base=main specifies that we want to merge into the main branch.
◆ --head=$CUR_BRANCH specifies that we want to merge from the current branch.
</code></pre>
<p><br></p>
<p>실제로 사용하면서 정확도를 좀 봐야겠지만(테스트로 해서 그런지 중간중간 동작하지 않는 명령어도 꽤 많이 나왔다.) 명령어 같은 경우는 검색해서 찾는 경우가 많으므로 GitHub Copilot만큼이나 꽤 유용할 것으로 보인다. 특히 설명도 잘 나와서 CLI를 배우는 데도 효과적일 것으로 생각된다. 한국어가 안 되는 건 아쉽지만, 터미널에서 한/영 전환하는 것도 귀찮긴 하니까 그렇게 까지 나쁘진 않다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1663?commentInput=true#entry1663WriteComment">댓글 쓰기</a></strong></p>