Tistory CLI

최근에 직접 만든 Tistory CLI 프로그램을 소개하고자 한다. GNU/Linux 사용자를 위한 이 프로그램은 내가 Tistory 웹에디터를 쓰기 싫어서 시작하게 된 프로젝트이다.

개발 관련 글을 쓰다보면 코드 뭉치를 써야 할 경우가 많이 있는데, 그 경우에 웹에디터의 WYSIWYG (What You See Is What You Get) 모드와 HTML 모드를 넘나들면서 문서 작업을 하는 것이 나에겐 고단한 일이다. 또한 웹에디터에서 제공하는 대부분의 기능을 사용하지 않을 뿐 더러, 사용하더라도 꽤나 지저분한 HTML 코드가 생성되는 것을 봤기에 더욱 꺼려지게 되었다. 이는 내가 설정한 CSS를 웹에디터가 알 길이 없으니 어쩔 수 없는 것이긴 하다만, 그렇다고 하더라도 친절한 내가 웹에디터의 처지를 이해해주고 HTML 모드에서 직접 쓰고 싶지는 않다. 결론적으로 Markdown 문서가 바로 Tistory로 넘어가면 좋겠다는 생각이 들어서 시작하게 되었다.

구현은 Python과 Rust로 각각 만들어봤다. Python은 내가 가장 편하게 다룰 수 있는 언어이므로, CLI 프로그램이 해야하는 행동들을 빠르게 구현하는 것에 목표를 두었다. 그리고 요즘 공부 중인 Rust를 사용해 볼 만한 기회라고 생각이 들어서, Python으로 구현된 기능을 최대한 만족시켜보려고 시도했다. 하지만 결과적으로 Python 프로젝트만이 실제로 사용할 수 있는 프로그램임을 아쉬움을 담아 알린다.

앞으로 설명하는 바는 Python 프로젝트에 치중해서 이야기하고자 한다. Rust 프로그램이 궁금하다면 Github 레포를 찾아가 보시길 바란다.


대상

  • 명령줄 환경이 친숙한 GNU/Linux 사용자
  • Tistory 블로거
  • Tistory 에디터를 싫어하는 블로거
  • Markdown을 편하게 사용하는 블로거

특징

1. 비밀 정보를 안전하게 저장한다

이 프로그램은 당연하게도 Tistory 오픈 API를 사용한다. 따라서 Tistory 가이드에 따라 Oauth 서버를 통해서 API access token을 발급받아서 사용해야 하는데, 이러한 비밀 정보들 (client ID, client secret, access token)을 어떻게 저장하고 관리하느냐가 실제로 사용하는 데 있어서 중요한 문제라고 생각했다. 그래서 찾게 된 것이 libsecret으로, Linux 운영 체제 레벨에서 비밀 정보 (예를 들어 계정 비밀번호)를 저장하기 위한 시스템 라이브러리였고, 다행히 Python 쪽에서 이를 사용할 수 있게 연결해주는 SecretStorage 모듈을 찾아서 사용할 수 있었다.

그래서 libsecret은 얼마나 안전한가? 에 대해서는 나도 잘 모른다 (유감). 추후에 공부해서 정리하는 시간이 왔으면 좋겠다. 우선은 GNOME 프로젝트의 권위에 맡기고자 한다.

2. 정말 최소한의 작업만을 한다

인증 및 토큰 저장, 카테고리 ID 받아오기, 그리고 포스팅. 이것이 CLI 프로그램이 하는 일이다. 그림 넣기, 태그 달기 등의 기능을 지원하지 않는다. 따라서 결국엔 티스토리 웹에디터에 들어가서 수정을 해야하기 때문에 포스팅은 무조건 비공개 글로 올리게하도록 결정했다. 이러한 결정은 KISS (Keep It Simple, Stupid) 정신에 따른 것이며, 귀찮아서 그런 것이아님을 강조한다.

3. Git Commit Message와 비슷한 문서 규칙

Tistory CLI가 요구하는 문서의 양식은 아래와 같다. 제목과 내용만 분리하면 된다.

첫 줄은 Tistory에 제목으로 올라간다

# 실제 Markdown 문서 시작

첫 줄인 Tistory 제목 다음 한 줄은 무조건 띄어서 제목과 내용을 구분한다.

* 신나게
* 내용을
* 쓴다

Markdown 문서 끝

4. 최소한의 외부 라이브러리 사용

Python 외부 의존 모듈은 두 개이다.

Oauth 인증 및 Tistory 오픈 API 사용을 위해 필요한 HTTP server와 client를 구현하기 위해서 Flask라던지 Requests라던지 우리의 일을 쉽게 만들어주는 것들이 있을 것인데, 그들의 힘을 빌리지 않고 Python 기본 HTTP 기능 안에서 해결했다. 그 이유로는?

  1. 실제 프로젝트가 가벼워진다.
  2. 나의 공부 목적이 있기도 하다.
  3. Oauth 인증을 위한 기초적인 HTTP server와 간단한 GET, POST 요청을 위한 client를 구현하려고 외부 라이브러리나 프레임워크를 사용하는 것은 배보다 배꼽이 더 큰 일이라고 생각했기 때문이다.

설치

PYPI에 등록해 두어서 쉽게 설치가 가능하다.

$ pip install tistory-cli

굳이 소스에서 직접 설치하고 싶다면

$ git clone https://github.com/dgkim5360/tistory-cli-python.git
$ cd tistory-cli-python
$ python setup.py install

사용법

자세한 설명은 Github 레포에서 확인하시길 바란다.

궁금증

이 프로그램을 만들면서 어려움을 느낀 점이 몇 가지 있어서 여기에 공유하고, 조언을 구하고 싶다.

1. 외부 모듈에 의존하는 기능에 대해서는 무엇을/어떻게 테스트해야 하는가

TDD를 하기 위해 고민을 했었는데, integration test도, unit test도 쉽지가 않아서 TDD가 잘 이루어지지 않았다. 결국 TDD를 하지 못하고, 우선적으로 구현한 것에 맞춰서 unit test를 작성하게 되었는데, 이렇게 흘러간 이유들이 있다. 이 프로그램이 핵심적으로 사용하는 것이 SecretStorage와 Oauth 인증을 통한 Tistory 오픈 API인데, 이 두 가지 모두 테스트하기 애매한 것들이었기 때문이다.

  • 비밀 정보를 저장하고, 가져오고, 삭제하는 기능은 순전히 SecretStorage의 API를 엮어서 쓰는 것인데, 이 부분의 테스트는 결국 mocking만이 존재하는 테스트가 되었다. SecretStorage의 어느 기능을 어느 argument를 넣어서 호출했는지 확인하고, 그 결과로 무엇을 줄 것이고, 이를 사용해서 내 코드가 무슨 일을 하는 지를 체크하는데, don't fight with the framework 라는 말을 생각해보면 이런 테스트 자체가 필요한 것인지 의문이 들었다.
  • Tistory 오픈 API를 사용해서 카테고리 정보와 포스팅을 하는 기능에 대해서는 Oauth 인증을 위한 비밀 정보를 어떻게 전달할 수 있을 지 몰라서 어려움이 겪었다. Access token을 받으려면 Oauth 인증을 해야하는데, 이를 위해서 내 계정의 client id와 client secret을 프로젝트 데이터 내에 넣을 수도 없고, .gitignore에 등록해서 Git 레포가 인식하지 않는 테스트용 파일을 만든다고 한들 다른 사람들이 테스트를 돌릴 수가 없는데 무슨 소용이 있나 싶은 것이다.

다행히 Python에서 mocking을 위한 기능을 지원해주어서 어찌저찌 unit test를 작성해놓긴 했는데, 이게 내가 제대로 한 것인지 확신이 없다. 제대로 한 것이 맞다면 조금 더 상세하게 유닛 테스트를 할 수 있는 방법이 있을 지, 제대로 한 것이 아니라면, 더 나은 방법이 무엇인지 궁금하다.

2. Rust의 경우 libsecret을 연동하는 crate이 없는데, 돌파할 방법이 있을까

운 좋게도 Python의 경우 SecretStorage 모듈을 사용해서 libsecret을 연결할 수 있었다. 그래서 안전하게 사용 가능한 프로그램이 될 수 있었는데, Rust의 경우 아래 두 가지 프로젝트를 찾았으나 현재 사용이 불가능한 상태였다.

울며 겨자먹기로 보안은 포기하고 (우선 내 공부가 우선이어서) 기존 Python 프로젝트의 기능을 충실히 구현하는 것으로 진행했는데, 덕분에 이 프로젝트는 순전히 공부용으로 마치게 되었다. libsecret을 사용할 수 없을 때, 내가 생각해 볼 수 있는 대안을 나열해 보면,

  • JSON과 같은 (whatever) 파일로 저장한다: 절대 안 될 일
  • 환경 변수를 읽어서 사용하게 만든다: 그러면 사용자가 매 번 환경 변수를 넣어줘야 하는데, 이는 현실적으로 이 프로그램을 사용하지 않게 만들 것 같다. 이를 개선하기 위해 쉡스크립트 파일에 환경 변수를 export하라고 권한다면, 위에 JSON 파일로 만드는 것과 다른 것이 없지 않을까?

libsecret을 사용할 수 없을 때 좀 더 안전하게 비밀 정보를 관리할 수 있는 방법이 있을지 알고 싶다.