이 글은 2022년 4월 4일 트위터에 쓴 글을 백업한 것입니다.

control + h 와 back space

<c-j>,<c-m> 등의 이야기를 해봅시다. 이건 vim의 범위를 좀 벗어나는 이야기이기도 해서 조심스럽기도 한데요, vim의 범위가 어디인지를 이야기해보는 것도 흥미로운 주제이긴 합니다. 일단 <c-j> 이야기를 하려 하긴 한건데 <c-j>가 중요한 건 아니고 하기 전에 음…음 <c-h> 먼저 이야기를 해보죠.

<c-h>표기법이 생소한 사람에겐 알 수 없는 기호로 보이겠지만 알고 보면 별거 아닙니다. control + h 죠. 이걸 vim에서는 <c-h>로 표기합니다. 그런데 사실 이건 vim만이 아니고 아마 터미널에서 입력 문자를 표기할 때 쓰던 옛날 표기법같습니다. 그런데 제가 그렇게 나이 많은 사람은 아니고..

중요한 건 <c-h>control + h라는 것입니다. 터미널에서 뭔가 입력하다가 control을 누르면서 h 를 누르면 백스페이스를 누른 것처럼 왼쪽으로 문자가 지워지는 경험을 하신 분들이 많을 것입니다. 그리고 mac 빌트인 터미널의 설정에 들어가 보면 "Delete로 Control-H" 전송이라는 항목도 보입니다.

Apple terminal

그래서 터미널에서 뭘 쓰다가 백스페이스까지 손을 옮기기 귀찮을 때 그냥 <c-h>를 눌러서 지우곤 하는 거죠. 키보드 오른쪽 꼭대기에 있는 BS보다야 h가 가까우니까요. 그런데.. hBS의 관계는 알고보면 꽤나 흥미롭습니다. 이건 아스키코드 테이블을 달달 외우고 다니던 선배 개발자들이라면 상식으로 알고 계실 내용 같기도 한데요, 아스키 코드 테이블을 보면 관계를 미루어 짐작할 수 있습니다.

사진을 첨부했습니다. H를 찾아보면 10진수로 72 이고, 16진수로는 48 이라는 걸 알 수 있습니다. 그러면 백스페이스를 볼까요? BS는 10진수로 8이고, 16진수로도 8입니다.

ascii code table

이미지 출처: https://www.asciitable.com/

이제 48과 8을 이진수로 바꿔서 봅시다.

  • H : hex 48 => 0100 1000
  • BS : hex 8 => 0000 1000

비트 하나가 차이나죠? 사실 컨트롤 키는 눌렀을 때 왼쪽에서 두 번째 비트에 영향을 줍니다. 그래서 H 랑 컨트롤을 누르면 01000000이 되어 백스페이스가 되는 것이죠.

이거랑 비슷하게 다른 문자들도 살펴보면 재미있습니다. vim에 익숙한 분들이라면 Esc 키의 대용으로 <c-[>를 사용할 수도 있다는 것을 아실 겁니다. 왜 이렇게 될까요?

그야 [ 가 hex로 5B0101 1011 이고, Escape가 1B0001 1011 이기 때문입니다.

아무튼 이런 이유로 터미널에서 <c-h>를 입력하는 건 백스페이스처럼 작동하는 단축키를 누른 게 아닙니다. 백스페이스 문자 자체를 생산한거죠. 그래서 control+h 는 단축키가 아니라고도 볼 수 있죠. 이건 터미널 환경 안에서 돌아가는 많은 프로그램에도 적용되며 당연히 vim에서도 그렇습니다.

이런 백스페이스 문자 생산의 전통은 아직도 이어져 내려와서 이게 통하는 애플리케이션도 꽤 많습니다. 예를 들어 구글 크롬의 주소입력창에서 <c-h>를 입력해도 한 글자 한 글자 잘 지워지는 것을 볼 수 있습니다. 슬랙도 그렇고.. OS 자체가 기본으로 지원하는 경우도 있을 텐데 확신은 안 듭니다.

c-j 와 c-m, 그리고 c-i

그러면 터미널에서 제가 거의 매일 사용하는 키 하나를 또 예로 들어보죠. <c-j> 입니다. 터미널에서 입력해 보세요. 엔터처럼 작동하죠? 그렇습니다. j0100 1010 이고 여기에서 두번째 비트를 0 으로 바꾸면 0000 1010 이 되는데 이게 line feed 이기 때문입니다.

line feed 가 이렇다면 캐리지 리턴도 있겠죠? 네 <c-m>이 그렇습니다. m0100 1101. CR0000 1101 이죠. 그래서 터미널에서 뭐 입력하고 엔터를 누르는 게 귀찮을 때 저는 그냥 c-jc-m을 누릅니다.

제가 가끔 사진을 올리곤 하는 제 키보드의 menter 키캡이 끼워져 있는 이유이기도 합니다.

my keyboard

그러면 이제 I를 살펴보죠. I0100 1001 이고… 두번째 비트를 바꾸면 0000 1001 인데, 이게 탭입니다. vim 에서 레지스터에 문자열을 저장할 때 탭이 ^I 로 저장된 걸 볼 수 있는데 바로 이것이 그 이유입니다.

vim register

이미지를 올리고 보니까 제일 오른쪽에 ^J도 보이네요. 이제 이게 LF 라는 걸 알고 있으니 " 레지스터에 탭asdLF 가 들어갔다는 것을 판별할 수 있습니다.

그러면 이제 C도 살펴봅시다. c0100 0011 이고, <c-c>0000 0011 입니다. 즉 end of text 죠. 터미널에서 control + c 를 누를 때마다 뭐가 멈추죠. d 는? end of transmission. 이 모든 것들이 vim 안에서도 같거나 비슷한 의미를 같습니다.

근본은 타자기

한편 좀 별개의 이야기이긴 한데 CR, LF, TAB 등은 타자기에서 왔습니다. 캐리지 리턴은 타자기에서 아마도 타자기 헤드를 왼쪽 끝으로 옮기는 거고, 라인 피드는 타자기에서 종이를 한 줄 올려서 다음 줄을 입력할 수 있게 해주는 거였을 겁니다. (제가 타자기 세대가 아니라 강한 확신 표현을 못씀)

  • 타자기에서 한 줄을 다 썼다
    • CR: 캐리지를 왼쪽 끝으로 옮기고,
    • LF: 종이를 한 줄 위로 올린다.

지금은 타자기를 쓰고 있지 않으니 CR 이나 LF나 비슷하면서도 조금씩 다른 취급을 받고는 있지만, 그건 그렇다 치고 또 한 가지 흥미로운 것은 ^I 즉 탭입니다.

저는 2017년에 이런 특수문자들과 관련된 잡다한 사실들을 그럭저럭 수집해두는 글을 쓴 적이 있는데요, 이 글을 쓰다가 알게 된 사실입니다.

[[/special-chars#cr-lf]]

타자기에 들어가는 tab 의 특허 문서를 어쩌다가 찾아서 읽게 됐죠. 글쎄 이게 1900년이더라고요. 지금이 2022년이니까 122년 전에 탭이 발명되어 특허가 된 것입니다.

https://patents.google.com/patent/US720520

그 때 제가 이 문서를 읽고 제 블로그에 이렇게 썼습니다.

F.W Hillard가 1900년 8월 22일에 미국에서 특허를 냈다… 탭 스탑에서 캐리지를 멈춰주는 (금속) 막대기가 "tabulator-rack I"이다..

물론 이게 알파벳 I의 두번째 비트를 반전한 게 tab이 된 이유인지는 모릅니다. 걍 우연일 수도 있고, ascii code를 디자인한 사람들이 의도적으로 그렇게 배치했는도 모르고요. 다만 이걸 보니까 <c-i>가 탭이라는 건 확실히 외우게 됐죠.

vim은 터미널에서 돌아가는 프로그램

그래서 이제 vim 이야기로 돌아갑니다. vim은 기본적으로는 터미널에서 돌아가는 프로그램입니다. 그래서 vim의 키 입력은 터미널 환경의 영향을 강하게 받을 수 밖에 없어요. vim 안에서 표현되는 탭이나 esc<tab> 이 아니라 ^[, ^I, ^J 따위로 표현되는 건 이런 이유일 거고, 그래서 vim을 쓰다보니 이 두번째 비트 하나의 반전을 두고 머릿속에서 키 몇 개들의 연결이 생깁니다.

이런 연결이 생기면 어떤 이상한 동작을 발견했을 때 정말 알 수 없는 이유로 모르겠는데도 대강 알겠는 요상한 순간이 오기도 하는데요. 그런 것 중 하나가 vim에서 insert 모드를 빠져나갈 때 쓰는 키들에 대한 거죠. insert 모드를 탈출할 때 보통 esc를 쓰는데 사실은 esc[ 에서 비트 하나를 뒤집은 거니까 <c-[>를 입력하는 건 vim 고인물이라기보다는 터미널 고인물이고…

한번은 동료가 저에게 vim을 배우다가 <c-c>로도 insert 모드를 빠져나갈 수 있다는 사실을 발견하게 됩니다. 꽤 괜찮은 조합이긴 합니다. tabcontrol 키 스왑을 하고 있는 사람이라면 이건 esc보다 매력적으로 보일 수도 있는 키조합이니까요. 그런데 이건 end of text 이고, 따라서 그냥 insert 모드를 normal 모드로 바꾸는 것 이상의 불온한 느낌이 듭니다.

실제로 이걸 vim에서 신나게 쓰다 보면 normal 모드로 돌아가는 것 말고, 어떤 플러그인이나 백그라운드에서 뭘 해주는 걸 설정해놨을 때 <c-c>를 누른 시점에서 뭔가 중단됐다는 걸 알아차리게 됩니다.

그래서 가끔 쓰긴 하지만 늘 누르지는 않는 키가 되죠. (가끔 쓰면 약간 신나긴 합니다.)

한편 esc 이야기가 나온 김에 vim에서 esc 입력의 대안을 찾는 것과 관련된 가장 유명한 문서는 이것일 거라 생각합니다. esc 누르는 것 때문에 괴로웠던 분들이라면 아마 자신만의 해법들을 찾으셨을 거라 생각하지만, 입문자라면 이 문서는 빨리 보면 좋다고 생각해요.

https://vim.fandom.com/wiki/Avoid_the_escape_key

아 뭐지 무슨 이야기를 하려다가 여기까지 온거지. 아무튼 생각나는대로 주절주절하고 있는데 전 전공자도 아니고 그냥 vim이랑 터미널로 삽질을 하다가 알게 된 것들을 이야기하고 있습니다. 그러니 뭔가 사실과 다른 게 있거나 제가 잘못 알고 있던 게 있다면 알려주시면 감사히 듣겠습니다.

오늘 vim 이야기는 여기까지 하죠.

다음날 이어서..

오늘은 렉걸리는 이야기를 해보죠. 그렇습니다. vim에서도 렉걸리는 경우가 있어요. 여러 플러그인 기능을 연달아 사용하는 복잡한 매크로를 1000@@ 해서 만번 돌려버린다던가, 아니면 <c-c>를 적절하지 않은 순간에눌러버렸다던가, 아니면 설정을 잘못했다던가 하는 등의 온갖이유로 화면이 멈춥니다.

물론 그렇다고 완전히 멈춰버리는 경우는 거의 본 적은 없는 것 같고, 대체로 스크롤한 화면이 새로 그려지지 않고 잔상이 남는 형태가 흔합니다. 물론 렉의 정의를 화면이 제때 갱신되지 않는 것 정도로 넓은 의미로 말할 수 있다면 말이죠.

이럴 때에는 화면을 다시 그려야 하는데요, vim 스스로 화면을 다시 그려야 하는 때를 놓쳤으므로 수동으로 그리도록 명령을 내려줘야 합니다. 그리고 화면을 다시 그리는.. 제가 아는 방법으로는 두 가지가 있어요. 하나는 아주 쉬운 방법이고, 다른 하나는 대부분은 있는지도 모르는 방법입니다.

기왕 이야기를 하게 됐으니 희귀한 방법부터 알아보죠. :redraw 명령을 쓰면 화면을 새로 그립니다. 사람이 실제 입력해 쓰는 경우는 드물고 vim플러그인을 개발할 때만 가끔 쓰일 겁니다. 이걸 어떻게 알았냐면.. 저도 알고 싶지 않았어요. vim 게임을 만들다 알게 됐습니다.

https://github.com/johngrib/vim-game-code-break

vim에서 돌아가는 이 게임은 아주 단순한 while 루프를 도는데요, 사용자 입력을 받고, 게임 상태를 업데이트한 다음, redraw 작업을 반복할 뿐입니다. 원시적인 게임이지만.. 만들면서 redraw가 필요했죠.

vim-game-code-break에서 redraw를 사용한 코드

그러면 이제 화면을 다시 그리는 쉬운 방법도 알아봅시다. 이 방법은 너무 쉬운데요, <c-l>control+L을 입력하는 것입니다. 아마 어젯밤에도 제 트윗을 읽으신 분들은 이미 아스키 코드 테이블을 열어서 보고 계실 수도 있겠다는 생각이 듭니다.

16진수로 생각해 봅시다. J4A 입니다. K, L 이니까 L4C 겠죠. 4C를 컨트롤 키와 함께 누르면 비트 하나가 손상될테니 C가 될 겁니다. 아스키 코드 테이블에서 C를 찾아보죠. C는 Form Feed, new page 입니다. 타자기로 생각하면 새로운 종이를 끼우는 거죠. 즉 화면을 갱신하는 문자입니다.

그래서 vim에서 <c-l>을 누르면 화면을 새로 그리게 됩니다. :redraw 보다 입력하기 훨씬 쉽죠. vim을 쓰다가 뭔가 화면이 이상하거나 스크롤이 비틀린 것 같을 때 누르면 됩니다. 물론 이건 vim에서 그렇게 정의한 게 아니라 form feed 문자를 터미널이 받아들이는 방식 때문이기 때문에

"때문이기 때문에" 라니… 아무튼 방식이 그래서 vim 바깥 터미널에서도 <c-l>은 잘 작동합니다. 가끔 터미널 화면을 청소하려고 clear 명령을 입력하는 경우가 있죠. <c-l>을 누르면 clear 명령과 같은 결과를 얻을 수 있습니다. 그래서 저는 셸 스크립트를 작성할 때가 아니면 그냥 <c-l>을 씁니다.

그런데 말입니다(정색). 6년 전인가 vim을 쓰다가 한번은 이런 의문이 생긴 적이 있습니다. 컨트롤 조합이 특별한 의미가 있는 게 아니라 단지 아스키 테이블의 또다른 문자를 생성하는 것 뿐이라면, 컨트롤 조합 문자도 vim에서 텍스트 파일로 포함시킬 수 없을까? 하는 거였는데요.

말하자면 텍스트 파일에 <c-l> 같은 걸 그냥 넣는 건데, 방법을 찾기란 어렵지 않았습니다. vim 매뉴얼에 방법이 있었거든요. (vim에서 :help i_ctrl-v를 입력해 보세요) <c-v>를 입력하고 그 다음에 다른 문자를 입력하면 이런 특별한 키들을 "그대로" 입력할 수가 있습니다.

영상을 찍어 봤습니다. 이렇게 에디터 안에 해당 문자들을 입력할 수 있죠.

영상을 보면 컨트롤L, 컨트롤C 같은 문자들을 그대로 입력하고 있습니다. 그리고 이건 git 으로 commit 해서 github에 올리잖아요? 그러면 github에서도 그대로 나옵니다. 최근에도 올린 게 하나 있었던 거 같은데… 찾아와볼게요. 아 여기 있네요. ^M이 있습니다.

https://github.com/johngrib/dotfiles/blob/2ffc50b3bd272571320202d604d761511ab67e11/nvim/vim-include/set-clojure.vim#L154

이걸 그대로 복붙해서 아무 곳에나 복붙해 보세요. 트위터에도 되고 vscode에도 되고 메모장에도 되고 아무데나 해보세요. 그러면 `^M`이 개행문자로 해석되어 github에서 볼 때 한 줄인 텍스트가 복붙한 곳에선 여러 줄로 보일 겁니다.

( 아니 이거 될 줄 알았는데 해보니까 안 되네요! 잘못된 정보입니다. 죄송합니다! )

복붙해서 → 복사해서 그리고 이쯤 되면 아마 눈치채신 분들도 있겠지만 이건 그냥 터미널에서도 마찬가지입니다. 터미널에서 <c-v>를 누르고, 엔터를 쳐 보세요. 그러면 ^M이 프롬프트에 나타날 겁니다. 그리고 <c-v>를 누르고 <c-m>을 눌러도 같은 것이 나타날 겁니다.

와장창 쓰고 쭉 훑어보니 뭔가 남는 건 없네요… 하하. 그치만 저는 오늘밤 30분간도 재미있어서 좋았고 여러분도 재미있게 읽어주셨다면 기쁘겠습니다. 오늘 이야기는 여기까지 하죠.