SCSS 파일을 자동으로 컴파일하게 하기 – vim

scss 파일을 저장할 때 자동으로 컴파일 해주는 기능을 만들어보자. 이런 종류의 기능은 보통 vim의 autocmd나 키맵 등을 설정하여 만들 수 있다. 오토 커맨드로 등록하는 경우, 특정한 패턴의 파일을 열거나, 파일을 새로 만들거나, 저장하는 전후 등의 시점에 자동으로 수행될 수 있다.

SCSS 컴파일은 외부 컴파일러에 의존하고 있다. 따라서 vim 내에서 실행하면 해당 프로그램이 실행되는 동안 vim은 해당 프로세스가 종료되기를 기다리면서 동작을 멈추게 된다. 비록 짧은 시간이기는 하지만 이 과정이 제법 신경쓰일 수 있다.

그런데 vim8부터는 비동기로 이런 작업을 처리할 수 있는 기능이 제공된다. (따라서 vim7 이하버전에서는 실행되지 않는데, 이를 위한 처리를 통합했다.) 여기서 컴파일은 pysassc에 의존하며 이는 파이썬 pip를 이용해서 libsass를 설치하면 얻을 수 있다.


이제 vim 내에서 어떤식으로 비동기 명령을 실행하는지 알아보자.

  1. job_start() 함수를 사용하면 특정한 외부 명령을 비동기로 실행할 수 있다. 이 함수가 실행되면 자식 프로세스로 주어진 명령이 시작되고, 해당 명령과 연결되는 job 객체가 생긴다.
  2. vim 과 해당 프로세스는 채널을 통해 연결되고 job 은 시작시의 job_start() 의 옵션으로 몇 가지 이벤트에 대한 핸들러함수를 지정받을 수 있다.
  3. "close_cb" 옵션으로 지정한 함수는 해당 job의 채널을 인자로 받게된다. 따라서 채널을 통해 외부 프로세스의 표준 출력 혹은 표준 에러를 읽어올 수 있다.

먼저 SCSSCompile1 이라는 함수를 작성한다. 이 함수는 비동기 작업이 지원될 때 호출될 함수이다. 그런 다음 SCSSCompile0 함수는 비동기 지원이 되지 않는 상황에 호출될 함수로 작성한다.

SCSSCompile1 함수에서는 pysassc 를 호출하여 현재 파일을 css 파일로 컴파일하도록 한다. 그리고 채널이 닫힐 때 SCSSCompileHandler 함수가 호출되도록 한다. 이 콜백 전달은 job_start() 함수에 두 번째 인자로 사전 형식으로 지정한다. close_cb 가 채널이 닫힐 때 호출되는 콜백이다.

function! SCSSCompile1()
    echom 'Compiling: ' . expand('%')
    let l:commands = ['pysassc', expand('%'), expand('%<') . '.css']
    let job = job_start(l:commands, {'close_cb': 'SCSSCompileHandler'})
endfunction

function! SCSSCompile0()
    echo 'Compiling...'
    let cname = expand('%')
    let oname = expand('%<') . '.css'
    normal! ':!sassc ' . cname . ' ' . 'oname'
endfunction

핸들러 함수는 채널을 인자로 받으며, 채널의 상태를 확인하여 표준 출력 부분의 데이터가 남아있으면 이를 읽어서 메시지로 출력한다. 이 때 ch_status() 함수와 ch_read() 함수를 사용한다. 이는 각각 한 줄씩만 읽어들이므로 반복하여 사용해야 한다.

function! SCSSCompileHandler(ch)
    echo 'Compiliation Completed'
    while ch_status(a:ch, {'part': 'out'})
        let output = ch_read(a:ch, {'timeout': 1})
        echom output
    endwhile
endfunction

이제 두 함수중 하나를 읽어들일 명령인 BuildSCSS를 정의한다. 이 명령은 SCSSCompile{ } 로 중괄호를 포함하는 함수를 호출하는데, 중괄호 이름은 내부 표현식의 평가 결과에 의한다. 즉 has('job') 이 있으면 1, 없으면 0이기 때문에 그 때마다 적당한 명령이 실행될 것이다.

command! -nargs=0 BuildSCSS call SCSSCompile{has('job')}()

최종적으로 *.scss, *.sass 파일을 저장할 때마다 이 명령이 실행되도록 autocmd를 등록한다.

augroup PYSASS
    au!
    au BufWritePost *.sass,*.scss BuildSCSS
augroup END

이상으로 scss 파일을 편집하고 저장하면 자동으로 css 파일로 컴파일하는 과정을 구현해보았다. 아래는 전체 코드이다.

function! SCSSCompile1()
    let l:commands = ['pysassc', expand('%'), expand('%<') . '.css']
    let job = job_start(l:commands, {'close_cb': 'SCSSCompileHandler'})
endfunction

function! SCSSCompile0()
    echo 'Compiling...'
    let cname = expand('%')
    let oname = expand('%<') . '.css'
    normal! ':!sassc ' . cname . ' ' . 'oname'
endfunction

function! SCSSCompileHandler(ch)
    echo 'Compiliation Completed'
    while ch_status(a:ch, {'part': 'out'})
        let output = ch_read(a:ch, {'timeout': 1})
        echom output
    endwhile
endfunction

function! SCSSCompile1()
    echom 'Compiling: ' . expand('%')
    let l:commands = ['pysassc', expand('%'), expand('%<') . '.css']
    let job = job_start(l:commands, {'callback': 'SCSSCompileHandler'})
endfunction

function! SCSSCompile0()
    echo 'Compiling...'
    let cname = expand('%')
    let oname = expand('%<') . '.css'
    normal! ':!sassc ' . cname . ' ' . 'oname'
endfunction

command! -nargs=0 BuildSCSS call SCSSCompile{has('job')}()
augroup PYSASS
    au!
    au BufWritePost *.sass,*.scss BuildSCSS
augroup END

vim의 autocmd 이벤트들

vim은 특정 이벤트가 발생할 때 자동으로 명령을 수행할 수 있는데, 이를 au[tocmd]로 지정할 수 있다. 자동 명령 세팅 방법은 다음에 다루기로 하고, 오늘은 이들 이벤트에 대해서 좀 정리

  • 버퍼 읽기/시작하기
    • BufNewFile 새 파일을 만들어서 편집하기 시작함
    • BufReadPre 파일을 읽기 전, 새 버퍼를 편집하기 시작함
    • BufRead 파일을 읽고 새 버퍼를 편집함
    • BufReadPost 위와 같이 파일을 읽고 새 버퍼를 편집함 (차이가?)
    • BufReadCmd 새 버퍼를 시작하는 명령을 받았을 때
  • 파일 읽기
    • FileReadPre :read 명령으로 새 파일을 읽기 직전
    • FileReadPost :read 명령으로 새 파일을 읽은 직후
    • FileReadCmd :read 명령을 입력받는 때
  • 버퍼 전체 쓰기
    • BufWrite 전체 버퍼를 파일에 쓰기 시작할 때
    • BufWritePre 전체 버퍼를 파일에 쓰기 시작할 때
    • BufWritePost 전체 버퍼를 파일에 쓴 후
    • BufWriteCmd 쓰기 명령을 받았을 때, 아직 파일에는 쓰기 전
  •  버퍼 일부 쓰기
    • FileWritePre 버퍼의 일부를 파일에 쓰기 시작함
    • FileWritePre 버퍼의 일부를 파일에 쓰기 시작함
    • FileWriteCmd 버퍼의 일부를 파일에 쓰는 명령을 받을 때
    • FileAppendPre 파일의 뒤에 내용을 덧붙일 때
    • FileAppendPost 파일의 뒤에 내용을 추가한 후
    • FileAppendCmd 파일에 추가 명령을 받을 때
  •  버퍼 관련
    • BufAdd 새 버퍼가 버퍼 목록에 추가된 직후
    • BufCreate 새 버퍼가 생성되어 버퍼 목록에 추가된 직후
    • BufDelete 버퍼 목록에서 버퍼가 삭제되기 전
    • BufWipeout 버퍼를 완전히 삭제하기 직전
    • BufFilePre 현재 버퍼의 이름을 바꾸기 직전
    • BufFilePost 현재 버퍼의 이름이 바뀐 직후
    • BufEnter 버퍼에 진입한 직후
    • BufLeave 버퍼를 떠나 다른 버퍼로 옮기기 직전
    • BufWinEnter 화면 창에 버퍼가 표시된 후
    • BufWinLeave 창에서 버퍼가 제거되기 직전
    • BufUnload 버퍼가 언로드되기 전
    • BufHidden 버퍼가 숨겨진 직후
    • BufNew 새 버퍼를 만든 직후
  • 스왑
    • SwapExists 스왑파일이 있을 때
  • 옵션
    • FileType “filetype” 옵션이 세팅될 때
    • Syntax “syntax”옵션이 세팅될 때
    • EncodingChanged “encoding” 옵션이 세팅될 때
    • TermChanged “term”의 값이 바뀔 때
  • 시작 및 종료
    • VimEnter vim 시작 후
    • GUIEnter GUI가 잘 시작된 후
    • TermResponse 터미널 응답 t_RV을 받은 후
    • VimLeavePre vim 종료 직전, viminfo 파일에 쓰기 전
    • VimLeave 종료 직전, viminfo 파일을 쓴 후
  • 그 외
    • FileChangedShell – Vim에서 파일을 편집한 이후, 이 파일이 변경됐음을 vim이 감지했을 때
    • FileChangedShellPost – 편집이 시작된 후 파일이 변경되었을 때
    • FileChangedRO – 읽기 전용 파일에 변경을 시작하려 할 때
    • ShellCmdPost – 쉘 명령을 실행한 후
    • ShellFilterPost – 쉘 명령으로 필터링한 후
    • FuncUndefined – 정의되지 않은 사용자 함수를 호출할 때
    • SpellFileMissing – 존재하지 않는 문법 파일을 사용하려할 때
    • SourcePre – vim 스크립트를 반입하기 직전
    • SourceCmd – 명령모드에서 vim 스크립트를 반입할 때
    • VimResized – vim 창의 크기가 변경되었을 때
    • FocusGained – vim이 입력 포커스를 받을 때
    • FocusLost – 입력 포커스를 잃을 때
    • CursorHold – 특정 시간 동안 아무 것도 눌러지지 않을 때
    • CursorHoldI – 입력 모드에서 특정 시간 동안 아무 키도 눌러지지 않을 때
    • CursorMoved – 일반 모드에서 커서가 움직일 때
    • CursorMovedI – 입력모드에서 커서가 움직일 때
    • WinEnter – 다른 창으로 들어갈 때
    • WinLeave – 창을 떠날 때
    • TabEnter – vim 창 내의 특정 탭으로 들어갈 때
    • TabLeave – 특정 탭을 떠날 때
    • ColorScheme –  색상 스킴이 변경될 때
    • RemoteReply – 서버의 응답을 vim이 받았을 때
    • QuickFixCmdPre – quickfix 명령이 실행되기 전
    • QuickFixCmdPost – quickfix 명령이 실행된 후
    • SessionLoadPost – 세션 파일의 로딩이 끝난 후
    • MenuPopup – 메뉴 팝업을 보여준 후
    • User“:doautocmd”와 함께 쓰임