원문 캡처 · ismail.karsli.net
원문 캡처 · ismail.karsli.net

TypeSQL 유지관리자는 TypeScript 7.1의 콘텐츠 매퍼(content mapper)를 쿼리 분석 결과를 저장하는 캐시에 활용해, 500개 쿼리의 타입 검사 시간을 자체 벤치마크에서 2.6초에서 0.13초로 줄였다고 밝혔다. 이 기능은 TypeScript 7.1 베타에 포함됐으며, 사용자가 설정에서 허용해야 작동한다.

TypeSQL은 일반 문자열로 작성한 PostgreSQL 쿼리의 행·매개변수 타입과 잘못 쓴 열 이름을 타입 검사하는 라이브러리다. 코드 생성 단계나 데이터베이스 연결 없이 TypeScript의 조건부 타입과 템플릿 리터럴 타입으로 SQL을 분석한다.

const q = sql("SELECT id, email FROM users WHERE id = $1", 42);
// ^? Query<{ id: number; email: string }, [number | null], "SELECT id, ...">

sql("SELECT nmae FROM users");
// ~~~~~~~~~~~~~~~~~~~~~~~~ column "nmae" does not exist. Perhaps you meant the column "name"?

일반적인 쿼리 하나를 분석하는 데 타입 인스턴스화가 약 9,000회 필요하며, 유지관리자의 측정으로는 검사 시간이 약 5밀리초다. 쿼리가 500개인 앱에서는 캐시가 없는 상태로 tsc를 실행할 때 타입 검사에 약 2.6초가 걸린다. TypeScript는 한 프로그램 안에서 결과를 기억하지만, 새 tsc 실행이나 편집기에서 새 검사기가 만들어지면 이전 계산 결과를 재사용하지 못한다.

콘텐츠 매퍼를 캐시로 활용

TypeScript 7.1의 콘텐츠 매퍼는 .vue나 .svelte 같은 확장자의 파일을 TypeScript 코드로 변환하도록 설계된 기능이다. tsconfig.json에서 패키지와 처리할 확장자를 지정하면 TypeScript가 해당 파일을 발견했을 때 패키지의 별도 프로세스에 내용을 전달하고 TypeScript 코드를 받아 검사한다.

유지관리자는 이 기능을 컴포넌트 변환이 아닌 캐시에 사용했다. 캐시 파일에는 JSON을 저장하고, 매퍼가 이를 타입 선언으로 바꾼다. 라이브러리는 SQL 파서를 실행하기 전에 쿼리 문자열이 선언된 캐시에 있는지 확인한다. 결과가 있으면 이를 사용하고, 없으면 기존 방식대로 파싱한다.

매퍼 프로세스는 백그라운드에서 별도의 TypeScript 세션을 열어 프로젝트의 sql() 호출을 찾고 결과를 계산해 캐시 파일에 기록한다. 편집기는 파일 변경을 감지해 매퍼를 다시 실행한다. 유지관리자는 이 방식에 별도 CLI, 편집기 확장, 앱 코드가 필요하지 않다고 설명한다.

측정 결과와 검증

아래 수치는 유지관리자가 4코어 머신에서 빌드된 패키지를 대상으로 skipLibCheck를 켜고 측정한 결과다. 앱에서 패키지를 컴파일하는 방식에 맞춘 조건이라고 밝혔다.

조건타입 인스턴스화검사 시간
쿼리 500개, 캐시 없음4.6M2.6초
쿼리 500개, 캐시 사용123K0.13초
쿼리 50개, 캐시 없음517K0.31초
쿼리 50개, 캐시 사용20K0.02초

유지관리자는 이 결과가 검사 시간 약 95% 감소, 타입 인스턴스화 97% 감소에 해당한다고 밝혔다. 스키마가 달라 캐시가 완전히 오래된 경우의 비용은 캐시가 없을 때와 비슷한 1.01배라고 설명했다. 변경이 적은 550개 쿼리 프로젝트에서는 편집 후 영향을 받는 쿼리만 다시 계산하며, 백그라운드 작업 한 번이 1초보다 짧게 끝난다고 덧붙였다.

정확성 확인에는 285개 테스트와 천 개가 넘는 타입 단언이 포함된 전체 타입 테스트를 캐시를 생성한 뒤 다시 실행하는 방법을 썼다. 모든 단언이 통과했고, 작업량의 약 73%가 캐시에서 처리됐다. 오류 스냅샷이 캐시 사용 여부와 관계없이 바이트 단위로 같아야 한다는 검사도 포함했다. 열 이름 변경, 널 허용 여부 변경, 열거형 값 추가 등 스키마를 바꾸는 테스트에서는 영향을 받는 캐시 항목만 제외되는지 확인했다.

언어 서버 테스트에서는 tsc --lsp를 실행해 캐시에서 호버 정보를 가져오는지, 백그라운드 작업이 새 항목을 기록한 뒤 서버가 이를 반영하는지 확인했다. 캐시 사용 전후의 호버 텍스트를 비교한 테스트에서는 같은 SQL 문자열에 중복 항목이 생겨 Query<…> | Query<…>로 표시되는 버그도 발견했다. 타입은 동등했지만 표시가 달랐다는 설명이다.

사용 조건과 한계

이 기능은 선택 사항이며, 사용하려면 아직 베타인 TypeScript 7.1이 필요하다. 정식 버전이 나오기 전에 콘텐츠 매퍼 사양이 바뀔 수 있다. tsc에서는 --runExternalCode 옵션이 필요하고, 편집기에서도 매퍼 실행을 허용해야 한다. 패키지의 코드가 편집기에서 실행되므로 사용자의 동의가 필요하다고 유지관리자는 설명한다. 백그라운드 작업 중에는 프로젝트를 보유한 두 번째 TypeScript 인스턴스가 실행되며, 몇 분 동안 유휴 상태가 되면 종료된다.

캐시 사용을 허용하지 않으면 동작은 바뀌지 않는다. 캐시가 없는 경우에도 캐시가 있는지 확인하는 작업 때문에 쿼리당 비용이 이전보다 약 0.2% 증가한다고 밝혔다. 설정 방법은 TypeSQL 타입 캐시 안내에서 확인할 수 있다.

부록: 원문 예시

text
{
 "contentMappers": [{ "package": "@typesql/core", "extensions": [".typesqlcache"] }]
}
text
declare module "@typesql/core" {
 interface QueryCache {
 "…": {
 "SELECT id, email FROM users WHERE id = $1": {
 a: { kind: "select"; cols: [ /* … */ ] }; // the analysis, already done
 c: { q: Query<{ id: number; email: string }, [number | null], "…"> };
 // …
 };
 };
 }
}