tsc가 내 템플릿을 보고 멋진 문자열을 발견하다
요약
본 글은 컴파일러가 없는 라이브러리(Relaxjs)에서도 TypeScript의 타입 검사를 활용하여 템플릿 오류를 포착하는 방법을 다룹니다. 특히 `compileTemplate` 함수에 타입을 부여함으로써, 빌드 단계에서부터 잘못된 속성 사용이나 이벤트 핸들러 오타 등을 강력하게 잡아낼 수 있음을 보여줍니다.
핵심 포인트
- 컴파일러 부재 라이브러리도 타입 시스템으로 템플릿 검사 가능
- 잘못된 사용을 타입 오류로 만드는 것이 가장 가치 높음
- `compileTemplate`에 타입을 부여하여 빌드 단계에서 에러 포착
- 검사기는 `{{path}}`, `if`, `loop` 등 모든 세그먼트의 타입을 추론함
coding agent와 @relax.js/core를 사용하는 시리즈의 여섯 번째 글입니다. 이 내용은 첫 번째 기사에서 제가 수정해야 한다고 느꼈던 부분입니다.
격차 (The gap)
Angular에서 작업했을 때, 템플릿의 잘못된 속성은 빌드 오류였습니다. vue-tsc도 Vue에 대해 동일하게 작동합니다. Relaxjs는 컴파일러가 없다는 점을 강점으로 내세우고 있으며, 그 대가는 템플릿이 문자열이라는 것입니다: {{user.naem}}은 텍스트이기 때문에 완벽하게 유효한 TypeScript입니다.
앞선 두 기사는 테스트에서, 오류를 포착하여 실행 시점에 잡는 것에 관한 내용이었습니다. 그것은 작동하지만, 에이전트가 기억해야 하는 세 가지 요소에 달려 있습니다: 테스트 작성, 대표 모델로 렌더링, 포착된 오류에 대한 단언(assert). 이 중 하나라도 빠지면 오타가 배포됩니다.
제가 에이전트들을 위해 작성하는 디자인 노트에서 계속 돌아가는 규칙은 다음과 같습니다: 잘못된 사용을 타입 오류로 만드는 API는 실행 시점 경고를 만드는 API보다 훨씬 가치가 높습니다. 왜냐하면 아무도 콘솔을 보고 있지 않기 때문입니다. 따라서 질문은 빌드 단계가 없는 라이브러리가 어쨌든 템플릿에 대한 타입 검사를 할 수 있느냐였습니다.
호출에 타입을 부여하기 (Give the call its types)
compileTemplate은 두 개의 타입 매개변수를 받습니다: render()가 받는 뷰 모델과, 두 번째 인수로 받는 함수 컨텍스트입니다.
interface ViewModel {
heading: string;
}
...
그리고 다음과 같이 사용합니다:
this.template = compileTemplate<ViewModel, Handlers>`
`<form class=
그리고 출력은 다음과 같습니다. 문자 그대로:
src/pages/ProfilePage.broken.ts:21:27 - error: Cannot resolve "profile.emial": Profile has no property "emial"
src/pages/ProfilePage.broken.ts:22:39 - error: "r-clik" is not a known event for <button>
src/pages/ProfilePage.broken.ts:26:39 - error: "user.name" is not a property name; html templates take flat names
...
종료 코드 1. 형식은 `tsc`의 것이며, 표현식당 한 줄, 열 번호는 호출이 아닌 표현식을 가리킵니다. 에디터와 에이전트는 이미 이를 읽는 방법을 알고 있습니다.
세 번째 줄이 제가 가장 마음에 드는 부분입니다. `html` 태그는 플랫 이름(flat names)을 사용합니다. 즉, `{{user.name}}`은 하나의 키로 조회되므로, 객체에 `user`가 있더라도 오류가 발생합니다. 검사기는 어노테이션 없이도 컨텍스트 타입(context type)을 알고 있습니다. 바인드 호출(`card({ user: ... })`)에서 가져오는데, 이는 즉시 호출이거나 템플릿이 저장된 파일 내의 변수 또는 클래스 필드의 첫 번째 호출일 수 있습니다.
## 무엇이 검사되는가 (What is checked)
- 모든 `{{path}}`, `if`, `unless` 및 핸들러 인자: 각 세그먼트는 그 앞 타입의 속성(property)입니다. `null`과 `undefined`는 런타임에서 빈 값으로 렌더링되므로 조회됩니다. 배열에 대한 `[0]`은 요소 타입을 제공합니다. `any`는 탐색을 종료시킵니다.
- `loop="row in rows"`: `rows`는 배열이며, `row`는 그 요소 내부의 요소 타입을 가지고 있고, 해당 요소가 닫힐 때 스코프를 벗어납니다.
- `{{fn(a, 'x', 1)}}` 및 `r-click="fn(row, event)"`: `fn`은 인자 개수와 타입을 받는 호출 시그니처를 가진 함수 타입의 속성입니다. `event`는 핸들러에서만 `Event`입니다.
- `r-<event>`: 요소의 DOM 타입에 존재하며, 런타임이 수행하는 것과 동일한 테스트입니다.
- 파이프(Pipes): 내장된 기능 중 하나이며, 호출이 자체 레지스트리(registry)를 전달하지 않는 경우 제외합니다.
- `{{`는 같은 줄에서 닫힙니다.
## 무엇이 건너뛰어지고, 그 내용 (What is skipped, and said so)
변수에 저장된 템플릿, `${}` 치환으로 빌드되었거나 파일에서 로드된 템플릿은 호출 지점에 텍스트가 없습니다. 타입 인자 없이 호출되는 `compileTemplate`도 구문(syntax), 이벤트 및 파이프 검사는 수행하지만 경로 해석(path resolution)은 하지 않습니다. 반환되거나 다른 파일에서 호출되는 `html` 바인드 함수는 컨텍스트 타입을 가지지 못합니다. 이 모든 경우들은 그 이유와 함께 요약에 계산됩니다:
12 templates checked, 2 skipped (1 no type argument, 1 not a literal)
템플릿 스킬은 이를 다음과 같이 표시합니다: 건너뛴 템플릿이 있는 침묵(silence) 상태는 녹색이 아닙니다.
## 런타임과 불일치하지 않는 이유
체커의 명백한 위험은 자신이 검사하는 것에서 벗어나는 것입니다. 두 가지 결정 사항이 이를 작게 유지합니다. 표현식 문법(expression grammar)(`{{expr}}`, 파이프, `loop`, 함수 호출)은 런타임과 체커가 공유하는 모듈 중 하나이므로, 이들은 표현식을 다르게 구문 분석할 수 없습니다. 그리고 이벤트 테스트는 런타임의 테스트입니다: 요소 타입에 대한 `on<event>`입니다.
공유되지 않는 것은 HTML 구조입니다. 런타임은 `DOMParser`를 사용하고; 체커는 DOM 없이 Node 환경에서 실행되므로 작은 스캐너로 템플릿을 읽습니다. 이 스캐너는 브라우저의 콘텐츠 모델 규칙을 적용하지 않으므로, 블록 요소에 의해 일찍 닫히거나 `<table>` 밖에 있는 `<tr>` 같은 태그가 `loop` 별칭을 렌더링하는 방식과 다르게 범위 지정할 수 있습니다. 템플릿을 잘 형성되게 유지하면 두 시스템은 일치합니다. 문서에서 이 점을 언급하는 이유는 에이전트가 읽을 수 없는 예외 사항은 존재하지 않는 예외 사항이기 때문입니다.
## 테스트 스위트를 실행하게 하라
명령어는 실행될 때만 도움이 됩니다. 에이전트는 변경 사항이 있을 때마다 `npm test`를 실행하며, 반드시 `check`를 실행하는 것은 아닙니다. 따라서 예제 앱에는 이 테스트가 있고, 스킬은 프로젝트에 하나가 부족하면 이를 추가하도록 에이전트에게 지시합니다:
const tsconfig = path.resolve(__dirname, '../tsconfig.json');
describe('templates', () => {
...
두 번째 단언(assertion)은 엄격한 것입니다: 이 프로젝트의 어떤 템플릿도 전체 검사보다 적게 받지 않습니다. 만약 정말로 동적인 템플릿이 어딘가에 있다면 이를 제거하세요.
typescript는 선택적 피어 의존성(optional peer dependency)입니다. 런타임에서는 아무것도 이를 로드하지 않으며, 오직 `check`만 합니다.
## 이것이 포기하는 것과 얻는 것
Angular는 컴파일러 내부에서 템플릿을 검사하며, 에디터 통합 기능과 함께 템플릿의 정확한 문자 위치에 오류를 표시합니다. 이는 호출 시 타입이 지정된 리터럴(literals with types) 템플릿에 대해 별도의 명령을 통해 정확한 문자 및 `tsc` 형식을 얻습니다. 하지만 에디터 밑줄(editor squiggles)은 얻지 못하며, 파일에서 로드되는 템플릿도 볼 수 없습니다.
가장 중요한 것은 에이전트입니다. 이제 실패는 이미 실행하는 명령의 출력에 있는 한 줄로 나타나며, 메시지는 타입과 속성 이름을 명시합니다. 다음 단계: 전체 세션(full session)을 거쳐 테스트 통과를 위한 프롬프트까지 나아갑니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기