hashcards는 데이터베이스 대신 마크다운 파일 디렉터리를 카드 컬렉션으로 사용하는 로컬 우선 간격 반복 앱이다. FSRS 스케줄러를 사용하며, 깃과 유닉스 도구를 통한 편집·자동화·공유가 쉽도록 설계되었다.
URL: https://borretti.me/article/hashcards-plain-text-spaced-repetition
hashcards는 Anki나 Mochi와 비슷한 계열의 로컬 우선(local-first) 간격 반복(spaced repetition) 앱이다. Anki처럼, 현재까지 가장 진보된 스케줄링 알고리즘인 FSRS를 사용해 복습 일정을 잡는다.
hashcards를 독특하게 만드는 점은 데이터베이스를 쓰지 않는다는 것이다. 대신 플래시카드 컬렉션은 다음처럼 마크다운 파일로 이루어진 디렉터리일 뿐이다:
Cards/
Math.md
Chemistry.md
Astronomy.md
...
그리고 각 파일(“덱”)은 이렇게 생겼다:
Q: 시냅스 소포(synaptic vesicles)의 역할은?
A: 시냅스 말단에서 방출되도록 신경전달물질을 저장한다.
Q: 뉴라이트(neurite)란?
A: 뉴런에서 뻗어 나온 돌출부로, 축삭(axon) 또는 수상돌기(dendrite)이다.
C: 말은 [생산된다] [브로카] 영역에서.
C: 말은 [이해된다] [베르니케] 영역에서.
일반적인 노트를 쓰듯이 플래시카드를 작성하되, 기본(질문/답변) 카드와 빈칸 채우기(cloze deletion) 카드를 표시하기 위한 가벼운 마크업을 쓴다. 그리고 공부할 때는 다음을 실행한다:
$ hashcards drill <카드 디렉터리 경로>
그러면 localhost:8000에서 웹 인터페이스가 열리고, 그곳에서 플래시카드를 복습할 수 있다. 성과와 복습 이력은 카드와 같은 디렉터리에 있는 SQLite 데이터베이스에 저장된다. 카드는 콘텐츠 주소 지정(content-addressed), 즉 텍스트의 해시로 식별된다.
이 핵심 설계 결정은 많은 이점을 낳는다. 원하는 편집기로 플래시카드를 편집할 수 있고, 플래시카드 컬렉션을 Git 저장소에 넣어 변경 사항을 추적할 수 있으며, 다른 사람들과 GitHub에서 공유할 수도 있다(내가 한 것처럼). 스크립트를 사용해 구조화된 데이터(예: 영어/프랑스어 어휘 쌍 CSV)로부터 플래시카드를 생성할 수도 있다. 어떤 앱의 데이터베이스 내부를 파헤치지 않고도 표준 유닉스 도구로, 혹은 프로그래밍으로 컬렉션을 질의하고 조작할 수 있다.
왜 새로운 간격 반복 앱을 만들었을까? 대체로 Anki와 Mochi 둘 다에 만족하지 못했기 때문이다. 게다가 내 플래시카드 컬렉션은 나에게 매우 중요해서, 그것이 원격 데이터베이스 어딘가에 있거나 내 컴퓨터 안의 불투명하고 쓸모없는 데이터 덩어리로 존재하는 것은 기분이 좋지 않다. “Git 저장소 안의 마크다운 파일”은 다른 접근들이 제공하지 못하는 수준의 소유감을 준다.
이 글의 나머지 부분은 Anki와 Mochi에 대해 내가 느낀 불만, 그리고 hashcards의 설계 결정을 어떻게 내리게 되었는지를 설명한다.
Anki는 내가 처음 사용한 SR 시스템이다. 오픈 소스라서 영원히 존재할 것이고, 플러그인이 엄청나게 많고, FSRS를 스케줄링에 최초로 도입한 SR 시스템이기도 하다. 매우 풍부한 통계를 제공하는데, 대부분은 쓸모없다고 생각하지만 구경하는 재미는 있다. 그리고 노트 타입(note types) 기능은 정말 좋다. 구조화된 데이터로부터 많은 플래시카드를 자동으로 생성할 수 있게 해준다.
Anki의 핵심 문제는 인터페이스가 정말 별로라는 점이다. 이는 여러 방식으로 나타난다.
첫째, 못생겼다. 특히 복습 화면이 그렇다. 이는 이미 종종 지루하고 좌절스러운 과정인 복습의 즐거움을 더 떨어뜨린다.
둘째, 단순한 일을 하기가 어렵다. Mochi의 좋은 점은 앱을 켜면 바로 복습 모드로 들어간다는 것이다. 알아차리기도 전에 카드를 돌리기 시작한다. Anki에는 “오늘 복습할 카드 전체를 공부하기”가 없다. 대신 덱에 들어가서 “Study Now” 버튼을 직접 눌러야 한다. 그래서 내가 하던 방식은 모든 덱을 “Root” 덱 아래에 넣고 그걸 공부하는 것이었다. 하지만 이건 해킹에 가깝다.
셋째, 카드 입력이 WYSIWYG 편집을 사용한다. 그래서 키보드에서 마우스로 왔다 갔다 해야 하고(지연이 늘고, 카드 만들기가 더 짜증나진다), 아니면 “이 텍스트를 cloze deletion으로 만들기”나 “이걸 TeX 수식으로 만들기” 같은 기본 작업을 위해 수많은 단축키를 외워야 한다.
마지막으로, 플러그인은 양날의 검이다. 사용할 _수 있는 선택지_가 있다는 점은 좋지만, 대부분의 플러그인을 실제로 사용하는 경험은 별로다. 전체 설정이 카드로 쌓아올린 집처럼 덜컥거린다. 대부분의 경우, 기능이 앱에 내장돼 있지 않다면 플러그인을 쓰느니 차라리 그 기능 없이 사는 편을 택하겠다.
Mochi는 Anki에 대한 주된 불만, 즉 인터페이스를 해결하려고 만들어진 것처럼 느껴진다. 직관적이고, 보기 좋고, 단축키가 풍부하다. 덜컥거림이 없다. WYSIWYG 대신 카드 텍스트가 마크다운인 것도 아주 즐겁다.
몇 가지 문제가 있다. 마크다운은 플래시카드를 작성하기에 마찰이 매우 적지만, Mochi에서 cloze deletion은 매우 장황하다. hashcards에서는 이렇게 쓸 수 있다:
Speech is [produced] in [Broca's] area.
Mochi에서의 동등한 표현은 이렇다:
Speech is {{1::produced}} in {{2::Broca's}} area.
타이핑이 많이 늘어난다. 몇 글자 차이라고 반박할 수도 있다. 하지만 교과서로 공부할 때나 어휘 표에서 단어를 옮겨 적을 때는 이런 작은 마찰이 누적된다. 플래시카드를 쓰는 게 짜증나면 덜 쓰게 되고, 그건 곧 얻는 지식이 줄어든다는 뜻이다. 반대로 카드 생성을 가능한 한 무마찰로 만드는 시스템은 더 많은 카드와 더 많은 지식을 의미한다.
또 다른 문제는 Mochi에 Anki의 노트 타입(note types)에 해당하는 기능이 없다는 것이다. 예를 들어 화학 원소에 대한 노트 타입을 만들고 원자 번호, 기호, 이름 같은 필드를 둔 다음, 다음과 같은 질문을 하는 플래시카드를 템플릿으로 생성할 수 있다:
그리고 다른 속성들도 마찬가지다. 이건 좋다. 자동화는 좋다. 일은 적게, 카드는 더 많이. Mochi에는 이 기능이 없다. 템플릿이 있긴 하지만, 그만큼 강력하진 않다.
하지만 내가 보기엔 Mochi의 가장 큰 문제는 알고리즘이다. 아주 최근에 FSRS 베타 지원을 추가하기 전까지, Mochi가 쓰던 알고리즘은 SM-2보다도 더 단순했다. 배수(multiplier)에 기반한 방식인데, 카드를 기억하면 간격에 1보다 큰 수를 곱하고, 잊으면 0과 1 사이의 수를 곱한다.
표면상의 근거는 단순함이다. 사용자가 알고리즘을 더 쉽게 추론할 수 있다는 것. 하지만 나는 이게 무의미하다고 생각한다. SR 앱의 핵심은 소프트웨어가 일정을 관리하고 사용자는 스케줄러가 어떻게 동작하는지 전혀 몰라도 되게 하는 것이다. 최적은 가능한 가장 진보된 스케줄링 알고리즘(즉 최소한의 복습 시간으로 최대한의 회상을 이끌어내는 알고리즘)을, 가능한 가장 직관적인 인터페이스 아래에 두는 것이고, 사용자는 그 혜택만 누리면 된다.
무작위 대조시험(RCT)이 없이는 Mochi/SM-2/FSRS를 비교할 수는 없겠지만, 내 주관적 경험으로는 Mochi의 알고리즘이 단기적으로는 잘 작동하나 장기적으로는 무너진다. 성숙한 카드를 잊었을 때 특히 나쁘다. 카드의 간격이 60일인데 ‘forget’을 누르면 간격을 1일로 리셋하지 않는다(그렇게 하면 잃어버린 지식을 재공고화하는 데 도움이 되는데도). 대신 간격이 forget 배수(기본값 0.5)를 곱해 _30일_이 된다. 무슨 소용인가? 60일 만에 잊은 거라면 30일 뒤에 더 잘 기억할 리가 없다.
forget 배수를 0으로 설정하면 이 문제를 고칠 수 있다. 하지만 이렇게 동작한다는 것을 알아야 하고, 결정적으로 나는 설정하고 싶지 않다! “스케줄러 파라미터 미세조정”을 또 하나의 기술로 익히고 싶지 않다. 스케줄러는 그냥 제대로 동작하길 바란다.
전반적으로 나는 간격 반복 알고리즘들이 너무 낙관적이라고 생각한다. “망각 지옥”에 빠지는 것보다는 카드를 조금 더 자주 보고, 복습에 시간을 더 쓰는 편이 낫다. 하지만 개발자들은 시스템을 너무 부담스럽게 만들면 유지율이 떨어질까 걱정해야 한다.
Anki는 인터페이스가 짜증나지만 알고리즘은 놀랍도록 잘 동작한다. Mochi는 인터페이스가 즐겁지만 알고리즘이 짜증난다. 몇 달이고 플래시카드를 돌리며 컬렉션을 키우다가, 카드들이 어떤 보이지 않는 나이 임계치를 넘으면 잊기 시작하고, 알고리즘은 내가 잊은 것을 다시 배우도록 도와주지 않는다. 결국 나는 ‘어차피 언젠가 다 잊을 텐데’라는 기대 때문에 번아웃이 와서 복습을 그만뒀다. 이제 FSRS 지원을 추가했지만, 지금은 1700장이 연체(overdue) 상태다.
추가로: Mochi에는 버튼이 “Forgot”과 “Remembered” 두 개뿐이다. 사용자는 단순하겠지만, 대부분의 SR 스케줄링 알고리즘이 더 많은 선택지를 제공하는 데는 이유가 있다. 회상의 정도가 다르면 카드 파라미터를 서로 다른 크기로 조정해야 하기 때문이다.
내가 간격 반복 시스템에 바라는 건 무엇일까?
첫째는 카드 생성이 무마찰이어야 한다는 것이다. 내 경험상 간격 반복의 가장 큰 병목은 복습 자체가 아니다(나는 이에 대해 매우 규율적이고, 몇 달씩 매일 SR 복습을 해왔다). 개념적 지식을 플래시카드로 바꾸는 것도 아니다. 가장 큰 병목은 그저 시스템에 카드를 입력하는 일이다.
어떤 개념이나 주제에 대한 지식을 가장 확실히 보강하는 방법은 그에 대한 플래시카드를 더 많이 쓰는 것이다. 같은 질문을 다른 방식으로, 다른 방향으로, 다른 각도에서 묻는다. 양이 늘면 같은 정보를 더 자주 보게 되고, 다양한 방식으로 묻는 것은 “카드의 모양을 외우는 것”을 막아주며, 일종의 중복성으로 작동한다. 그 지식 조각을 마음속 다른 부분과 연결하는 여러 개의 간선이 생기는 셈이다.
그리고 나는 종종 이렇게 생각하곤 했다. ‘이건 플래시카드를 하나 더 쓰면 더 단단해지겠다.’ 하지만 추가 카드 하나가 너무 번거로워서 하지 않기로 결정했다.
카드를 시스템에 넣는 과정에 마찰이 크면 카드를 덜 쓰게 된다. 그리고 기회비용이 있다. 쓰지 않은 카드는 배우지 않은 개념이다. 시간을 따라 적분하면, 잃어버리는 것은 지식의 ‘바다’ 전체다.
그래서 시스템은 카드 입력을 수고 없이 만들도록 설계되어야 한다. 이것이 hashcards 텍스트 포맷 설계를 이끈 원칙이었다. 예를 들어 cloze deletion은 대괄호를 쓰는데, 미국 키보드에서는 대괄호가 시프트 없이 입력 가능하기 때문이다(모치의 중괄호와 비교해보라). 그리고 괄호가 두 개가 아니라 하나다. 원래 포맷은 카드당 한 줄, 빈 줄로 카드 구분, Q/A 카드는 슬래시로 양면을 나누는 방식이었다:
What is the atomic number of carbon? / 6
The atomic number of [carbon] is [6].
이게 더 마찰이 적은 것은 맞다. 하지만 여러 줄짜리 플래시카드에 문제가 생긴다. 여러 줄 카드는 꽤 흔해서 2등 시민 취급을 하면 안 된다. 결국 나는 현재의 포맷으로 정착했다:
Q: What is the atomic number of carbon?
A: 6
C: The atomic number of [carbon] is [6].
타이핑이 약간만 늘고, 카드의 시작과 끝, 카드 종류를 시각적으로 쉽게 식별할 수 있다는 장점이 있다. 나는 최적의 포맷이 무엇인지 Claude와 오랫동안 설전을 벌였다.
또 다른 마찰의 원천은 카드 생성이 아니라 _편집_이다. 핵심 문제는 지식이 시간이 지나며 바뀌고 개선된다는 점이다. 교과서들은 종종 이런 방식을 취한다. 1장에서 어떤 존재론(ontology)을 소개해놓고 3장에 가서는 “사실 그건 거짓말이었고, 이 과목의 진짜 존재론은 이거다”라고 말한다. 그러면 과거의 플래시카드를 다시 돌아가서 고쳐야 한다. 그렇지 않으면 어떤 카드는 학부 수준 정의를 묻고, 다른 카드는 대학원 수준 정의를 물어서 모호함이 생긴다.
그래서 교과서로 공부할 때는 교과서별 덱을 만들고, 각 장마다 하위 덱을 둔다. 그러면 플래시카드를 원본 자료에 맞춰 정렬하기 쉽고(정합성을 보장), 각 장 덱은 보통 수십 장 수준이라 탐색하기도 쉽다.
가끔은 같은 개념에 대해 여러 카드를 써서 한꺼번에 모두 업데이트해야 한다. 덱이 크면 관련 카드를 찾기 어렵다. hashcards에서는 덱이 그저 마크다운 파일이다. 어떤 카드의 위아래에 있는 카드들은 대체로 의미적으로 연관되어 있다. 위아래로 스크롤하면서 그 자리에서 수정하면 된다.
그런데 왜 Git 저장소의 플레인 텍스트 파일이어야 할까? 왜 위 포맷을 쓰되, 데이터베이스를 가진 “일반적인” 앱으로 만들지 않을까?
플래시카드를 Git 저장소의 플레인 텍스트 파일로 저장하는 간격 반복 시스템이라는 막연한 아이디어는 오랫동안 내 머릿속을 맴돌았다. 2011년쯤 IRC에서 Anki 고수에게 그런 게 존재하냐고 물었던 기억도 난다. 어느 시점에는 Andy Matuschak의 노트를 읽었다. 그의 시스템에서는 플래시카드가 산문(prose) 노트와 함께 놓인다. 표기법도 내 것과 비슷해서 질문/답변 카드는 Q와 A 태그를, cloze deletion은 {중괄호}를 쓴다. 그리고 카드는 콘텐츠 주소 지정, 즉 해시로 식별된다. 이건 명백히 좋은 아이디어다. 하지만 그의 코드는 비공개이고, 게다가 나는 산문 노트와 플래시카드는 아주 다른 짐승이라고 느낀다. 둘을 섞을 필요도, 원하지도 않는다.
하지만 플레인 텍스트 간격 반복이라는 아이디어의 우선순위가 올라간 건, 내가 즉흥적으로 현재 hashcards 워크플로와 비슷한 흐름을 쓰기 시작했기 때문이라고 생각한다.
교과서나 웹사이트로 공부할 때, 나는 마크다운 파일에 플래시카드를 적곤 했다. 보통 cloze deletion을 [foo] 같은 약식으로 썼다. 그다음 파이썬 스크립트로 그 약식을 Mochi가 사용하는 {{1::foo}} 표기법으로 변환했다. 그리고 지식이 쌓이고 무엇이 관련 있고 중요하게 기억할 만한지에 대한 감각이 좋아질수록, 파일 안의 플래시카드를 계속 편집했다. 그리고 장이나 문서 등을 끝낸 뒤에야 그제서야 플래시카드를 Mochi로 수동 임포트했다.
그런데 마지막 단계가 사실 불필요하다는 생각이 들었다. 나는 이미 플래시카드를 가볍게 주석을 단 마크다운 형태로 플레인 텍스트 파일에 쓰고 있었다. 호기심에 FSRS를 이미 구현해보기도 했다. 실업 상태(funemployment) 동안 만들 개인 프로젝트를 찾고 있었다. 그래서 hashcards는 그때쯤 매우 깔끔하게 생긴 ‘구멍’이었고, 나는 그 안을 칠하기만 하면 됐다.
플레인 텍스트 저장을 쓰면 많은 시너지가 생긴다는 것도 알게 됐다:
wc로 전체 단어 수를 구하거나, awk로 카드 집합을 일괄 수정할 수 있다.결과적으로, 플래시카드를 만들고 편집하는 일이 거의 무마찰에 가깝고, 진보된 간격 반복 스케줄러를 사용하며, 플래시카드를 돌리기 위한 우아한 UI를 제공하는 시스템이 되었다. 다른 사람들도 유용하게 쓰길 바란다.