Typst가 1.0에 이르기 전에 깨지는 변경을 어떻게 정의하고, 생태계의 고통을 줄이기 위해 패키지별 target 컴파일러 버전과 자동 마이그레이션 도구를 도입할 수 있는지에 대한 제안.
Evolving TypstJanuary 5, 2026 사람들은 우리가 언제 Typst의 1.0 버전을 릴리스하느냐고 자주 묻습니다. 버전 번호 “0”은 종종 미성숙함의 지표로, “이 소프트웨어는 아직 사용할 준비가 안 됐다”라고 크게 적힌 표지처럼 받아들여집니다. 개인적으로 저는 그것이 꽤 안타깝다고 생각합니다. 저는 Typst가 프로덕션 사용에 준비되었다고 _생각_하지만, 지금 당장 1.x 버전을 내는 것은 순진한 일이라고도 생각합니다.
제게 버전 번호는 마케팅 도구가 아니라 기술적 차원의 약속입니다. Typst는 _시맨틱 버저닝_을 사용하고, SemVer는 버전 번호의 각 부분에 특정 보장을 연결합니다. 따라서 릴리스를 공개하기 전에, 그 보장이 무엇인지 그리고 우리가 그것을 제공할 수 있는지 확실히 해야 합니다.
버전 번호 1.x의 프로젝트에 대해 SemVer는, 프로젝트가 호환되지 않는 변경을 도입할 때 2.x를 릴리스할 것을 요구합니다. 하지만 무엇이 호환되지 않는 변경에 해당하는지는 프로젝트/프로젝트가 속한 생태계에 달려 있습니다.
Typst의 경우, 좋은 정의를 내리기가 사실 꽤 어렵습니다! 때로는 명확합니다. 예를 들어, 문법에 대한 명백히 비호환적인 변경은 당연히 호환되지 않는 변경입니다. 하지만 레이아웃이 약간 달라지게 만드는 변경은 어떨까요? 스타일링 속성의 기본값을 바꾸는 변경은요? 내장 show 규칙을 바꿔서 사용자 스타일이 다르게 조합되게 하는 변경은요? 내부 정렬 동작을 바꿨는데 그 결과로 기존의 잘못된 코드가 깨지게 되는 변경은요? 이건 그렇게 단순하지 않습니다. 그리고 저는 이런 서로 다른 종류의 변경을 어떻게 분류할지 정하는 것이, 우리가 호환되지 않는 변경을 어떻게 다룰지에 대한 어떤 약속을 하기 전에 결정적으로 중요하다고 생각합니다.
하지만 그건 일부일 뿐입니다. 1.x에 도달하는 것과 관련된 또 다른 부분은, 제가 Typst에 적용하고 싶은 변경들 중에 어떤 관점에서 보더라도 호환되지 않는 변경인 것들이 몇 가지 있다는 점입니다. 이런 변경들은 한동안 지평선 위에 있었습니다(오픈 소스 공개 이후로 사실상 모두가 커스텀 엘리먼트를 간절히 기다려 왔죠). 아직 일어나지 않은 이유 중 하나는 단순히 기술적으로도, 디자인적으로도 복잡하고 우리가 다른 것들을 우선시해 왔기 때문입니다.
하지만 두 번째로, 점점 더 강해져 온 장애물이 있습니다. 약속이 없었음에도 불구하고, Typst는 사실상 점점 더 안정적인 것이 되어 왔습니다. 최근 Typst 릴리스를 되돌아보면, 호환되지 않는 변경은 더 온화해지고 더 사소해졌습니다. 특히 Typst Universe가 성장하면서, 기반이 되는 무언가를 깨뜨리면 생태계 전반에 파급 효과가 생길 것입니다.
예를 들어, 어떤 새 컴파일러 기능을 쓰기 위해 프로젝트를 업그레이드하고 싶다고 합시다. 그런데 사용하는 패키지 중 하나가 그 버전에서 깨져 있고 아직 업데이트된 버전이 없다면, 할 수 있는 일이 거의 없습니다. 최악이죠. 이제 의존성은 괜찮지만 새 버전에 성가신 문법 변경이 있어서 마크업의 큰 부분을 수정해야 한다고 가정해 봅시다. 그보다는 덜 최악이지만, 여전히 짜증납니다.
SemVer 수준에서라면 “그게 사업의 비용이지, 어차피 우리는 0.x니까”라고 손을 들어버릴 수 있습니다. 그리고 저는 그게 전적으로 공정하다고 생각합니다. 하지만 실제로는 그 이상이 있습니다. 호환되지 않는 변경이 생태계에 고통스럽다면, 유지관리자로서 그것을 만드는 일도 점점 더 고통스러워집니다. “이 변경을 피할 수 있을까?”, “이 세 가지 변경을 묶을 수 있을까?” 같은 생각을 하게 되죠. 이것에는 좋은 점도 있는데, 행동을 신중하게 하도록 강제하기 때문입니다. 하지만 동시에 프로젝트를 느리게 하고, 더 나은 방향을 위한 매우 큰 호환되지 않는 변경이 실제로 일어날 가능성을 줄이기도 합니다.
저는 이 상황에 만족하지 않습니다. 말했듯 저는 Typst가 프로덕션에 준비되었다고 생각하지만, 동시에 _아직 멀었다_고도 생각합니다. 여전히 출시하고 싶은 것들이 많고, 그중 일부는 Typst를 비호환적으로 바꾸어야 합니다. 그래서 저는 0.x 단계에서도 이런 질문을 던질 가치가 충분히 있다고 생각합니다. 어떻게 하면 깨짐을 덜 고통스럽게 만들 수 있을까? 다소 이기적으로 말하자면, 우리가 더 자유롭고 두려움 없이 행동할 수 있게 하기 위해서요.
그럼 우리가 할 수 있는 일은 무엇일까요? 위에서 제가 그린 두 시나리오 중 첫 번째, 더 고통스러운 경우를 봅시다. 여러분은 정말로 그 새로운 레이아웃 기능이 필요하지만, 다이어그래밍 패키지가 아직 업데이트되지 않았고, 게다가 다이어그램도 정말 필요합니다. 여러분이 0.14에서 0.15로 업그레이드하려고 하는데 어떤 호환되지 않는 문법 변경이 있다고 합시다. 이 문제를 해결하는 건 사실 그렇게 어렵지 않습니다. 여러분의 프로젝트와 다이어그램 패키지 사이에는 명확한 경계가 있고, Typst가 둘에 서로 다른 동작을 노출하는 것도 충분히 가능합니다. Typst가 이를 위해 필요한 정보는 아주 조금뿐입니다. 어떤 것에 어떤 동작을 노출할지입니다.
패키지에 Typst가 노출해야 하는 동작은, 그 패키지가 개발되던 시점에 노출하던 동작입니다. 본질적으로 이것은 타깃 Typst 버전에 대해 개발한다는 뜻입니다. 이는 typst.toml에서 compiler 필드를 통해 이미 선언할 수 있는 최소 Typst 버전과 나란한 개념입니다. 따라서 저는 다음과 같은 문법을 상상해 볼 수 있습니다:
[package]
name = "fancy-diagrams"
version = "0.4.0"
entrypoint = "lib.typ"
compiler = { min = "0.15", target = "0.17" }
이렇게 하면 여러분의 패키지는 “0.15 미만에서는 동작하지 않고, 0.17보다 최신이라면 0.17 이후의 호환되지 않는 변경을 내게는 노출하지 말아줘”라고 말하게 됩니다. 그리고 compiler = "0.15"는 둘이 같다는 축약 표기가 될 것입니다.
Rust에 익숙하다면, 이것이 아마 Edition 메커니즘을 떠올리게 할 텐데, 기술적으로는 действительно 기본적으로 그거와 같습니다. 하지만 사회적으로는 저는 이를 다르게 생각하겠습니다. 시간이 흐르면서 특정 호환 동작을 영원히 유지하겠다고 약속하진 않을 겁니다. 그러면 시간이 갈수록 컴파일러 코드베이스가 크게 복잡해질 수 있으니까요. 대신 한두 버전 정도만 유지해서, 변경이 생태계에 미치는 영향을 완화하고 사람들이 패키지를 적응시키고 마이그레이션할 시간을 주겠습니다. 제게 이것을 생각하기 좋은 방식은 _스테로이드를 맞은 사용 중단 경고_입니다. 실제로 저는 이런 동작이 트리거될 때마다 경고를 띄울 것 같습니다.
또한 target 설정은 안정성의 약속이 아니라는 점을 유의하세요. 어떤 호환되지 않는 변경에 대해 호환 동작을 제공하는 일이 아주 쉽다면 당연히 포함하겠지만, 변경의 영향이 작고 호환 동작을 제공하는 것이 지나치게 어렵거나 심지어 불가능하다면, 우리는 여전히 일반적인 호환되지 않는 변경을 할 수 있습니다. 지금 사용 중단 경고에서 하는 것처럼요.
타깃을 지정하지 않는 패키지에 대해서 무엇을 할 것인지라는 질문이 남습니다. 현재의 패키지 생태계로부터 이 메커니즘의 효과를 최대한 끌어내려면, 가능한 모든 호환 동작을 적용하는 것이 합리적인 선택일 것입니다. 하지만 새 패키지를 만들었는데 갑자기 Typst가 옛날 동작으로 돌아가 버리면 혼란스러울 수 있습니다. 그래서 저는 compiler 필드를 필수로 만들겠지만, 적어도 지금은 오류가 아니라 경고로 이 요구사항을 표현하겠습니다.[1]
Typst 프로젝트 자체(패키지가 아닌)의 경우에는, 필요한 마이그레이션을 적용할 수 있도록 소스에 완전히 접근할 수 있으니 항상 최신 동작을 노출하는 것이 매우 합리적으로 보입니다. 게다가 정말 필요하다면 더 오래된 컴파일러 버전에 머무를 수도 있겠죠. 그렇다 하더라도, 드문 사용 사례를 위해 프로젝트의 타깃 버전을 선택하는 CLI 플래그/설정도 여전히 상상해 볼 수 있습니다.
이 target 메커니즘은 전체 생태계를 즉시 교란하지 않으면서도 우리가 호환되지 않는 변경을 할 수 있게 해줄 것입니다. 사람들에게 마이그레이션할 시간을 벌어줍니다. 하지만 마이그레이션 자체를 더 빠르게 해주지는 않습니다. 특정 호환되지 않는 변경을 어떻게 처리할지는 물론 변경의 성격에 달려 있습니다. 어떤 변경은 문법적으로 사소하게 고칠 수 있을 수도 있고, 복잡한 의미적 변경을 요구할 수도 있습니다.
저는 이런 변경들 중 일부에 대해서는 자동화된 마이그레이션 도구를 제공할 가치가 있다고 생각합니다. 이는 특히 수학 우선순위에 대한 조만간 있을 법한 변경을 위해 우리가 고려하고 있는 부분입니다(배경은 제 블로그 글 “Math Mode Problem”을 참고하세요).
또 다른 잠재적 변경(아직 결정되진 않았지만)으로는 함수 파라미터 문법을 재정비하는 것이 있을 수 있습니다. 현재의 param: default는 param: type = default가 되고, 여기서 : type 부분은 선택적 타입 주석이 됩니다.
이 두 예시는 모두 문법적이며 많은 문서에 영향을 줄 것입니다. 그래서 자동화된 마이그레이션 도구의 이상적인 후보입니다. 더 의미적인 마이그레이션도 가능할 수 있지만, 비용 대비 효과의 절충은 사례별로 판단해야 할 것입니다. 저는 모든 호환되지 않는 변경에 대해 마이그레이션 도구를 제공하겠다고 약속하진 않을 겁니다.
마이그레이션 도구에 대한 구체적인 설계는 아직 없으니, 이 글은 설계 논의를 시작하는 발화점이라고 생각해 주세요. 제 개인적인 우선순위는 발견 가능성, 사용 편의성, 거짓 양성의 회피, 그리고 CLI 및 웹 앱 모두와의 좋은 통합입니다.
구현 수준에서는, 타깃 호환성 메커니즘과 마이그레이션 도구가 실제로는 밀접하게 연결될 수도 있습니다! 예를 들어, 먼저 호환 모드에서 문서를 컴파일하면서 호환 동작이 트리거된 모든 지점을 수집하고, 그 정보를 곧바로 자동 마이그레이션을 구동하는 데 사용하는 것이 합리적일 수 있습니다.
Typst는 아직 버전 번호가 0이지만, 성숙해지면서 프로젝트에는 변화에 대한 저항이 생겼습니다. 이는 변화가 충분히 동기부여되어 있는지 보장해 주므로 프로젝트가 발전하며 갖추게 되는 건강한 특성입니다. 반대로, 더 나은 방향을 위한 큰 변화는 부담스럽게 만들기도 합니다.
기술적으로는 버전 번호 0이 우리에게 호환되지 않는 변경을 자유롭게 할 수 있게 해줍니다. 하지만 이는 사람들이 Typst 위에 구축한 것들을 반복적으로 교란할 때 소모되는 사회적 자본을 무시합니다. 어떤 사람들은 0.x 단계에서 마이그레이션 도구에 투자하는 것은 우선순위를 잘못 잡은 일이라고 주장할지도 모릅니다. 하지만 저는 다르게 생각합니다. 저는 이것이 지금 좋은 투자라고 봅니다. Typst를 더 자유롭게 깨뜨릴 수 있게 해주면서 동시에 안정성에 더 가까워지게 하고, 사람들이 그토록 바라는 버전 번호 “1.0”에도 가까워지게 해주니까요!