Git hook으로 jekyll 블로그 카테고리 자동생성하기
Jekyll 블로그의 단점..
Jekyll 블로그는 카테고리별 페이지를 만들어주지 않는다. 처음 Jekyll로 블로그를 시작했을 땐 어떻게 해야 카테고리 페이지를 따로 만드는지, Tag들이 모여있는 페이지를 만들어놨는지 하나도 이해하지 못해서 카테고리 없이 지냈던 시절도 있었다..😂
알고보니 카테고리나 태그 페이지들은 그 레이아웃에 맞는 markdown (또는 html이었을수도?) 파일을 하나씩 만들어줘야하는 것이었다. (세상에 요즘 세상에 수동이라니) 다른 사람들은 어떻게 쓰고있는지 잘 모르겠지만 나는 꾸역꾸역 약 1년 반을 수동으로 써왔다. 그러다가 블로그에 점점 글이 많아지면서, 다양한 종류의 글들이 생겨나기 시작했는데 그 때마다 카테고리를 만들어줘야하는게 슬슬 번거로워지기 시작했다.
그렇게 아 이거 자동화 해봐야지.. 하고 생각했었는데, 마땅히 좋은 방법이 떠오르지 않아서 블로그 이슈에 묵혀져있던 상태로 또 몇달이 지나 지금에서야 생각이 났다. 이슈를 생성한지 엄청나게 오래된 줄 알았는데 생성 날짜를 보니 생각보다 많이 오래되진 않았었다..
위 사진처럼 어렴풋이 여러가지 방법들을 생각해봤는데, ruby를 활용하는 방법은 별로 마음에 들지 않았다. Jekyll이 지원하지 않는 라이브러리를 쓰게되면 GitHub의 자동 빌드를 이용할 수 없게 되기 때문이다. 물론 이제 GitHub Actions를 사용해서 자동으로 빌드하고 배포하는게 어려운 일은 아니지만 GitHub이 자동 빌드를 지원해줬기 때문에 사용했던건데, 그걸 버리게 되면 Jekyll이 아니라 차라리 다른 것을 쓰는게 낫겠다고 생각했다. 나름 1년이 넘게 삽질한 블로그라, 아직까진 이 형태를 버리고 싶지 않았다.
어쩌다 Git Hook을?
이렇게 어영부영 잠시 이슈를 잊어가던 중 최근에 마크다운 파일을 읽어서 정보를 가져오는 일을 한적이 있는데, 문득 블로그 카테고리도 비슷한 방식으로 자동 생성할 수 있을 것 같다는 생각이 들었다. 하지만 이걸 수동으로 실행시키면 커밋을 올리기 전에 명령어 치는 걸 까먹으면 그만..🙄이라 원점으로 돌아가겠단 생각이 들었고, 어차피 이 내용들은 다 git에 올라가게 될테니 git hooks을 사용해보기로 결정했다.
Git Hooks
어떤 이벤트가 생겼을 때 자동으로 특정 스크립트를 실행할 수 있도록 만들어진 방식이다. commit 전에 호출되는 pre-commit
, commit 후에 호출되는 post-commit
등 다양한 종류의 이벤트에 대해서 스크립트를 걸어둘 수 있다. 자세한 건 여기를 참고하면 좋을 것 같다.
git hook이 실행되는 걸 보기만하고, 적용해본 적은 한번도 없어서 어떤 방식으로 스크립트를 적용하는지 한번 찾아봤다. git이 적용된 폴더에 .git/hooks
를 가보면 다양한 샘플 파일들이 있는데, 여기서 .sample
확장자를 떼면 git에 이 스크립트들을 적용할 수 있다.
이 중에서 어떤 이벤트에 스크립트를 적용해볼까 고민하다가 내가 원하는 형태는 새로운 글을 올렸을 때 새로운 카테고리 파일도 함께 생성되는 형태여서 pre-commit
이벤트에 스크립트를 적용하기로 했다!
Git Hooks를 Javascript로 작성해보자?!
sample 파일들은 모두 shell 스크립트로 작성되어있지만 평범하길 거부한? 나는 이번에는 shell script 보다는 javascript를 사용하기로 했다. 단순히 shell 보단 javascript로 코드가 먼저 떠올랐기 때문이다..ㅎ
Git Hooks 스크립트 안에는 첫줄에 이 코드가 어떤 언어로 동작할지를 명시하는데, 샘플 파일에는 아래처럼 #!/bin/sh
이렇게 첫 줄에 shell 스크립트로 동작하도록 명시하고 있다.
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
# ...
자바스크립트로 동작하게 만들기 위해선 #!/usr/bin/env node
라고 첫줄에 적어주면 된다.
카테고리 파일을 만들어보자
카테고리를 가져오기 위해선 일단 포스트들을 전부 훑어보고, 어떤 카테고리들이 있는지 가져와야한다. 여기선 node.js의 파일 시스템과 npm front-matter 모듈을 이용했다. 깃 훅을 동작시키기 위해서 node_module을 설치하는게 조금 이상한 것 같기도 하지만.. 일단은 이걸 써보는데 의의를 뒀으니 이 부분은 빠르게 넘어갔다.
front-matter 모듈을 이용하면 markdown 파일에서 상단에 적은 front-matter 부분을 객체 형태로 받아올 수 있어서 좀 더 편리하게 각 포스트들의 정보를 가져올 수 있다.
구현은 그렇게 어려운 부분은 아니었기 때문에 자세한 이야기는 생략🤤
커밋에 이 파일을 포함시키기
이렇게 카테고리 파일을 생성하고, 이 파일들을 커밋에 포함시키기 위해서는, commit을 하기 전에 파일들을 이번 커밋에 추가해줘야한다. 때문에 git add
명령어를 실행시켜줄 필요가 있었는데 이 작업도 해본적이 없어서 조금 낯설었다.
찾아보니 nodejs에서는 child_process라는 모듈을 이용해서 하위 프로세스를 생성하고, shell command를 실행시킬 수 있었다.
이 모듈엔 nodejs가 single thread로 동작하기 때문에 속도를 향상하기 위해 사용되는.. 이라는 배경이 있는데, 좀 더 자세한 내용은 이 포스트를 참고하면 좋을 것 같다.
이 모듈을 통해서 명령어를 실행시킬 수 있는 방법은 exec
와 spawn
을 사용하는 것이었는데, 두 방식 모두 shell command를 실행시킨다는 것은 같지만 exec
는 버퍼/문자열을 리턴해 좀 더 간단한 데이터를 받는 경우, spawn
은 스트림을 리턴해 큰 데이터를 받는 경우에 사용하는 것을 권장하고 있었다. 나는 비교적 작은 양의 데이터를 가져온다고 생각했고, 사용성도 더 간편한 exec
를 사용하기로 했다.
child_process 모듈의 exec
는 원래 첫번째 인자로 string, callback을 받는데 공식문서를 읽어보면 util 모듈의 promisify를 사용하는 예제가 있다. 이 모듈은 콜백함수 부분을 Promise로 반환해줘서 좀 더 깔끔하게 이용이 가능했고, 이 방식대로 util과 child_prosess를 조합해 명령어를 실행시켜볼 수 있었다.
const childProcessExec = require('child_process').exec;
const util = require('util');
const exec = util.promisify(childProcessExec);
exec('git add ./categories');
오류처리하기
이 명령어가 실패할 일은 잘 없겠지만 그래도 만약 명령어가 실패하게 된다면 커밋은 중단돼야하고, 왜 중단되었는지 에러 내용이 나오면 좋겠다고 생각했다. (사실 중간에 실패했는데 프로세스가 안멈춰서 VSCode가 전부 꺼지기도 했었음😒)
프로세스를 exec
함수는 단순히 실행만 시키는 게 아니라 결과로 error
, stderr
, stdout
을 리턴하는데, 이를 통해서 어떤 부분에서 에러가 발생했는지 알 수 있고 명령어의 결과도 볼 수 있다.
error
객체는 명령어를 아예 실행시키지 못했을 때의 에러를 담고 있는 객체이고, stderr
는 명령어를 실행했지만 그 안에서 발생한 에러를 담고 있는 객체이다. (error
객체는 명령어를 실행시키지 못했을 때라 정확히 어떤 오류를 담고 있을지는 예상이 가지 않지만, stderr
같은 경우는 git add .
의 경우 스테이징에 올릴 파일이 없을 때라던지, LF CRLF 경고 등이 있을 것 같다.) stdout
은 예상 그대로, 명령어를 실행하는데 성공했을 때의 결과를 담고 있는 객체이다.
따라서 이 객체에 값이 담겼는지에 따라서 분기, 출력을 한 뒤에 프로세스를 종료시키면 오류처리는 끝이다!
//...
const {error, stderr, stdout} = await exec('git add .');
if(error){
//...
process.exit(1);
}
if(stderr){
//...
process.exit(1);
}
//...
process.exit(0);
하지만 모든 상황에서 process.exit(0)
으로 프로세스를 종료시키면 에러가 출력됐을 때도 정상적으로 종료되기 때문에, 0이 아닌 다른 값을 전달해서 비정상적으로 종료되었다는 것을 알려주면 오류처리는 정말 끝이다 😎
완성!
그래서 이제 commit을 하기전에, 아래와 같이 pre-commit이 돌아서 전체 포스트에 대해서 카테고리를 확인하고 새로 만들어준다.
개선하기
이 글을 작성하면서 생각해보니 아쉬운 점이 조금 남는다. 지금은 전체 파일을 훑어서 카테고리를 생성하도록 되어있는데, 이 스크립트는 매 commit마다 카테고리 전체를 검증하기 때문에 굳이 전체 파일을 검사할 필요가 없을수도 있겠다는 생각이 들었다.
commit에 올리는 글만 탐색해서 카테고리를 업데이트 하는 방식이라면 포스트가 많아지더라도 시간을 단축할 수 있고, 더 효율적일 것 같긴하지만 여러모로 고려해봐야할 것이 좀 있어서 아직은 이대로 두었다.
근데 또 생각해보니 pre-commit은 스크립트를 어디 등록해놓지 않으면 이 컴퓨터(?)외에는 쓸 수가 없다.. 다른 방법을 또 생각해봐야하나…
이것도 이슈에 등록해놔야지! 😂😂