TypeScript 7 업그레이드로 Astro check가 실패할 때: 타입 검사를 지우지 말고 의존성을 복구하자
Dependabot이 TypeScript를 6.0.3에서 7.0.2로 올리는 PR을 만들었다. 버전 번호만 보면 평범한 메이저 업데이트다. 하지만 Astro 프로젝트에서는 이 변경 하나로 astro check가 제대로 실행되지 않을 수 있다.
공개된 Astro/Starlight 프로젝트 이슈를 살펴보다가 서로 다른 해결책 두 가지를 발견했다. 첫 번째는 실패하는 astro check를 CI에서 없애는 방법이었다. 두 번째는 TypeScript를 6으로 되돌리고 검사기는 그대로 유지한 다음, Dependabot이 7.x 업데이트를 다시 제안하지 않도록 잠시 제한하는 방법이었다. 실제로 병합된 쪽은 두 번째였다.
단순히 peer dependency에 새 버전이 추가되지 않은 문제만은 아니다. TypeScript 7.0은 네이티브 컴파일러로 전환됐고, 일부 프레임워크 언어 도구는 여전히 TypeScript 6의 프로그래밍 API에 의존한다. Microsoft도 TypeScript 7.0 공식 발표에서 Astro, Vue, Svelte, MDX와 같은 임베디드 언어 도구 체인은 마이그레이션이 끝나기 전까지 TypeScript 6을 유지하도록 권고했다.
증거의 범위부터 분명히 하자. 특정 커밋에 고정된 의존성 파일과 두 PR, 원본 프로젝트의 GitHub Actions를 확인했다. 별도로 Node.js 표준 라이브러리만 사용한 합성 테스트 5개를 직접 실행해, exports 설정 때문에 “설치된 패키지를 찾을 수 없다”고 오진하는 현상을 검증했다. 그러나 원본 Astro 프로젝트에서 npm install이나 astro check는 실행하지 않았다. 아래에서 언급하는 성공적인 Astro CI는 원본 프로젝트의 실행 결과이며 내가 독립적으로 재현한 결과가 아니다.
검사 자체를 없앨까, 호환되는 버전으로 복원할까?
사건은 vishwakarma Issue #8에 기록돼 있다. 먼저 Dependabot PR #3이 TypeScript를 7.0.2로 올렸고, 유지관리자는 새로운 조합에서 astro check가 호환성 문제를 겪는다고 보고했다.
PR #9는 아예 검사를 제거하는 안이었다. apps/web/package.json에서 check 스크립트와 @astrojs/check 의존성을 빼고, Makefile의 typecheck-web도 삭제해 check-web에 포맷 검사와 빌드만 남기는 방식이었다. 이 PR은 병합되지 않고 닫혔다.
실패하던 검사를 실행하지 않으면 CI가 녹색으로 바뀔 수는 있다. 하지만 astro build가 성공했다는 사실은 astro check가 제공하던 .astro 템플릿 및 TypeScript 타입 진단이 정상이라는 뜻이 아니다. 업그레이드가 어렵다는 이유로 기존의 품질 게이트를 없애는 건 문제를 해결한 것과 다르다.
최종 병합된 PR #10은 검사기를 보존하면서 Web workspace의 TypeScript 버전을 원래 호환 조합으로 되돌렸다.
- "typescript": "^7.0.2"
+ "typescript": "^6.0.3"
수정 이후의 고정 커밋을 보면 apps/web/package.json에는 Astro ^7.2.10과 @astrojs/check ^0.9.10이 명시돼 있다. 동일한 커밋의 package-lock.json에서 @astrojs/[email protected]이 선언한 TypeScript peer 범위는 다음과 같다.
"peerDependencies": {
"typescript": "^5.0.0 || ^6.0.0"
}
이는 실제 lockfile의 선언을 읽은 결과다. 내가 npm을 실행해 수집한 오류 로그가 아니다. 적어도 이 버전의 검사기는 TypeScript 7을 지원한다고 선언하지 않았다.
복원한 구성이 실제로 CI를 통과했다는 제3자 증거도 있다. GitHub Actions #38103905437은 PR head 5d828e8d55c5db833f5cca17f28da3b6aba05a99에서 실행됐다. Web 작업의 make setup-web, make check-web와 Python 작업이 모두 성공했다. 이 결과가 확인해 주는 것은 검사기를 유지한 수정 후 구성이지, 내가 TypeScript 7에서 실패하는 장면을 직접 재현했다는 사실이 아니다.
설치된 TypeScript가 왜 ‘설치되지 않음’으로 보일까?
호환성 문제에는 서로 다른 두 층이 있다.
더 근본적인 것은 컴파일러 API다. TypeScript 7.0은 Go 기반의 네이티브 구현으로 전환했다. tsc 명령이 동작하더라도 TypeScript 6에서 제공하던 안정적인 프로그래밍 API가 그대로 들어 있다는 뜻은 아니다. Astro는 .astro 파일 안의 TypeScript를 자체 언어 서비스와 결합해 분석한다. 컴파일러 실행 파일을 업그레이드하는 것만으로 그 연동이 완성되지는 않는다.
다른 층은 오류 메시지를 잘못된 방향으로 이끈다. Astro의 상위 이슈 #17268에는 TypeScript 7 패키지의 exports 변경으로 이전 검사 로직인 require.resolve('typescript')가 ERR_PACKAGE_PATH_NOT_EXPORTED를 낼 수 있다고 기록돼 있다. 모든 모듈 해석 예외를 “TypeScript 미설치”로 처리하면 사용자는 엉뚱한 문제를 고치려고 시간을 쓰게 된다.
이 메커니즘만큼은 직접 확인하고 싶었다. Astro나 실제 TypeScript 7을 설치하지 않고 Node.js v22.16.0 표준 라이브러리로 두 개의 임시 모의 패키지를 만들었다. 하나는 기존 방식의 엔트리 포인트를 제공한다. 다른 하나는 패키지 루트를 내보내지 않고 ./package.json 하위 경로만 노출했다. 합성 테스트에서 사용한 핵심 설정을 읽기 좋게 정리하면 이렇다.
{
name: 'native-ts-synthetic',
version: '7.0.0-fixture',
type: 'module',
exports: {
'./package.json': './package.json'
}
}
이 가짜 패키지에 require.resolve('native-ts-synthetic')를 실행하면 파일이 실제로 있는데도 ERR_PACKAGE_PATH_NOT_EXPORTED가 나온다. 하지만 ./package.json을 명시적으로 공개했으므로 native-ts-synthetic/package.json은 찾을 수 있다. 처음부터 존재하지 않는 패키지에서는 두 경로 모두 MODULE_NOT_FOUND를 반환한다.
격리된 Linux / Node.js v22.16.0 환경에서 npm 서드파티 패키지 없이 node --test ./node-exports-resolution.test.cjs를 실행했다. 아래는 실제 Node TAP 테스트 출력의 일부이며 Astro 오류 로그를 꾸민 것이 아니다.
# OBSERVED: legacy package resolution = FOUND
# OBSERVED: package exists; require.resolve(root) = ERR_PACKAGE_PATH_NOT_EXPORTED
# OBSERVED: require.resolve(package.json) = FOUND
# OBSERVED: absent package = MODULE_NOT_FOUND in both paths
# OBSERVED: catch-all = false missing; code-aware fallback = detected installed
# tests 5
# pass 5
# fail 0
5개 테스트는 모두 통과했다. 여기서 검증된 것은 특정 Node.js 패키지 해석 동작이지 실제 TypeScript 7, Astro, astro check의 호환성이 아니다. package.json을 대체 경로로 사용하는 것도 만능은 아니다. 실제 패키지가 해당 하위 경로를 export하지 않았다면 이 방법 역시 실패한다.
미설치 오진을 수정하는 것만으로 TypeScript 7.0에 없는 기존 프로그래밍 API가 생기는 것도 아니다. Astro의 PR #18053은 2026년 9월 병합되어 TypeScript 7 관련 오류 안내를 개선했다. 해당 이슈가 닫혔다는 사실을 TypeScript 7.0의 Astro 타입 검사 완전 지원으로 해석해서는 안 된다.
영향받은 Astro 프로젝트의 CI를 복구하는 방법
우선 현재 설치된 TypeScript와 @astrojs/check 버전, lockfile의 peer 조건, 실제 검사 오류를 확인하는 편이 좋다. Dependabot이 메이저 업그레이드를 제안했다고 해서 프레임워크의 모든 언어 도구까지 준비됐다는 보장은 없다.
이 공개 프로젝트는 apps/web 아래 web이라는 npm workspace를 사용한다. 다음 명령은 실제 PR #10을 참고해 정리한 독자가 자신의 수정 브랜치에서 실행할 수 있는 절차다. 원본 프로젝트에 대해 내가 실행한 명령은 아니다.
# 현재 의존성 확인
npm ls typescript @astrojs/check -w web
# 이 사례에서 선택한 호환 버전으로 복원
npm install --save-dev 'typescript@^6.0.3' -w web
# lockfile을 기준으로 재설치하고 검사·빌드 수행
npm ci
npm run check -w web
npm run build -w web
의존성 충돌이 있으면 npm ls 자체가 0이 아닌 종료 코드를 낼 수 있으며, 이것도 조사에 도움이 되는 신호다. 원래 커밋을 보존하거나 새 수정 브랜치를 만든 뒤 Node/npm 요구 버전과 실제 workspace 이름을 확인하자. 단일 패키지 프로젝트에서는 -w web이 필요 없다. ^6.0.3은 이 특정 버전 조합의 대응책이지 모든 Astro 프로젝트의 영구 처방이 아니다.
동일한 자동 업데이트가 다시 들어오는 일도 막아야 한다. 고정 커밋의 .github/dependabot.yml에는 npm 업데이트에만 적용되는 임시 규칙이 있다.
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
ignore:
- dependency-name: typescript
versions: [">=7.0.0"]
이 ignore는 반드시 해당 npm 설정에 한정하자. 관계없는 패키지의 업데이트까지 막을 이유는 없다. 제거 조건도 남겨 두는 것이 좋다. Astro 검사기와 언어 서비스가 새로운 통합 방식에 대응하고 자신의 CI가 통과할 때 규칙을 다시 검토해야 한다. 그렇지 않으면 임시 우회책이 영원한 버전 고정으로 굳어진다.
이 프로젝트의 check-web에는 포맷 검사, astro check, 빌드가 포함된다. 수정 검증은 lockfile이 의도한 버전을 담았는지, npm ci가 재현 가능한 설치를 수행하는지, astro check가 실제로 실행되는지, 빌드도 정상인지 각각 확인해야 한다. 기존에 .astro 타입 오류를 감지하는 테스트가 있다면, 여전히 그 오류를 발견하는지도 확인하는 편이 더 확실하다.
TypeScript 7.1 이후에는 버전 고정을 풀어도 될까?
버전을 잠시 낮췄다고 해서 새로운 컴파일러를 영원히 피해야 하는 것은 아니다. Microsoft는 TypeScript 7.0 발표 당시 새로운 프로그래밍 API와 @typescript/typescript6처럼 기존 도구와 최신 tsc를 공존시키는 방법을 설명했다. 다만 tsc 자체의 실행, Astro 템플릿 분석, 에디터 언어 서비스, CI 타입 검사는 각각 별도의 검증 대상이다.
이번 자료 조사 시점에 Astro #17268은 닫혀 있고, 오류 메시지를 개선하는 PR #18053은 병합됐다. 반면 .astro와 TypeScript 7 Content Mapper를 연결하려는 PR #17852는 열려 있었다. 이 내용은 특정 시점의 소스 상태이므로 향후 버전의 지원 상황까지 보증하지 않는다.
TypeScript 메이저 업데이트 뒤 astro check가 실패했다면 먼저 오류와 의존성 계약을 확인하고 검사기를 유지한 채 호환되는 조합으로 복원한 다음 lockfile과 CI를 검증하자. 새로운 방식으로의 마이그레이션은 따로 계획하면 된다. 중요한 것은 빨간 배지를 없애는 게 아니라, 원래 있던 타입 검사가 계속 제 역할을 하도록 만드는 일이다.
왜 개인 기술 블로그에 Astro를 선택했는지 궁금하다면 이전에 쓴 Astro·Markdown·GitHub Actions 비교 글도 참고할 수 있다. 이번 사례는 Astro를 선택한 후에 자동 의존성 업데이트가 기존 품질 게이트를 조용히 없애지 않도록 하는 문제다.